Pular para o conteúdo

Domain Model

O domínio canna-br é organizado em 8 bounded contexts com fronteiras explícitas. Comunicação entre contextos ocorre exclusivamente por domain events e referências por ULID — nunca por FK direta entre contextos. Cf. ADR-001.

ContextoAggregate RootCore Invariante
MembershipMemberConsentimento + prescrição válidos antes de qualquer dispensação; quota mensal enforced
CultivationCultivationBatchPlant ULID permanente; progressão de estágio forward-only; destruição requer testemunha
ProcessingHarvestBatchApenas RESPONSAVEL_TECNICO aprova lab; COA hash imutável pós-aprovação
InventoryInventoryLotLiberação requer LabSampleApproved; RECALLED é terminal e irreversível
DispensationDispensationImutável após criação; DispensationRecorded + MemberQuotaConsumed + LotQuantityDeducted em 1 append atômico
ComplianceRead model somenteNunca emite commands; lê projeções; relatórios são value objects imutáveis
FinanceFinancialStatementValores como Decimal(15,2); BiologicalAssetValuation (CPC 29) criado automaticamente por HarvestRecorded
Identity & AccessUserTOTP obrigatório para roles críticos; segregação RDC 1.014: dispensador ≠ aprovador COA ≠ cultivador
cultivation_batch (ULID)
└── plants[] (ULID permanente — tag física QR)
└── harvest_batches (ULID)
└── processing_runs (ULID)
└── lab_samples (ULID) → laudo PDF MinIO
└── inventory_lots (ULID)
└── dispensations (ULID)
└── SNGPC XML (batch diário)

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

Quota válida + Receita vigente + Lote AVAILABLE
→ RecordDispensation
→ decide() verifica quota E estoque
→ append único: DispensationRecorded
+ MemberQuotaConsumed
+ LotQuantityDeducted
→ BullMQ async: SNGPC XML, PDF recibo, email

Optimistic concurrency no stream do lote: dois RecordDispensation paralelos no mesmo lote — o segundo falha por versão divergente e é re-validado. Sem 2PC.

Membership: MemberRegistered, ConsentRevoked, MedicalRecordExpired, QuotaUpdated, MemberAnonymized

Cultivation: CultivationBatchStarted, PlantRegistered, PlantStageAdvanced, PlantDestroyed, HarvestRecorded

Processing: ProcessingRunCompleted, LabSampleSubmitted, LabSampleApproved, LabSampleRejected

Inventory: LotQuarantined, LotReleased, LotExhausted, LotRecalled

Dispensation: DispensationRecorded, MemberQuotaConsumed, LotQuantityDeducted, QuotaExceededAttempt, SngpcXmlGenerated, SngpcXmlSent

Finance: BiologicalAssetValued, DreGenerated, MensalidadeRecorded

InvarianteEnforcement
Quota mensal (Membership × Dispensation)Use case RecordDispensation verifica antes de criar aggregate
Liberação de lote requer lab aprovado (Processing × Inventory)Event handler de LabSampleApproved
Audit log imutável (todos os contextos)PostgreSQL RULE bloqueia UPDATE/DELETE em event_log — independente de app layer
Segregação RBAC (RDC 1.014)Middleware HTTP antes de cada use case; não bypassável via API direta
COA hash imutável pós-aprovaçãoPostgreSQL column-level trigger como segunda linha de defesa
  1. Domain Layer (TypeScript) — guards dentro dos aggregates antes de emitir qualquer event
  2. Application Layer — pré-condições cross-context nos use cases (role, estado de entidades em outros contextos)
  3. Database LayerUNIQUE constraints, CHECK clauses, RULE no_update/no_delete, column triggers
  4. RBAC Layer — middleware requireRole() antes de qualquer handler; nenhum bypass possível via REST direta
RolePermissões principais
ADMINGestão de usuários, configuração do tenant
RESPONSAVEL_TECNICOAprovação de COA, assinatura BSPO
DISPENSADORRegistrar dispensações
CULTIVADORRegistrar plantas, avançar estágios
FINANCEIROVisualizar/gerar DRE e financeiro
AUDITORLeitura completa, sem escrita
MEMBROAcesso ao próprio prontuário