Ponto de partida
Como ler
Caixa cheia é agregado especificado; borda azul é raiz de agregado ou árvore; borda tracejada é classe da plataforma gryd, que o Nexio consome e não possui. ? no fim do nome é anulável. Campo em azul nasceu ou mudou em 01/09/2026.
id e tenantId não aparecem: toda classe tem os dois, id atribuído pelo agregado e tenantId injetado do token e nunca aceito do cliente. createdAt, updatedAt e isDeleted também são universais. Classes com muitos campos trazem a contagem do que ficou de fora.
Este documento é derivado. Regra, invariante e contrato de erro vivem na spec de cada módulo; aqui só a forma. Divergência entre os dois é defeito deste arquivo — a spec ganha sempre.
Banda 1 · gryd e a fronteira
Plataforma e escopo
O usuário é global e não pertence a tenant; tudo o que o liga a uma organização passa por UserTenant. O token do GrydAuth traz userId, tenantId, papéis e permissões — e não traz empresa. O conjunto permitido é resolvido no servidor a cada requisição.
A multiempresa não encosta no gryd. A plataforma não conhece Company e não deve conhecer: pôr companyId em UserRole faria a camada de baixo depender de uma tabela de um produto que roda sobre ela. Por isso o recorte mora em UserCompanyScope, do Nexio, referenciando o userId do gryd sem chave estrangeira — o mesmo padrão que CostCenterAssignment já usa hoje, inclusive na consulta de elegibilidade ao GrydAuth. A tabela só estreita, nunca concede: quem dá organização, papel e permissão continua sendo o token, então linha órfã é inerte e ausência de linha significa todas as empresas.
Banda 2 · o dinheiro
Organização e dinheiro
Duas árvores — centro de custo e plano de contas — com a mesma mecânica: parentId, caminho materializado, depth, allowsPosting só em folha, duas portas de status e path como token de concorrência.
Os dois eixos se cruzam em um lugar só. CostAllocation é o único ponto do modelo onde centro de custo e conta contábil se encontram, e ele é value object da linha do documento — não vive em nenhum dos dois. Derivar a conta do centro de custo, ou o contrário, é o erro que transforma relatório contábil em relatório gerencial errado.
Banda 3 · o que se compra
Catálogo e fornecimento
A terceira árvore com a mesma mecânica é a taxonomia. E a simetria que importa: Company/Establishment do lado de dentro tem a mesma forma de Supplier/SupplierEstablishment do lado de fora — raiz de 8 e CNPJ de 14, nos dois.
Nada aqui tem companyId, e é decisão, não esquecimento. Item, tipo, categoria, unidade e fornecedor são vocabulário do tenant inteiro. Um item que existe só numa empresa é um item que ninguém mais acha; um fornecedor homologado por empresa é a mesma diligência paga N vezes. O recorte por empresa começa no documento, não no cadastro.
Banda 4 · quem decide
Motor de aprovação
Sete classes de configuração e quatro de execução. A separação é a tese do módulo: a instância congela as políticas que a formaram, com versão — editar a regra não reescreve o que já está em curso.
Empresa entra aqui como critério, não como coluna. ApprovalCriteria ganhou empresa e estabelecimento na lista de recortes, e ApprovalRequest carrega a empresa do documento. Dar um companyId à própria política criaria um segundo mecanismo concorrente para a mesma pergunta — e o critério já resolve, inclusive o caso de uma política Base do grupo convivendo com uma Complementar de uma empresa só.
Integridade
Cardinalidades
Toda relação do modelo, com a regra que a governa. É a tabela que responde "posso apagar isso?".
Plataforma e escopo
| Relação | Cardinalidade | Regra |
|---|---|---|
| User → UserTenant | 1 · N | Uma linha por organização. Remover o vínculo apaga em cascata papéis e permissões diretas naquela organização |
| UserTenant → UserRole | 1 · N | O papel é sempre dentro de um tenant. Não existe papel global |
| UserCompanyScope → Company | N · 0..1 | Nulo = todas as empresas do tenant; ausência de linha, também. O escopo efetivo é a união das linhas vigentes. FK composta (tenantId, companyId) → companies(tenantId, id), com dois índices únicos parciais — um para o caso nulo, outro para o preenchido |
| UserCompanyScope → User | N · 1 | userId do gryd sem FK — é outro serviço. Validado contra o diretório na escrita; linha órfã é inerte, porque o token nunca mais traz aquele tenant |
| Estabelecimento padrão do usuário | — | Não é tabela. Preferência de interface em localStorage, seguindo o precedente do próprio gryd. "Preferências do usuário" é conceito da plataforma; uma tabela homônima no Nexio criaria duas fontes para a mesma pergunta |
Organização e dinheiro
| Relação | Cardinalidade | Regra |
|---|---|---|
| Company → Establishment | 1 · 1..N | Exatamente um Headquarters. Empresa ativa nunca fica sem estabelecimento ativo; companyId é imutável |
| Establishment → cnpj | — | Os 8 primeiros caracteres têm que ser o cnpjRoot da empresa. Divergência é 422 |
| GLAccount → GLAccount | 0..1 · N | Árvore. Ciclo recusado; accountType herdado do pai e divergir dele é 422 |
| GLAccount × Company | N · N | Via GLAccountCompany. Ausência de linha = habilitada — a linha existe para restringir ou carregar o código externo |
| CostCenter → CostCenter | 0..1 · N | Árvore. Nó de empresa não aceita filho de outra empresa; nó corporativo aceita qualquer filho |
| CostCenter → Company | N · 0..1 | Nulo = corporativo. Aponta para a empresa, nunca para o estabelecimento |
| CostCenter → CostCenterAssignment | 1 · N | Mesma pessoa e papel não podem ter períodos sobrepostos no mesmo nó — garantido por EXCLUDE no banco, não em memória |
| CostAllocation → CostCenter / GLAccount | N · 1 / N · 0..1 | Só nó e conta com allowsPosting. Soma dos percentuais fecha em 100 com regra de resíduo declarada |
| NumberSequence → Company / Establishment | N · 0..1 | Exigidos conforme o scope; preenchidos fora dele é 422 |
Catálogo, fornecimento e aprovação
| Relação | Cardinalidade | Regra |
|---|---|---|
| CategoryNode → CategoryNode | 0..1 · N | Árvore de profundidade livre, com teto de política em TenantSettings |
| CategoryNode → GLAccount | N · 0..1 | Sugestão. A busca sobe a árvore: o nó mais próximo com conta preenchida vence |
| Item → GLAccount | N · 0..1 | Primeiro degrau da cascata; a categoria é o segundo. Sempre editável na linha |
| Item → UnitOfMeasure | N · 1 | baseUomId imutável depois do primeiro documento. Conversões vivem no item, não na unidade |
| Supplier → SupplierEstablishment | 1 · 1..N | Espelha Company → Establishment. Filial pode estar baixada com a matriz ativa |
| ItemSupplier → Item / Supplier | N · 1 | supplierEstablishmentId compõe a chave; nulo vale para todos os estabelecimentos do fornecedor |
| ApprovalPolicy → ApprovalLevel | 1 · 1..N | Política sem nível é recusada na publicação. Editar publica versão nova; a anterior segue respondendo pelas instâncias vivas |
| ApprovalRequest → ApprovalStep → ApprovalAssignment | 1 · 1..N · 1..N | A instância congela as políticas com versão. ApprovalEvent é append-only e é a fonte da verdade da trilha |
O que o diagrama não mostra
Invariantes entre agregados
Regras que atravessam duas ou mais classes e por isso não cabem em nenhuma caixa.
A empresa da linha tem que bater com a do nó
Uma linha de documento da empresa A só aponta para centro de custo corporativo ou da empresa A. Validado na linha, com o companyId efetivo lido do nó — não no cadastro, porque o cadastro é anterior ao documento.
Nulo significa a mesma coisa em toda parte
CostCenter.companyId nulo é nó corporativo. UserCompanyScope.companyId nulo — e a ausência da linha — é acesso a todas as empresas. ItemSupplier.supplierEstablishmentId nulo vale para todos os estabelecimentos do fornecedor. GLAccountCompany ausente é conta habilitada. Uma convenção só — nulo é "todos", nunca "nenhum" e nunca "não sei".
Três árvores, uma mecânica
CostCenter, CategoryNode e GLAccount compartilham caminho materializado, código imutável, ancestrais sem recursão, cascata assimétrica de status e path como token de concorrência. Uma quarta forma de fazer árvore neste produto seria erro por escolha.
Documento congela; cadastro muda
Preço, fator de conversão, cadeia de aprovação, dados do fornecedor, empresa e conta são copiados para a linha quando ela nasce. Nenhuma seta deste diagrama é lida em tempo de execução por um documento já emitido.
Aprovador precisa de escopo na empresa do documento
Um nível que resolve para alguém sem companyId compatível — nem a empresa do documento, nem nulo — conta como nível vazio: cai no fallbackApproverId e registra o motivo na trilha. Sem isso, o motor produz tarefa para quem não consegue abrir o documento.
O escopo só estreita, e nunca atravessa a organização
UserCompanyScope não concede organização, papel nem permissão — isso continua vindo do token do GrydAuth. Ela responde apenas em quais empresas daquele tenant o que o token já concedeu tem efeito. Daí duas consequências: linha órfã é inerte, e ausência de linha pode ser permissiva sem risco.
O que precisa ser garantido é o outro lado — a empresa tem que ser do mesmo tenant da linha. Aí a chave é composta e local, dentro do esquema do Nexio: (tenantId, companyId) → companies(tenantId, id). E a unicidade são dois índices parciais, não um: no Postgres dois nulos não colidem, e "todas as empresas" duplicado passaria batido.
Dois CNPJs com o mesmo nome são coisas diferentes
Establishment é comprador; SupplierEstablishment é vendedor. Foi por isso que ItemSupplier.establishmentId virou supplierEstablishmentId em 01/09/2026 — com as duas classes existindo, o nome curto era ambíguo em toda consulta.
Vocabulário
Enumerações
| Enum | Valores | Onde |
|---|---|---|
| TenantType | Company | Group | Plataforma. Group é eixo de acesso e nunca guarda dado |
| TaxRegime | SimplesNacional | MEI | LucroPresumido | LucroReal | TaxExempt | NotApplicable | Company e Supplier — o mesmo enum dos dois lados |
| EstablishmentType | Headquarters | Branch | Establishment e SupplierEstablishment |
| IcmsTaxpayerStatus | Taxpayer | Exempt | NonTaxpayer | Establishment. É o indIEDest da NF-e |
| RegistrationStatus | Active | Suspended | Inactive | Closed | Situação cadastral, dos dois lados |
| AccountType | Asset | Liability | Equity | Revenue | Expense | GLAccount. Herdado do pai |
| SequenceScope | Tenant | Company | Establishment | NumberSequence. Padrão Company |
| ResetPolicy | Never | Yearly | Monthly | NumberSequence. Padrão Yearly |
| AssignmentRole | Owner | Deputy | Watcher | CostCenterAssignment |
| PredominantUse | ProductionInput | Resale | ConsumableUse | FixedAsset | PersonalBenefit | Item e ItemType. Critério de aprovação |
| UomClass | Count | Weight | Volume | Length | Area | Time | Service | UnitOfMeasure. Conversão só dentro da classe |
| SupplierEntityType | Company | MEI | Individual | RuralProducer | Foreign | Supplier. Governa a obrigatoriedade de todo o resto |
| CompanySize | NotInformed | ME | EPP | Other | Supplier. Indicativo, nunca verdade contratual |
| PolicyKind | Base | Complementar | ApprovalPolicy. Base concorre, complementar empilha |
Governança
Convenções e dívidas
| Convenção | Estado | Detalhe |
|---|---|---|
| Identificadores em inglês | vale | Entidade, atributo, valor de enum, rota, permissão e código de erro. Exceção: dados de negócio brasileiros — cnpj, primaryCnae, SimplesNacional, siglas de unidade |
| Sufixo do erro decide o HTTP | vale | _NOT_FOUND → 404 · _CONFLICT e _ALREADY_EXISTS → 409 · _UNPROCESSABLE → 422 · sem sufixo → 400 |
| Fora de escopo responde 404 | vale | Registro que existe mas não está no escopo do usuário se comporta como inexistente. 403 confirmaria a existência |
| Estado só por rota própria | vale | /activate e /deactivate; PUT nunca muda isActive |
| Valores de enum do motor de aprovação | dívida | mode (Qualquer | Todos | Quorum), scopeMode (Documento | Fatia) e os estados de ApprovalRequest, ApprovalStep e ApprovalAssignment estão em português, contra a convenção. Normalizar antes de existir dado |
establishmentId dentro do agregado do fornecedor | dívida | SupplierAddress e SupplierContact ainda usam o nome curto. Sem ambiguidade dentro do agregado, ambíguo na leitura. Renomear quando essas classes forem tocadas |
Limites
O que ainda não está aqui
Oito agregados a especificar. A ordem e o custo de adiar estão no mapa de domínio.
| Classe | Onde encaixa | O que já está reservado |
|---|---|---|
Requisition | Espinha transacional · a primeira | establishmentId obrigatório, companyId derivado e congelado, linhas com CostAllocation, número por NumberSequence |
PurchaseOrder | Espinha transacional | Mais requestingEstablishmentId, para compra centralizada |
Receipt · Invoice | Espinha transacional · three-way match | A nota chega endereçada a um CNPJ de 14 e já traz IBS e CBS desde 03/08/2026 |
QuotationRequest | Espinha transacional | Depende da decisão sobre portal do fornecedor |
Contract | Estrutural | ItemSupplier.contractId já existe como gancho |
DeliveryLocation | Estrutural | ItemSupplier.shipToLocationId reservado; aponta para o Establishment que recebe fiscalmente |
Budget | Estrutural | Verba por empresa × centro de custo × conta × período. Os três eixos existem |
| Transversais | Attachment · AuditLog · Notification · ExternalRef · ReasonCode | NumberSequence já saiu com a spec de empresa |