Pular para o conteúdo

Chain of Custody — ULID

Cada planta tem um ULID atribuído no momento da germinação. Esse identificador acompanha o material vegetal até a dispensação final, passando por todos os estágios intermediários:

cultivation_batch (ULID)
└── plants[] (ULID permanente — tag física impressa)
└── harvest_batches (ULID)
└── processing_runs (ULID)
└── lab_samples (ULID) → laudo PDF no MinIO
└── inventory_lots (ULID)
└── dispensations (ULID)
└── SNGPC XML (por dispensação + batch diário)

Cada seta representa uma relação de rastreabilidade auditável. Nenhum material entra no estoque sem ter um inventory_lot ligado a um harvest_batch, que por sua vez está ligado a plants com ULIDs permanentes.


CritérioUUID v4Auto-increment INTULID
Sortable por tempoNãoSim (sequencial)Sim
Globalmente únicoSimNão (por DB)Sim
URL-safeParcial (hífens)SimSim
Sem coordenação de servidorSimNãoSim
Legível por humanosNãoSimParcial
Collision probability (80 bits)2^122N/A2^80

ULID escolhido por:

  1. Sortable por tempo — relatórios cronológicos sem ORDER BY created_at extra em índices compostos
  2. Globalmente único sem coordenação — múltiplos workers podem gerar ULIDs em paralelo sem sequência centralizada
  3. URL-safe — IDs aparecem em URLs administrativas e tags QR impressas
  4. Auditável visualmente — os primeiros 10 caracteres codificam o timestamp, facilitando investigação manual

O ULID da planta é gerado no registro do cultivation_batch e atribuído a cada plant individualmente. Esse mesmo ULID é:

  1. Armazenado no banco de dados como chave primária imutável
  2. Impresso em tag física (QR code ou código de barras) fixada na planta
  3. Usado para rastrear todas as transformações subsequentes do material
plant.id = "01HQ7XKZM3N4P5Q6R7S8T9V0W"
Tag física: QR → /plants/01HQ7XKZM3N4P5Q6R7S8T9V0W

Regra absoluta: Plant ULID nunca é reutilizado, mesmo após destruição documentada da planta. Registros de destruição referenciam o ULID original.


O audit log é imutável na camada de banco de dados, não na camada de aplicação. Nenhum código de aplicação pode apagar ou alterar entradas de auditoria.

-- Tabela de audit log
CREATE TABLE audit_log (
id ULID PRIMARY KEY DEFAULT gen_ulid(),
table_name TEXT NOT NULL,
record_id TEXT NOT NULL,
action TEXT NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
old_data JSONB,
new_data JSONB,
actor_id ULID REFERENCES users(id),
actor_role TEXT NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
ip_address INET,
session_id TEXT
);
-- Bloquear UPDATE na audit_log
CREATE RULE audit_log_no_update AS
ON UPDATE TO audit_log
DO INSTEAD NOTHING;
-- Bloquear DELETE na audit_log
CREATE RULE audit_log_no_delete AS
ON DELETE TO audit_log
DO INSTEAD NOTHING;
-- Revogar permissão de TRUNCATE do role da aplicação
REVOKE TRUNCATE ON audit_log FROM app_role;

Adicionalmente, pgAudit registra eventos DDL diretamente nos logs do PostgreSQL, criando uma segunda camada de auditoria fora do alcance da aplicação.


CampoTipoDescrição
idULID PKIdentificador do lote de cultivo
strain_idULID FKCepa (genetics)
start_dateDATEData de plantio
expected_plantsINTQuantidade planejada
grow_mediumTEXTSubstrato (coco, terra, hidro)
location_idULID FKÁrea de cultivo
statusENUMseedling / vegetative / flowering / harvested
CampoTipoDescrição
idULID PKPermanente. Nunca reutilizar.
batch_idULID FKcultivation_batch de origem
tag_printed_atTIMESTAMPTZQuando tag física foi impressa
destroyed_atTIMESTAMPTZNULL se viva, timestamp se destruída
destruction_reasonTEXTDoença, pragas, voluntária
CampoTipoDescrição
idULID PKIdentificador do lote de colheita
plant_idsULID[]Plantas colhidas neste lote
harvest_dateDATEData de colheita
wet_weight_gDECIMAL(10,2)Peso úmido total (g)
dry_weight_gDECIMAL(10,2)Peso seco após cura (g)
thc_percentageDECIMAL(5,2)Resultado do lab (pode ser NULL antes do laudo)
cbd_percentageDECIMAL(5,2)Resultado do lab
CampoTipoDescrição
idULID PKIdentificador do lote de estoque
processing_run_idULID FKProcessamento de origem
product_typeENUMflower / oil / extract / capsule
quantity_gDECIMAL(10,2)Quantidade em estoque (decrementada por dispensações)
available_fromDATENão dispensar antes desta data
expires_atDATEValidade
CampoTipoDescrição
idULID PKIdentificador da dispensação
member_idULID FKMembro que recebeu
lot_idULID FKinventory_lot de origem
quantity_gDECIMAL(10,2)Quantidade dispensada
dispensed_atTIMESTAMPTZTimestamp exato (usado no SNGPC XML)
dispensed_byULID FKUsuário responsável
prescription_idULID FKPrescrição médica vinculada
sngpc_batch_idTEXTID do batch SNGPC enviado