Ponto de partida
Como ler
Caixa cheia é agregado especificado; borda azul marca a classe que se lê ao lado de outras bandas — raiz, árvore ou filha que carrega uma invariante — e não é a contagem de raízes de agregado, que é do mapa de domínio; borda tracejada é classe da plataforma gryd, que o Nexio consome e não possui. ? no fim do nome é anulável.
id e tenantId não aparecem. id é atribuído pelo agregado; tenantId é injetado do token e nunca aceito do cliente. Exceções declaradas: sem tenantId — Currency e CurrencyPair, catálogo do produto, e User, da plataforma; com tenantId anulável, em que nulo é linha global do produto — ExchangeRate (cotação oficial), ReasonCode, AttachmentType e UnitOfMeasure. createdAt e updatedAt também não aparecem; isDeleted não aparece e não existe nas trilhas append-only (BudgetEntry, ApprovalEvent). 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 do CostCenterAssignment. A tabela só estreita, nunca concede: organização, papel e permissão continuam vindo do token. E ela falha fechada: sem linha vigente, a pessoa não alcança empresa nenhuma — não existe linha nem nulo que signifique todas (RN-EMP-15). A linha de quem sai da organização é encerrada pela remoção, e a readmissão não a revive (RN-EMP-24).
Banda 2 · o dinheiro
Organização e dinheiro
Onze classes. 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, path como guarda de move e version int como token de concorrência. O DeliveryLocation pendura no estabelecimento e cruza com a árvore de custo por sugestão, e a linha de documento congela a entrega no DeliveryAddressSnapshot. A numeração é configuração (NumberSequence) mais contador por partição (NumberSequenceCounter).
Establishment porque é ele quem recebe fiscalmente, e só sugere o centro de custo; o que a linha congela da entrega são colunas próprias, e o deliveryLocationId é só rastro. A fatia de rateio não tem empresa: usa a do documento.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. E a fatia não carrega empresa: usa sempre a empresa do documento, e é contra ela que o nó, a conta e o Budget são lidos (RN-CC-34).
Banda 3 · a verba
Orçamento
Seis classes, e a única do modelo que é livro-razão: BudgetEntry só aceita INSERT, o saldo é projeção e todo estorno é linha nova de sinal contrário — e sobre o grupo: estornar um ato estorna, na mesma transação e na mesma proporção, cada entrada que ele gravou (RN-O43). O movimento carrega dois motivos de naturezas diferentes: entryReason, obrigatório, que a máquina escolhe, e a tripla do ReasonCode, anulável, que só existe quando houve uma pessoa.
BudgetBalance soma por tupla — o saldo de uma linha é lido na hora, das tuplas que ela cobre. BudgetPolicy pendura as quatro políticas no nó de centro de custo em vez de engordá-lo com colunas, e ganha vigência por isso. Currency e ExchangeRate estão na banda seguinte porque são transversais: cotação, pedido e nota consomem as mesmas.Três estágios, e cada um liquida o anterior. BudgetEntry.stage percorre Commitment → Obligation → Actual conforme o documento avança, e a passagem de estágio grava o movimento novo junto com o negativo no estágio anterior, apontado por liquidatesEntryId e limitado ao saldo aberto dele, no mesmo postingGroupId (RN-O42). É a auto-relação tracejada no topo do diagrama, e é o que impede o mesmo dinheiro de ser contado duas vezes entre a requisição, o pedido e a nota.
Banda 4 · a conversão
Moeda e câmbio
Cinco classes transversais. ExchangeRate é uma das quatro classes do modelo com linha global de tenantId nulo: ExchangeRate.tenantId nulo é a cotação oficial compartilhada por todos os clientes. O tipo decide a origem: PtaxVenda e PtaxCompra só existem oficiais; Manual, Contracted e Commercial, só do tenant — e por isso a taxa do tenant nunca sombreia a oficial (RN-CUR-08). A mudança do tipo de taxa e da janela de idade é proposta por uma pessoa e efetivada por outra, e fica registrada em CurrencySettingsChange, listada entre os satélites.
Currency é catálogo do produto e não tem tenantId — o cliente ativa, não cria. CurrencyPair é o que faz a captura automática existir: sem a lista de pares, a rotina diária não sabe o que buscar, e ela se mantém sozinha porque ativar uma moeda garante o par. ExchangeRate não se edita: corrigir é gravar linha nova e apontar supersededById na antiga — daí a auto-relação. E o movimento que já converteu guarda taxa, tipo, data e quoteUnit como valores, não como chave estrangeira, para que substituir a cotação corrija o futuro sem reescrever o passado.Banda 5 · o que se compra
Catálogo e fornecimento
A terceira árvore com a mesma mecânica é a taxonomia — idêntica às outras duas também no path em texto, no depth, no allowsPosting e na vigência. 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. O vínculo item × fornecedor é uma relação com filhas: ItemSupplier é a relação — quem fornece, de que estabelecimento, com que status, tolerâncias e preferência —, ItemSupplierUom são as unidades de compra dela, com fator próprio, e ItemSupplierPrice é a versão de preço, filha e append-only. Destino, empresa compradora, modalidade de frete e vigência ficaram no preço; o que identifica a contraparte ficou na relação; a embalagem não entra em chave nenhuma. Contrato não é campo da relação: o futuro ContractItem aponta para ela.
Supplier são coisas diferentes: parentSupplierId é grupo econômico, entre raízes distintas; supersedesSupplierId é sucessão declarada, de um fornecedor inativado para o que o substitui.Nenhum cadastro aqui tem companyId, e é decisão, não esquecimento. Item, tipo, categoria, unidade, fornecedor, relação de fornecimento e qualificação 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. A única coluna de empresa da banda é ItemSupplierPrice.companyId?, e ela não recorta o cadastro: é eixo de preço, anulável — nulo vale para todas as empresas —, e a resolução nunca empresta o preço de outra empresa (RN-ITM-28). O recorte por empresa começa no documento.
Banda 6 · quem decide
Motor de aprovação
Nove classes de configuração desenhadas e quatro de execução. ApprovalProcess é do produto — o tenant só lê; ApprovalProcessSettings e ApprovalSettings são o que o tenant ajusta, por processo e por tenant; ApprovalGroupMember é entidade-filha do agregado ApprovalGroup, que dá vigência ao membro do comitê. Duas classes do motor não têm caixa e estão nos satélites: ApprovalSettingsChange, o registro do ato de controle que muda um parâmetro crítico — quem propõe não efetiva —, e ApprovalRequestExcludedUser, a lista de quem não decide a instância. 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.
Supplier (cadastro e correção de identidade), SupplierQualification, BankAccountChangeProposal, Item, Budget e BudgetChangeRequest guardam um approvalRequestId, e o efeito no assunto commita na mesma transação da decisão que conclui a instância, pelo contrato de desfecho do processo — Applied, Refused(code) ou Failed. Dez processos-semente e quinze critérios; PermissionHolder é o tipo de nível das políticas padrão semeadas. Todo evento da trilha pertence à instância; o da tarefa carrega também o assignmentId.Empresa entra aqui como critério, não como coluna. ApprovalCriteria tem empresa e estabelecimento entre os recortes, e ApprovalRequest carrega a empresa do documento quando o assunto tem uma (subjectHasCompany) — os quatro processos de cadastro do tenant não têm. 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 Supplemental de uma empresa só.
Banda 7 · a prova
Anexo e arquivo
Seis classes, e duas delas não são nossas. É a banda em que o Nexio depende, em cada operação, de uma entidade de outro bounded context — e a dependência é sem chave estrangeira, dos dois lados.
Attachment aponta para o StoredFile e para um dono polimórfico, e nenhum dos dois é chave estrangeira: o primeiro é de outro DbContext, o segundo é de outro contexto do próprio Nexio. Quem sustenta a integridade é o registro de descritores por ownerType, o evento de descarte de rascunho e a varredura diária. Cada anexo tem a sua própria FileReference: a do original nasce no GrydFiles, na liberação do arquivo, a partir da referência esperada que o Nexio declara na confirmação; a da cópia propagada, o Nexio cria depois do próprio commit — nenhuma escrita atravessa a transação dos dois módulos. AttachmentLegalHold e AttachmentErasureRequest são atos de controle: a guarda é por caso, filha do anexo, e liga o legalHold do arquivo; o pedido de eliminação é sobre o arquivo, com todas as cópias, e aponta para o StoredFile sem chave estrangeira. As setas para Company, Supplier e ReasonCode saem da banda e estão nas cardinalidades.A regra de corte, em uma frase. Arquivo com metadado próprio — número, órgão emissor, validade, status derivado da data — é entidade do domínio: ItemDocument e SupplierDocument, que guardam storedFileId, nunca URL. Os dois são satélites: não têm caixa aqui, e os campos vivem na spec do agregado dono — ver a seção de satélites. Arquivo que é só evidência de um documento é Attachment. Sem essa linha, ou o anexo ganha campos de validade que quase nunca são preenchidos, ou a certidão perde a vigência que é a razão de ela existir.
Banda 8 · o aviso
Notificação
Três classes, e a banda mais magra do modelo — de propósito. Canal, template, fila e entrega são do GrydNotifications; o Nexio guarda a exceção que o usuário configurou e a memória curta que impede o mesmo aviso duas vezes. O catálogo de slugs e o registro de resolvedores de audiência não aparecem aqui porque não são tabela: são registro em código.
NotificationPreference só existe quando o usuário difere do padrão do catálogo — ausência de linha é a configuração, e é isso que faz um slug publicado depois funcionar para quem configurou antes. NotificationThrottle não se liga a nada nesta banda, e não é omissão: é memória de entrega, sem dono, varrida por job — perder a tabela reenvia, não corrompe. A preferência aponta para o usuário e para o catálogo em código, não para o template; as setas que importam saem da banda e estão nas cardinalidades.Onde está o resto do módulo — as classes que não existem. O catálogo de avisos é uma NotificationDefinition por slug, em código, validada na inicialização — mesma escolha do AttachmentOwnerRegistry da banda 7 e pela mesma razão: o que é do produto e muda com release não vira linha que o tenant edita. O tenant personaliza o texto pelo override de NotificationTemplate, que a plataforma já oferece, e não o comportamento. Quantos slugs existem é conta da Notificação, não desta página — aqui só se contam classes.
Banda 9 · o porquê
Motivo
Uma classe, e é a mais conectada do modelo — seis módulos a leem, em cinco bandas: orçamento, moeda, catálogo e fornecimento, aprovação e anexo. Nenhuma dessas setas é desenhada aqui, e não por economia: um ReasonCode não é dono nem filho de nada, ele é uma dimensão que os atos congelam. As relações estão nas cardinalidades.
reasonCodeId · reasonCode · reasonLabel, com os dois últimos congelados no instante da decisão — regra 1 do mapa aplicada a um cadastro de apoio —, e o complemento em texto onde o ato o admite: reasonComment? na regra do Motivo; no motor ele é o comment da tarefa e do evento, e o BudgetEntry carrega só os três primeiros. Não existe contador de uso, não existe coleção de ocorrências e não existe campo de cor, severidade ou peso: cada um desses seria uma porta para o motivo passar a decidir comportamento, que é o que a spec proíbe na primeira regra.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 · 1 | companyId obrigatória: não existe linha "todas as empresas", e sem linha vigente a pessoa não alcança empresa nenhuma (RN-EMP-15). O escopo efetivo é a união das linhas vigentes em [validFrom, validTo), instantes UTC. FK composta (tenantId, companyId) → companies(tenantId, id): empresa de outra organização é recusada pelo banco. Para o mesmo (tenantId, userId, companyId), vigências sem sobreposição — requisito; restrição de exclusão por faixa é exemplo de implementação, e o equivalente é serializar a escrita e conferir sob a trava (RN-EMP-22, RN-EMP-23). A linha não se apaga nem se reescreve: encerrar grava validTo, endedByUserId e endedAt, uma vez |
| UserCompanyScope → User | N · 1 | userId do gryd sem FK — é outro serviço, validado contra o diretório na escrita, nunca na leitura. grantedByUserId é quem concedeu (em CompanyCreation, quem criou a empresa); endedByUserId, quem encerrou por ato — nulo no encerramento de sistema pela remoção da pessoa da organização. A remoção encerra as linhas, e a readmissão não as revive: linha com validFrom anterior ao início do vínculo vigente não é lida (RN-EMP-24). Ninguém mexe nas próprias linhas (RN-EMP-41) |
| Estabelecimento padrão do usuário | — | Não é tabela do Nexio. Vive na UserPreference da plataforma (TenantScopedEntity, única por (userId, tenantId, key)), chave nexio.default-establishment-id — minúsculas, porque a plataforma normaliza a chave —, lida e escrita por GET/PUT /auth/me/preferences/{key} — sem caixa neste diagrama e sem entrar na contagem da plataforma, porque o produto a consome como contrato. "Preferências do usuário" é conceito da plataforma: uma homônima no Nexio criaria duas fontes para a mesma pergunta. Sem esquema e sem cascata: quem lê valida — Guid que não aponta para estabelecimento ativo que a pessoa alcança é ignorado |
Organização e dinheiro
| Relação | Cardinalidade | Regra |
|---|---|---|
| Company → Establishment | 1 · 1..N | Exatamente um Headquarters, que nasce na mesma transação que a empresa (POST /companies com a matriz inline); headquartersId nunca é nulo. Estabelecimento criado depois nasce Branch; trocar a matriz é só PATCH /companies/{id}/headquarters, atômico (RN-EMP-38). Empresa ativa nunca fica sem estabelecimento ativo; desativar a empresa cascateia para os estabelecimentos (RN-EMP-04, RN-EMP-05); companyId é imutável |
| Establishment → Company (cnpjRoot) | N · 1 | cnpjRoot do estabelecimento são os 8 primeiros caracteres do cnpj, e a FK composta (tenantId, companyId, cnpjRoot) → companies(tenantId, id, cnpjRoot) faz o banco recusar raiz de outra empresa; no agregado, 422 ESTABLISHMENT_CNPJ_ROOT_MISMATCH_UNPROCESSABLE (RN-EMP-03) |
| Establishment → DeliveryLocation | 1 · N | establishmentId é obrigatório e imutável — é quem recebe fiscalmente. O estabelecimento precisa ter allowsReceiving. O mesmo galpão servindo três empresas do grupo são três linhas, uma por CNPJ destinatário. Passar allowsReceiving a falso com ponto ativo pendurado é recusado (RN-EMP-17, RN-DL-19) |
| DeliveryLocation → CostCenter | N · 0..1 | defaultCostCenterId sugere o rateio da linha, nunca o impõe. Mão única: o nó de centro de custo não ganha endereço. Obra é nó da árvore; aqui mora só o endereço dela. Canteiro interestadual exige defaultCostCenterId (RN-DL-07). As vigências dos dois não cascateiam; o que existe é aviso (RN-DL-14) |
| DeliveryLocation → stateCode | — | Derivado do cityIbgeCode quando há endereço próprio, copiado do estabelecimento quando não há; nunca é nulo e nunca é digitado. Divergir da UF do estabelecimento torna o ponto interestadual — isInterstate, derivado — e exige explicação (RN-DL-05). Mudar município, UF ou allowsReceiving do estabelecimento revalida todos os pontos dele, na mesma transação (RN-DL-19) |
| DeliveryLocation → recipientCnpj | — | ThirdParty exige recipientCnpj, recipientName e deliveryOperationType; os outros tipos os recusam. A raiz do CNPJ é comparada com o cnpjRoot de toda Company do tenant — leitura, não FK: raiz da empresa destinatária é sempre recusada; raiz de outra empresa do tenant só em ThirdPartyProcessing ou GeneralWarehouse (RN-DL-06, RN-DL-20) |
| Linha de documento → DeliveryLocation | N · 0..1 | deliveryLocationId no cabeçalho e por linha; é rastro, nunca lido para exibir — nulo no endereço eventual (isOneTimeDelivery). A linha congela a entrega em 26 colunas próprias, consultáveis — o DeliveryAddressSnapshot (RN-DL-09); todas as linhas entregam em pontos do mesmo establishmentId do documento (RN-DL-08, 422 DELIVERY_LOCATION_ESTABLISHMENT_MISMATCH_UNPROCESSABLE). Os campos pessoais do eventual são anonimizados ao fim da guarda do documento (RN-DL-21) |
| GLAccount → GLAccount | 0..1 · N | Árvore. Ciclo recusado; accountType herdado do pai e divergir dele é 422 |
| GLAccount × Company | N · N | Via GLAccountCompany, chave (glAccountId, companyId). Ausência de linha = habilitada — a linha existe para restringir ou carregar o código externo. Conta desabilitada na empresa do documento não é sugerida nem aceita na fatia (422 GL_ACCOUNT_NOT_ENABLED_FOR_COMPANY_UNPROCESSABLE, na fatia e na submissão), e o lookup exige companyId. Código enviado ao ERP: GLAccountCompany.externalCode, senão GLAccount.externalCode; sem nenhum, o movimento fica aguardando código externo, nunca sai sem código (RN-EMP-39) |
| CostCenter → CostCenter | 0..1 · N | Árvore. Nó de empresa não aceita filho de outra empresa; nó corporativo aceita qualquer filho. No move para pai de empresa X, toda a subárvore movida precisa ser de X (RN-CC-36) |
| CostCenter → Company | N · 0..1 | Nulo = corporativo. Aponta para a empresa, nunca para o estabelecimento. Imutável depois do primeiro uso — filho, vínculo ou uso declarado por outro módulo pela consulta de uso do .Contracts do Centro de Custo (RN-CC-36). externalCode é único por (tenant, companyId), com nulo colidindo com nulo: dois corporativos não repetem código (RN-CC-38) |
| CostCenter → CostCenterAssignment | 1 · N | Mesma pessoa e papel não têm períodos sobrepostos no mesmo nó — vigência sem sobreposição sobre (tenantId, costCenterId, userId, role, período). É requisito, não recurso de banco: restrição de exclusão por faixa é exemplo, e o equivalente é verificação na aplicação sob trava — nunca em memória sem trava (RN-CC-17). O vínculo exige que a pessoa alcance a empresa do nó; nó corporativo exige escopo completo (RN-CC-30, RN-EMP-27). Alçada muda só pela rota própria (RN-CC-35); approvalLimit nulo é ausência de alçada (RN-CC-31) |
| CostAllocation → CostCenter / GLAccount | N · 1 / N · 0..1 em rascunho, N · 1 na submissão | Só nó e conta com allowsPosting; o nó é corporativo ou da empresa do documento, e a conta está habilitada nessa empresa (GLAccountCompany) — a fatia não tem empresa própria (RN-CC-34). A conta é anulável só enquanto o documento é rascunho — na submissão toda fatia a tem (RN-CC-29, 422 ALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE), o que garante a tupla que BudgetEntry exige. allocationId é a identidade estável da fatia; order é exibição e desempate (RN-CC-33). Soma dos percentuais fecha em exatamente 100, resíduo na maior fatia (RN-CC-22, RN-CC-23) |
| NumberSequence → NumberSequenceCounter | 1 · 0..N | Um contador por partição do scope — um no tenant, um por empresa ou um por estabelecimento —, criado na primeira emissão sob a unicidade (sequenceId, partitionKey) e nunca compartilhado entre configurações (RN-EMP-30). partitionKey nunca é nulo; é ela, e não as FKs, que decide a colisão. A emissão é uma operação atômica sobre o contador, em transação própria antes da do documento; o período só avança, no timeZone da configuração (RN-EMP-31, RN-EMP-32) |
| NumberSequenceCounter → Company / Establishment | N · 0..1 | companyId preenchido em Company e Establishment, nulo em Tenant; establishmentId só em Establishment. FKs anuláveis, sem valor sentinela |
| NumberSequence → NumberSequence | 0..1 · 0..1 | replacesSequenceId. Trocar a forma de numerar é substituir a configuração, nunca editá-la — só padding se edita. Para cada (tenant, documentType), vigências [effectiveFrom, retiredAt) sem sobreposição; a sucessora aponta sempre para a mais recente e só entra com prefixo novo (que não é começo de outro nem começa com outro) ou período novo (RN-EMP-28, RN-EMP-29) |
| Número emitido | — | Único por (tenantId, documentType, companyId, number) em todo documento numerado, em qualquer escopo; composto por prefix + código do estabelecimento (só em Establishment) + período (se resetPolicy ≠ Never) + contador, em varchar(60). Imutável; lacunas são permitidas (RN-EMP-32) |
Orçamento
| Relação | Cardinalidade | Regra |
|---|---|---|
| Company → Budget | 1 · N | Um exercício por empresa e período: sobreposição entre Budget Approved ou Closed da mesma empresa é recusada, excluída a versão substituída (RN-O07, RN-O54). É requisito: exclusão de faixas no banco é exemplo de implementação, com verificação na aplicação onde não houver equivalente |
| BudgetEntry → Company | N · 1 | A empresa do documento — não da fatia, que não carrega empresa própria. Não há rateio entre empresas na v1, e é ela que escolhe o Budget consumido (RN-O40) |
| Budget → BudgetLine | 1 · N | Chave lógica (budgetId, costCenterId, glAccountId, periodKey) em que nulo colide com nulo: duas linhas coringa idênticas no mesmo período são a mesma linha (BUDGET_LINE_ALREADY_EXISTS). É requisito, não recurso de banco — a implementação precisa passar no teste das duas coringas idênticas (RN-O54) |
| BudgetLine → CostCenter / GLAccount | N · 0..1 | Nulo = qualquer. Preenchido, cobre o nó e a subárvore por prefixo de path. Aceita nó sem allowsPosting — orçar em nó intermediário é o caso normal |
| BudgetLine → BudgetEntry | 0..1 · N, só em Adjustment | budgetLineId é obrigatório em Adjustment, que muda a verba de uma linha, e nulo nos três estágios de consumo. O consumo não guarda linha nem exercício: grava a tupla concreta (empresa, centro de custo, conta, balde), e toda linha do exercício que a cobre nas duas dimensões é lida na hora, com a própria política — o pior sinal vence (RN-O31, RN-O08). Cadastrar linha, aprovar versão ou aprovar o exercício com o período começado vale para o que já foi gravado, sem movimento novo (RN-O32) |
| BudgetEntry → BudgetEntry | 0..1 · N, três vezes | liquidatesEntryId: a entrada positiva cujo saldo aberto este negativo consome — liquidação, liberação de resíduo, remanejamento, virada —, limitado ao aberto (RN-O42). reversalOfEntryId: a entrada que este movimento desfaz, no estorno do grupo, que pode ser parcial e proporcional (RN-O43); estorno não se estorna. pairedEntryId: no positivo nascido em par, o alvo do negativo do par; na virada, a entrada levada. Liquidar é avançar, estornar é desfazer — não são a mesma coisa |
| BudgetBalance → Company / CostCenter / GLAccount | N · 1 | Uma linha por tupla concreta — (companyId, costCenterId, glAccountId, competencePeriodKey, stage), só estágios de consumo —, única na tupla. Cache, não verdade: atualizada na mesma transação do movimento, só pelas entradas que de fato entraram, e reconstruível a partir do razão. O saldo de uma BudgetLine não é gravado: é a soma, lida na hora, das tuplas sob ela (RN-O02) |
| BudgetPolicy → CostCenter / Company | N · 0..1 / N · 0..1 | costCenterId nulo = padrão do tenant; companyId nulo = todas as empresas, e a política com empresa vence a de empresa nula para o mesmo campo e o mesmo nó (RN-O37). Cada campo é herdado subindo o path independentemente dos outros. Sobreposição de vigência do mesmo (costCenterId, companyId), com nulo colidindo com nulo, é recusada com BUDGET_POLICY_OVERLAPPING_PERIOD_CONFLICT (RN-O21, RN-O54). É aqui que mora a decisão de bloqueio por verba: o motor de aprovação lê a overBudgetPolicy resolvida e obedece (RN-O26) |
| Budget → BudgetChangeRequest | 1 · N | Origem e destino têm que ser do mesmo exercício. Aprovado, grava Adjustment; o valor efetivo da linha é amount + Σ Adjustment |
| BudgetChangeRequest → BudgetLine | N · 0..1, duas vezes | fromLineId e toLineId: Transfer exige os dois, Supplement só toLineId, Reduction só fromLineId; ambos do mesmo budgetId. Na decisão final de Transfer e Reduction, o disponível da origem é conferido sob trava — faltando, Refused(BUDGET_CHANGE_INSUFFICIENT_UNPROCESSABLE) (RN-O19, RN-O49) |
| BudgetEntry → ExchangeRate | — (sem coluna) | Não há relação: o movimento guarda exchangeRate, quoteUnit, rateType e rateDate como valores, herdados do documento — da submissão ou da última rodada nova — ou da entrada que liquida, no negativo de liquidação (RN-O41, RN-O30, RN-CUR-22). Substituir a cotação corrige o futuro; refletir a taxa corrigida num documento é estorno e relançamento com entryReason = RateCorrection, nunca Adjustment (RN-O53, RN-CUR-11); a reconversão da rodada nova é estorno e relançamento com entryReason = Reconversion, sujeitos a Block (RN-O59) |
Moeda e câmbio
| Relação | Cardinalidade | Regra |
|---|---|---|
| Currency → TenantCurrency | 1 · N | Uma linha por tenant que ativou. BRL nasce ativa e não é desativável na v1; desativar com documento em aberto é 409 |
| Currency → CurrencyPair | 1 · N | Duas vezes, por fromCurrency e toCurrency. Único por par. O par inverso é outro par — não se deriva por inversão, porque compra e venda não são recíprocas. Global: autoFetch é a união das ativações dos tenants, e a edição exige permissão de plataforma |
| CurrencyPair → ExchangeRate | 1 · N | Único em (tenantId, from, to, rateType, rateDate) entre as linhas não substituídas, com tenantId nulo contando como valor — a oficial colide com a oficial. É requisito: índice único filtrado é exemplo de implementação, e conferir sob trava do par e da data é o equivalente. O tipo decide a origem — PtaxVenda e PtaxCompra só com tenantId nulo, gravadas pela rotina e substituídas só pela plataforma; Manual, Contracted e Commercial só com tenantId — e a taxa do tenant não sombreia a oficial (RN-CUR-08, RN-CUR-24) |
| ExchangeRate → ExchangeRate | 0..1 · N | supersededById. Cotação não se edita: corrigir é gravar linha nova e marcar a antiga, com motivo obrigatório. A resolução só enxerga a vigente |
| Tenant → CurrencySettings | 1 · 1 | Tipo de taxa por processo (processRateTypes, em linhas próprias por (tenantId, processCode)) e do movimento sem documento (defaultRateType), janela de idade da cotação, destinatários do alerta e o interruptor do override. É onde mora o defaultRateType que a spec de Orçamento citava |
| CurrencySettings → CurrencySettingsChange | 1 · N | defaultRateType, processRateTypes e maxRateAgeDays são críticos: não mudam pelo PUT (409), e sim por proposta Pending efetivada por outra pessoa com update:currency-settings. Registro de ato de controle: nenhuma escrita além da transição Pending → Applied | Discarded (RN-CUR-28). Satélite, sem caixa |
Anexo e arquivo
| Relação | Cardinalidade | Regra |
|---|---|---|
| StoredFile → Attachment | 1 · N | Sem chave estrangeira — outro módulo, outro DbContext. N porque a propagação faz vários anexos compartilharem o mesmo arquivo. storedFileId é imutável no anexo |
| StoredFile → FileReference | 1 · N | Do GrydFiles. Único por (storedFileId, ownerScope, ownerKey) entre as não liberadas; liberar é append de releasedAt, nunca exclusão |
| Attachment → FileReference | 1 · 0..1 | ownerScope = "nexio.attachment", ownerKey = attachment.id. A referência do original não nasce na confirmação do upload: o Nexio declara, em processo, a referência esperada (expectedOwnerScope · expectedOwnerKey) e o GrydFiles cria a referência na liberação do arquivo — até lá o anexo existe, não é referenciável e não é baixável, e a invariante do GrydFiles ("nada é referenciável antes de ser escaneado") sobrevive intacta. Onde a referência está, o anexo guarda em fileReferenceStatus, sem consultar o outro módulo. Veredito Infected revoga o anexo com FileInfected, e a reprovação (Purged com rejectionCode), com FileRejected — aviso a quem anexou ou, sem ele, ao ResponsibleOf do dono. A cópia propagada cria a sua, depois do commit — com uma referência só, descartar a origem liberaria o arquivo enquanto o destino ainda o exibe. Nenhuma escrita no GrydFiles cruza a transação do Nexio (RN-ATT-35) |
| AttachmentType → Attachment | 1 · N | tenantId nulo no tipo é tipo global do produto; linha do tenant com o mesmo code o sobrepõe — padrão do NotificationTemplate, e o mesmo do ReasonCode. A sobreposição só alonga a retenção e não desliga isControlEvidence (RN-ATT-25). O anexo congela retentionMode e retentionYears no upload: mudar o tipo depois não o alcança. Tipo desativado não invalida anexo existente; trocar de tipo só com o dono em rascunho e entre tipos de mesma retenção |
| Attachment → Attachment | 0..1 · N | copiedFromAttachmentId dá o pai, rootAttachmentId a origem da cadeia sem recursão. Revogar a origem não revoga as cópias |
| Attachment → dono | N · 1 | ownerType + ownerId, sem FK e sem navegação de volta: nenhum agregado tem coleção de anexos. Módulo que registra um ownerType sem descritor completo — doze respostas — derruba a inicialização; valor do enum cujo módulo ainda não existe não é exigido e não aceita anexo (RN-ATT-03). O estado do dono chega pelo evento OwnerStateChanged, que o Anexo consome e grava em lastKnownOwnerState — o dono nunca escreve no anexo (RN-ATT-28) |
| Company → Attachment | 1 · N | companyId derivado do dono e denormalizado, para o filtro por UserCompanyScope caber na linha sem join entre contextos. Fora do escopo é 404 |
| Supplier → Attachment | 0..1 · N | Obrigatório em anexo de origem portal, resolvido do token da RFQ e congelado. É o que dá o isolamento entre concorrentes por comparação de coluna, e o que permite promover o arquivo a SupplierDocument |
| Attachment → AttachmentLegalHold | 1 · 0..N | Uma linha por caso — no máximo uma ativa por (attachmentId, caseReference). Retirar preenche liftedByUserId, liftedAt e liftReason uma vez; a linha nunca é apagada. A primeira guarda ativa sobre qualquer anexo de um arquivo liga o legalHold do StoredFile, e a retirada da última pede o desligamento — depois do commit, com a pendência em fileHoldAppliedAt/fileHoldReleasedAt (RN-ATT-32, RN-ATT-35) |
| StoredFile → AttachmentErasureRequest | 1 · 0..N | Sem chave estrangeira. O pedido é sobre o arquivo e alcança todas as cópias, ou nenhuma. No máximo um Requested por (tenantId, storedFileId); nunca apagado nem reescrito (RN-ATT-33, RN-ATT-38) |
| Attachment → AttachmentErasureRequest | 1 · 0..N | O anexo pelo qual o pedido foi aberto; imutável. Quem registra precisa de escopo e CanRead nele — sem isso, 404 |
Notificação
| Relação | Cardinalidade | Regra |
|---|---|---|
| User → NotificationPreference | 1 · 0..N | Uma linha por exceção, única por (tenantId, userId, slug, channel). Ausência de linha é o padrão do catálogo (defaultChannels); gravar valor igual ao padrão apaga a linha. Só o próprio usuário edita a sua |
| Catálogo (slug) → NotificationPreference | 1 · 0..N | Sem FK: o catálogo é registro em código (NotificationDefinition). Slug desconhecido é 422; canal fora de allowedChannels é recusado |
Catálogo, fornecimento e aprovação
| Relação | Cardinalidade | Regra |
|---|---|---|
| CategoryNode → CategoryNode | 0..1 · N | Árvore de profundidade livre na v1. catalogMaxCategoryDepth é gancho declarado, padrão nulo: enquanto nulo, nada é recusado (RN-ITM-12). Mesma mecânica das outras duas árvores |
| 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 da primeira linha de documento, e nunca unidade de embalagem (RN-ITM-10). O fator resolve em quatro níveis fixos: unidade de compra da relação (ItemSupplierUom) → conversão do item (ItemUomConversion) → conversão global, só intraclasse → erro, nunca 1:1 (RN-UOM-06) |
| Supplier → SupplierEstablishment | 1 · 1..N | Espelha Company → Establishment, para todo entityType: quem não tem CNPJ tem um estabelecimento sem cnpj, e a identidade (cpf, ou taxIdCountry + foreignTaxId) fica na raiz. Filial pode estar baixada com a matriz ativa |
| ItemSupplier → Item / Supplier | N · 1 | A chave da relação tem três partes — itemId, supplierId, supplierEstablishmentId?; o nulo vale para todos os estabelecimentos do fornecedor. A linha de documento aponta para esta caixa e congela, ao lado, a versão de preço e a faixa resolvidas na submissão e a unidade de compra usada (itemSupplierPriceId, itemSupplierUomId). A FK é anulável: compra spot de item catalogado (RN-ITM-39) e linha não catalogada (RN-ITM-36) |
| ItemSupplier → ItemSupplierPrice | 1 · 0..N | Append-only, com a chave (itemSupplierId, validFrom, shipToStateCode?, companyId?, freightTerms?), nulos tratados como iguais, e vigências que não se cruzam dentro da mesma combinação sem validFrom; incoterm? fica fora da chave, só com fornecedor Foreign. Zero é estado legítimo: relação homologada sem preço acordado existe. Reajustar é gravar versão nova, nunca editar — e é isso que faz a camada 3 da antiduplicidade bloquear na relação sem impedir o reajuste |
| Tenant → ApprovalSettings | 1 · 1 | Uma linha por tenant, criada no provisionamento com os padrões do produto: materialidade do rateio (percentual e valor — vale o menor), teto de duração da delegação, expediente, dias não úteis e fuso (timeZone, IANA). A materialidade só muda por proposta que outra pessoa efetiva (ApprovalSettingsChange, RN-AP-69). Mesmo padrão de Tenant → CurrencySettings: entidade por assunto, nunca JSON genérico |
| 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 (RN-AP-06). Cada rodada recria os passos, e os da anterior vão a Obsolete; roundNumber na instância e no passo. Toda decisão serializa na raiz, pelo version da instância — não há token no passo nem na tarefa (RN-AP-44). ApprovalEvent é append-only e é a fonte da verdade da trilha |
| Assunto → ApprovalRequest | 1 · 0..N, no máximo 1 viva por processo | Supplier, SupplierQualification, BankAccountChangeProposal, Item, Budget e BudgetChangeRequest apontam para a instância viva por approvalRequestId, sem FK (contexto do motor). No máximo uma viva por assunto e processo; outra só pelos caminhos declarados, e um assunto já aprovado abre instância nova, nunca reabre a terminada: documento rejeitado que volta, revisão pendente do item (RN-ITM-41) e correção de identidade de fornecedor aprovado, com o cadastro ainda Approved (RN-FOR-39). O assunto submete e aplica o efeito pelo contrato de desfecho; nenhum tem rota própria de aprovar (princípio 7) |
| ApprovalProcess → ApprovalPolicy | 1 · N | O processo é do produto — dez linhas, o tenant só lê; a política é do tenant e aponta o processo por processId. Exatamente uma Base isDefault, de critérios vazios, por processo (RN-AP-03); nos processos sem valor ela nasce semeada e publicada (RN-AP-29) |
| ApprovalProcess → ApprovalProcessSettings | 1 · 1 por tenant | Os parâmetros que o tenant ajusta por processo. Um campo, um dono: núcleo de controle, subjectFlags, política semeada e uso de motivo são do ApprovalProcess. No BANK_ACCOUNT_CHANGE, dedupe, reatribuição, escalonamento e delegação são falsos e travados, e o reserva é proibido (RN-AP-61, RN-AP-03) |
| ApprovalRequest → ApprovalPolicy | N · 1..N, com versão | A instância guarda cada política que a formou com a versão (RN-AP-05, RN-AP-06); passo e evento repetem policyId · policyVersion · levelOrder. Editar a política não alcança instância viva |
| ApprovalLevel → ApprovalGroup | N · 0..1 | Só em approverType = ApprovalGroup. Grupo referenciado por política ativa não fica vazio (409); membro de grupo usado como reserva ou por nível de controle só muda por ApprovalSettingsChange (RN-AP-69) |
| ApprovalGroup → ApprovalGroupMember | 1 · N | Entidade-filha, com vigência; a resolução lê os membros vigentes na ativação do passo e de novo na decisão (RN-AP-33, RN-AP-59) |
| Usuário → ApprovalDelegation | 1 · N | Uma vigente por (delegante, processo); scope nulo = todos e conflita com qualquer outra (APPROVAL_DELEGATION_OVERLAP_CONFLICT). validTo obrigatório, até maxDelegationDays. Não alcança processo não delegable |
| ApprovalRequest → ApprovalRequestExcludedUser | 1 · 1..N | Sempre ao menos o Requester — e o Proponent no BANK_ACCOUNT_CHANGE —, declarados pelo módulo dono na submissão; só cresce: cada reavaliação acrescenta o editor, ninguém sai (RN-AP-60) |
| ApprovalRequest → ApprovalEvent | 1 · 1..N | Append-only, ordenado por (approvalRequestId, sequence), com sequence alocado na transação da decisão; stepId/assignmentId nulos em evento de instância. A única escrita admitida é apagar o texto de comment por LGPD (RN-AP-48) |
| ApprovalStep → ApprovalStep | N · 0..1 | inheritedFromStepId: o passo da rodada anterior cuja aprovação este herdou (RN-AP-21). controlOfStepId: o passo marcado que o passo de controladoria controla (RN-AP-58) |
| Tenant → ApprovalSettingsChange | 1 · N | Registro de ato de controle sobre parâmetro crítico: materialidade do tenant, reserva, alçada estourada, dedupe e delegação do processo, e membros de grupo de reserva ou de controle. Alvo polimórfico, sem FK. Quem propõe não efetiva; nenhuma escrita além de Pending → Applied | Discarded (RN-AP-69) |
Motivo
| Relação | Cardinalidade | Regra |
|---|---|---|
| Ato → ReasonCode | N · 0..1 | Sem FK quando o ato está em outro bounded context. O ato congela reasonCode e reasonLabel no instante da decisão; o relatório agrupa pelo reasonCode congelado, nunca pelo id (RN-RSN-18). Consumidores: ApprovalAssignment e ApprovalEvent (Fluxo), Attachment e a eliminação a pedido do titular (Anexo), Item, Supplier, ExchangeRate, BudgetEntry e BudgetChangeRequest |
| ReasonCode (tenant) → ReasonCode (global) | 0..1 · N, por code | Sobreposição, não colisão: a linha do tenant com o code de um global vale no lugar dele só naquele tenant; code único em cada camada. Escrita sobre id global materializa a linha do tenant e é ato de controle (RN-RSN-06, RN-RSN-22). Não existe apagar |
| ApprovalProcess → ReasonCodeUsage | N · 1 (rejeição) · N · 0..1 (devolução) | rejectReasonUsage e returnReasonUsage, do produto: ItemReject, SupplierReject, BankAccountChangeReject, BudgetReject, BudgetChangeReject nos processos donos, ApprovalReject nos demais; devolução ApprovalReturn, nula no BANK_ACCOUNT_CHANGE (RN-AP-30, RN-RSN-16) |
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 fatia é da empresa do documento, e o nó tem que bater com ela
Toda fatia de rateio de um documento da empresa A só aponta para centro de custo corporativo ou da empresa A. A fatia não carrega empresa: a empresa é a do documento — derivada do establishmentId e congelada —, e é contra ela que o companyId efetivo do nó é lido, fatia a fatia, e que o Budget consumido é escolhido (BudgetEntry.companyId). Não há rateio entre empresas na v1 (RN-CC-34, RN-EMP-06, RN-O40); como demanda de várias empresas se consolida é decisão da spec da Cotação, dentro da RN-EMP-26 da Empresa. Validado no documento, não no cadastro, porque o cadastro é anterior ao documento.
O documento tem três eixos, e só três
Quem recebe fiscalmente (establishmentId, com allowsReceiving), quem pediu (requestingEstablishmentId, nunca nulo — igual ao establishmentId quando quem pede é quem recebe —, da mesma empresa e com allowsPurchasing) e a empresa do documento (companyId, derivada do establishmentId validado contra o escopo e congelada). companyId nunca vem do cliente nem do token (RN-EMP-08). Não existe buyerEstablishmentId: quem opera a compra é usuário, resolvido por UserCompanyScope. Estabelecimento de outra empresa como quem pediu é 422 REQUESTING_ESTABLISHMENT_COMPANY_MISMATCH_UNPROCESSABLE (RN-EMP-33, RN-EMP-34). Toda escrita resolve exatamente uma empresa (RN-EMP-26).
Entrega em outra UF é derivada e sempre explicada, nos três caminhos
Quando a UF de entrega difere da do estabelecimento destinatário, a entrega é interestadual — isInterstate no ponto, deliveryIsInterstate na linha, derivados e nunca digitados — e precisa de uma de três explicações: recebedor distinto com deliveryOperationType (RN-DL-20); type = ConstructionSite com defaultCostCenterId, só no ponto cadastrado (RN-DL-07); ou interstateNote, a única explicação do endereço eventual sem recebedor distinto (RN-DL-12). Sem UF divergente, ou com recebedor distinto, interstateNote é recusada; e recebedor distinto exige deliveryOperationType em qualquer UF. Verificada no cadastro do ponto, no uso dele em documento e no endereço eventual (RN-DL-05). É a única invariante do modelo que existe por causa de legislação estadual. O Nexio não calcula o imposto nem enquadra a operação; ele garante que a dimensão exista, esteja congelada na linha e chegue ao ERP fiscal.
Nulo significa a mesma coisa em toda parte — no cadastro
CostCenter.companyId nulo é nó corporativo. ItemSupplier.supplierEstablishmentId nulo vale para todos os estabelecimentos do fornecedor; ItemSupplierPrice.shipToStateCode e companyId nulos valem para qualquer UF e qualquer empresa; SupplierBankAccount.supplierEstablishmentId nulo, para todos os estabelecimentos. GLAccountCompany ausente é conta habilitada. Attachment.companyId nulo é anexo do tenant — visível em qualquer empresa que o usuário enxergue —, porque quatro dos donos com spec não têm empresa (Item e Supplier são vocabulário e cadastro do tenant; SupplierQualification e a proposta bancária pendem de um fornecedor) e o ApprovalRequest é condicional: herda a empresa do alvo quando ele tem uma. O filtro é "empresa no escopo ou empresa nula", a mesma forma de GLAccountCompany e BudgetPolicy — e, para quem não tem empresa nenhuma no escopo, nada; sem essa linha as duas implementações possíveis falham em silêncio — a estrita faz a certidão do fornecedor sumir para todo mundo, a frouxa a mostra a quem não deveria vê-la. BudgetLine.costCenterId e glAccountId nulos valem para qualquer nó e qualquer conta; BudgetPolicy.costCenterId nulo é o padrão do tenant, e BudgetPolicy.companyId nulo vale para todas as empresas — preenchido vence nulo no mesmo nó e no mesmo campo (RN-O37).
Uma convenção só — nulo é "todos", nunca "nenhum" e nunca "não sei". Ela vale para abrangência de cadastro, nunca para acesso: no UserCompanyScope não há nulo nem ausência que libere — sem linha vigente, nenhuma empresa (RN-EMP-15), porque um cadastro abrangente demais aparece numa tela e se corrige, e um acesso abrangente demais entrega dado de outra empresa sem ninguém ver. Exceções, todas declaradas: CostAllocation.glAccountId nulo significa "ainda não escolhida", existe só em rascunho, e a submissão o recusa (RN-CC-29); NumberSequenceCounter.companyId e establishmentId nulos significam "a partição não tem essa dimensão" — decidido pelo scope da configuração, sem valor sentinela (RN-EMP-30); ItemSupplierPrice.freightTerms nulo é "não se aplica", e só existe fora de mercadoria (RN-ITM-27); e, no fornecedor, cnpjRoot, cnpj, establishmentType e registrationStatus nulos significam "não se aplica a este entityType" — decidido pelo tipo de pessoa, nunca pelo usuário (RN-FOR-03). establishmentType tem um segundo nulo, declarado: em Company e MEI é "ainda não verificado" — lido só da consulta à Receita, nunca inferido, e enquanto nulo nada que dependa de "ser matriz" é decidido.
Toda aprovação humana é uma instância do motor
Onde uma segunda pessoa decide sobre o pedido de outra, o agregado dono submete ao motor (POST /approvals) e aplica o efeito na transação da decisão que conclui a instância, pelo contrato de desfecho do processo; nenhum módulo tem rota própria de aprovar ou rejeitar. Atos unilaterais com motivo — bloqueio e desbloqueio, promoção manual, revogação, substituição, escape de duplicata, expurgo, desligar legal hold — não são aprovação. Princípio 7, RN-AP-28 e RN-AP-38 do Fluxo.
Há segundas pessoas sem instância, todas declaradas na página dona e nenhuma com rota de "aprovar": a confirmação da carga inicial do catálogo (RN-ITM-42) e a da carga inicial bancária (RN-FOR-31) — janela única por tenant, dupla custódia sobre o mesmo arquivo, registro do ato de controle —, a confirmação por segunda pessoa da alteração ChainContact com telefone confiável e fora de trava (RN-FOR-34), a execução, por outra pessoa, da eliminação a pedido de titular quando o arquivo é prova de controle, Permanent ou ainda está sob guarda (RN-ATT-38), a contestação registrada por dentro, que só produz efeito quando outra pessoa confirma (RN-FOR-30), a publicação de política, que não pode ser feita por quem a criou nem por quem a editou (RN-AP-55), e a efetivação das mudanças de parâmetro crítico do motor e da moeda — quem propõe não efetiva (RN-AP-69, RN-CUR-28). Se a dupla custódia fora do motor é uma forma de aprovação ou uma categoria própria de ato de controle está em decisão; até lá, cada uma vale só onde a spec dona a declara, e segunda pessoa sem instância fora delas é defeito.
Três árvores, uma mecânica
CostCenter, CategoryNode e GLAccount compartilham caminho materializado em texto (path com separador nas duas pontas + depth, publicado como level), código imutável, ancestrais sem recursão, allowsPosting só em folha com exceção declarada, vigência opcional, cascata assimétrica de status por rotas próprias de ativar e desativar, move só por PATCH /parent com path como guarda, e version int + expectedVersion como token de concorrência. Uma quarta forma de fazer árvore neste produto seria erro por escolha.
O que é saldo é projeção; o que é fato é linha
BudgetBalance é a única classe do modelo que existe só por desempenho, e a regra que a mantém honesta é ser reconstruível a partir de BudgetEntry a qualquer momento. Ela guarda uma linha por tupla concreta — empresa, centro de custo, conta, balde e estágio —, não por linha de orçamento: o saldo de uma BudgetLine é a soma, lida na hora, das tuplas sob ela nas duas dimensões (RN-O02). Divergência entre projeção e razão é defeito, não decisão. Vale a mesma disciplina do ApprovalEvent, que é append-only — a única escrita é apagar o texto de um comentário por LGPD — e é a prova da trilha: quando um número precisa ser explicado, ele não pode ser uma coluna que alguém somou.
A competência é a data de necessidade, não a do documento
O balde orçamentário de um movimento vem de quando a coisa precisa existir. A requisição de dezembro para entrega em janeiro consome janeiro — o que mata a corrida de fim de ano e faz o compromisso que atravessa a virada já nascer no exercício certo. É por isso que Requisition nasce obrigada a expor data de necessidade por linha, e não só no cabeçalho.
Documento congela; cadastro muda
Preço — a versão e a faixa usadas —, os dois fatores (convNum/convDen da unidade pedida e priceConvNum/priceConvDen da unidade de preço) com a origem deles (factorSource) e a unidade de compra usada, dados do fornecedor, empresa, conta, dados fiscais de quem recebe (taxRegime, icmsTaxpayerStatus, stateRegistration, suframaCode — RN-EMP-37) e o endereço de entrega (RN-DL-09) são copiados para o documento — no nascimento da linha ou na submissão (RN-UOM-10, RN-ITM-37, RN-ITM-39). A cadeia de aprovação é congelada na instância, na submissão (RN-AP-06).
Nenhuma seta deste diagrama é lida em tempo de execução por um documento já emitido — com uma exceção declarada no motor: quem decide cada passo é resolvido na ativação dele (vínculos de centro de custo, membros de grupo, delegação vigente — RN-AP-07), e a reavaliação recasa contra as políticas vigentes na data dela (RN-AP-47).
Aprovador precisa de escopo na empresa do documento
O filtro é por pessoa, nunca por nível: toda resolução — ativação, delegação, reatribuição, escalonamento e reserva — só dá tarefa a quem tem conta ativa, act:approvals (e a permissão do nível, em PermissionHolder), linha vigente de UserCompanyScope na empresa do documento — e ausência de linha é nenhuma empresa — e não está entre os excluídos da instância (RN-AP-59). Nos tipos que caminham a árvore isso é garantido antes, no vínculo, que só é criado para quem alcança a empresa do nó (RN-CC-30, RN-EMP-27); nos demais, a publicação exige gente elegível em cada empresa que a política alcança (RN-AP-40). Nível que o filtro esvazia cai no reserva — fallbackApproverType · fallbackApproverRef, tarefa ByFallback —, marca resolvedOutOfScope no passo e avisa a controladoria (RN-AP-39); em processo com valor, o reserva segue authorityExceededBehavior (RN-AP-58). Nos processos cujo assunto não tem empresa vale só a permissão. Sem isso, o motor produz tarefa para quem não consegue abrir o documento.
O escopo só estreita, falha fechada 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, e responde fechado: sem linha vigente, nenhuma empresa — não existe nulo nem ausência que libere, qualquer que seja o número de empresas (RN-EMP-15). Resolução que não roda não libera: falha com 500, nunca com conjunto mais amplo (RN-EMP-25). Fora do escopo é 404.
O que precisa ser garantido do lado do produto: a empresa é do mesmo tenant da linha — chave composta e local, (tenantId, companyId) → companies(tenantId, id) —; as vigências da mesma pessoa na mesma empresa não se cruzam, a mesma mecânica do CostCenterAssignment; e a concessão não escala entre empresas: só se concede o que se alcança (RN-EMP-19), nunca além da própria vigência (RN-EMP-42, com onGrantorChange gravado na concessão), ninguém mexe nas próprias linhas (RN-EMP-41), e toda empresa, ativa ou inativa, mantém uma linha vigente sem validTo de pessoa ativa com update:company-scopes (RN-EMP-24).
Anexo não tem dono no banco, e por isso tem três guardas
Nenhuma chave estrangeira liga o Attachment ao documento dono nem ao StoredFile. A integridade é mantida por três mecanismos que só valem juntos: o registro de descritores por ownerType, que derruba a inicialização quando um módulo registra dono sem descritor completo; o evento de descarte de rascunho, que é o caminho normal; e a varredura diária (attachment-orphan-sweep), que é a rede de segurança, libera a referência só quando o último estado conhecido do dono era rascunho e reporta o que encontra. Dono de módulo ainda ausente não é varrido: ausência de módulo não é ausência de dono. Órfão não é ocorrência normal — é sintoma de um consumidor que esqueceu de publicar o evento.
Revogar libera o arquivo só quando o documento é rascunho nunca submetido
Fora do rascunho nunca submetido a FileReference não é liberada e o blob continua preso: liberar faria o arquivo morrer no fim da carência e a auditoria receber a lápide do GrydFiles, guardando o registro da prova e perdendo a prova. A referência de documento que já passou por alguém sai por dois caminhos só: o fim da guarda — 31/12 do ano do fechamento do dono mais os anos congelados no anexo, liberado pelo job attachment-guard-release — ou a eliminação a pedido de titular; e nenhum dos dois com guarda legal ativa. Guarda legal é ato por caso, nunca propriedade do tipo.
Dois CNPJs com o mesmo nome são coisas diferentes
Establishment é comprador; SupplierEstablishment é vendedor. Por isso o vínculo do item com o fornecedor, e todo o agregado do fornecedor, usam supplierEstablishmentId, e não o nome curto, que seria ambíguo em toda consulta.
Motivo é dado, nunca regra
Nenhum ReasonCode muda fluxo, estado ou permissão — quem decide comportamento é o motor de aprovação, a política de orçamento e a máquina de estados do documento. E o corolário: motivo escolhido por máquina não é ReasonCode — é enum. BudgetEntryReason ficou fora do catálogo porque deixá-lo dentro daria ao tenant o poder de renomear ou desabilitar um valor de que o motor depende, que é exatamente a porta que esta regra existe para fechar. As duas perguntas convivem no mesmo movimento do razão: "por que este movimento existe" é da máquina; "por que a pessoa fez isso" é do catálogo, e só existe quando houve pessoa. A única exceção é requiresComment, que é comportamento de formulário. E o que o ato guarda é a chave mais o código e o rótulo congelados: renomear o código não pode reescrever a decisão que alguém tomou lendo o rótulo antigo, e o relatório agrupa pelo código congelado, nunca pelo id — a sobreposição do tenant cria linha nova com id novo (RN-RSN-18).
Nenhum aviso carrega valor sob permissão
Quantia, saldo, preço, total e limite de alçada não entram no corpo de uma notificação — ela carrega identificação, contexto e link. A regra é positiva: toda variável declara a sua classe — Identifier, Label, DateOrCount ou Link —, e texto digitado por uma pessoa (comentário, justificativa, descrição) nunca é variável. É verificada na semeadura, não em tempo de execução: variável sem classe, ou de classe fora das quatro, derruba a inicialização, e a lista de nomes proibidos fica como segunda malha. O motivo é que a permissão do destinatário não existe no momento em que o texto é montado — e o e-mail encaminhado no dia seguinte não tem correção.
Vocabulário
Enumerações
| Enum | Valores | Onde |
|---|---|---|
| AttachmentOwnerType | Requisition | RequisitionItem | QuotationRequest | QuotationResponse | PurchaseOrder | PurchaseOrderItem | Receipt | Invoice | Contract | QuotationRequestItem | ApprovalRequest | Supplier | Item | SupplierQualification | Budget | BankAccountChangeProposal | BudgetChangeRequest | Attachment. Persistido como texto: acrescentar valor não é migração. Estar na lista não é ser dono — o valor só aceita anexo quando o módulo dono registra o descritor com as doze respostas, e a RN-ATT-03 confere no startup os valores registrados. Sete têm spec dona e respostas declaradas na página de Anexo; os outros dez são dos documentos ainda não especificados |
| AttachmentVisibility | Internal | Supplier | Attachment e AttachmentType. Desde a v1, porque decidir depois obrigaria a reclassificar acervo e o modo de falha é vazamento |
| UploadedByKind | User | PortalUser | System | Attachment. Autoria de negócio; o uploadedBy do StoredFile continua sendo identidade do tenant no GrydAuth |
| RevocationSource | User | OwnerDiscarded | OrphanSweep | FileInfected | FileRejected | Erasure | Attachment. Separa o que uma pessoa decidiu do que o sistema concluiu — é o que a tela usa para escrever a frase certa. FileRejected é a reprovação do arquivo pelo GrydFiles e vem com rejectionCode; Erasure é a eliminação a pedido de titular |
| RetentionMode | UntilReleased | RetainUntil | Permanent | AttachmentType declara, Attachment congela no upload, StoredFile executa. O Nexio decide quanto tempo guardar; o GrydFiles cobra a declaração |
| AttachmentOwnerState | Draft | InProgress | Closed | Attachment.lastKnownOwnerState. É o mesmo vocabulário que o StateOf do descritor devolve, menos o Missing — que é resposta do descritor, nunca estado gravado. Quem grava é o Anexo: na criação, pelo StateOf; depois, pelo evento OwnerStateChanged(ownerType, ownerId, state, occurredAt) que o dono publica depois do commit — evento mais antigo que o gravado é ignorado, e o dono nunca escreve no anexo (RN-ATT-28). O occurredAt do Closed conta a guarda |
| FileOutcome | Available | Infected | Rejected | Failed | O que o contrato de veredito (IFileOutcomeHandler) entrega ao ownerScope, depois do commit do GrydFiles, pelo menos uma vez. Rejected é o Purged com rejectionCode; Failed só avisa. Não é o FileStatus: o handler relê o arquivo e decide pelo estado atual, não pela mensagem. O nome é deste Modelo — a página do GrydFiles lista os quatro desfechos sem nomear o tipo |
| FileStatus | Pending | Scanning | Available | Infected | Failed | Purged | StoredFile, da plataforma. Não é espelhado no anexo: muda, e por isso é consultado em lote |
| Attachment.fileReferenceStatus | Reserved | Expected | CreatePending | Active | ReleasePending | Released | NeverCreated | Onde está a FileReference do anexo, sem consultar o GrydFiles. Reserved é a reserva do upload ainda não confirmada; Expected, confirmada com a referência esperada; CreatePending e ReleasePending são escrita decidida e ainda não confirmada — pendência à vista, repetida pelo job (RN-ATT-35). A página de Anexo não dá nome ao tipo |
| AttachmentErasureRequest.status | Requested | Executed | PurgeBlocked | Purged | Cancelled | Terminais: Purged e Cancelled; Executed e PurgeBlocked alternam até o GrydFiles confirmar o expurgo. O bloqueio do registro não é estado: é calculado na leitura e na execução (RN-ATT-33). A página de Anexo não dá nome ao tipo |
| ApprovalRequestStatus | InApproval | Approved | Rejected | Returned | Cancelled | ApprovalRequest. Rejected e Cancelled são finais — documento rejeitado que volta abre instância nova. Approved é terminal condicional: sai só por revaluate da alteração que não é ajuste (para InApproval, rodada do zero) e por cancel (RN-AP-43); a alteração que é ajuste continua Approved, e o desfecho grava a diferença (RN-AP-71); no BANK_ACCOUNT_CHANGE não reabre. Returned volta ao requisitante com a instância viva. Não há Draft — o rascunho é do documento — nem Expired — nada vence por decurso |
| ApprovalStepStatus | Pending | Active | Approved | Rejected | Skipped | Obsolete | ApprovalStep. Skipped só por activationMinValue, pela dedupe de passo comum de uma decisão ou pela dispensa da base no pedido que nasce de requisição aprovada (RN-AP-68) — nunca por falta de aprovador, e nunca em passo de controle. Obsolete é o que a reavaliação produz |
| ApprovalAssignmentStatus | Pending | Approved | Rejected | Returned | Obsolete | Reassigned | ApprovalAssignment. Obsolete quando outra tarefa do nível já fechou o passo em modo Any. A linha permanece |
| ApprovalMode | Any | All | Quorum(n) | ApprovalLevel. É aqui que vive o paralelo; entre degraus da cadeia é sequencial — níveis de políticas diferentes com o mesmo stage ativam juntos (RN-AP-65). Rejeição encerra a instância em todos os modos. Quorum(n) conta pessoas distintas por quem agiu; menos de n elegíveis é resolução vazia, nunca quórum menor (RN-AP-59). Vote(n) — votação com dissidência tolerada — é fase 2, restrito a BUDGET, BUDGET_CHANGE e CONTRACT, e não entra no enum até lá |
| ApprovalScopeMode | Document | Line | Slice | ApprovalLevel e ApprovalStep. Line: um passo por linha que casa os critérios de linha da política de origem, scopeAmount = valor da linha, sem materialidade, dedupe por (pessoa, linha) (RN-AP-46). Cada processo admite só alguns (admittedScopeModes, RN-AP-54): Slice só onde há fatia |
| AssignmentOrigin | Direct | ByRole | ByGroup | ByPermission | ByDelegation | ByEscalation | ByReassignment | ByFallback | ApprovalAssignment, com o id da delegação, do grupo, do papel ou da permissão em originRef. ByFallback marca toda tarefa que veio do aprovador de reserva, em qualquer processo (RN-AP-58) |
| EscalationBehavior | None | Notify | Escalate | ApprovalProcessSettings, por processo: política e nível herdam o prazo, não o que fazer com ele. O alvo é escalationTargetType · escalationTargetRef, no nível, exigido quando o nível não caminha árvore (RN-AP-35); allowEscalation = false — travado no BANK_ACCOUNT_CHANGE — proíbe escalar. Não existe AutoApprove: a ausência é a decisão |
| ApproverType | SpecificUser | Role | CostCenterOwner | CostCenterOwnerUpToAmount | ApprovalGroup | PermissionHolder | CategoryOwner | RequesterManager | ExternalEmail | Auto | ApprovalLevel. Os seis primeiros são MVP; os quatro últimos, fase 2. PermissionHolder é o alvo das políticas padrão semeadas. Os tipos de árvore (CostCenterOwner…) só existem onde há fatia — cada processo declara os tipos admitidos (admittedApproverTypes, RN-AP-54). Panel e CostCenterOwnerAtDistance(n) são v2 e não entram no enum |
| ApprovalProcessCode | REQUISITION | QUOTATION | PURCHASE_ORDER | CONTRACT | SUPPLIER_REGISTRATION | SUPPLIER | BANK_ACCOUNT_CHANGE | ITEM | BUDGET | BUDGET_CHANGE | Os dez processos-semente, uma linha de ApprovalProcess do produto cada. Quatro não têm valor: SUPPLIER_REGISTRATION, SUPPLIER, BANK_ACCOUNT_CHANGE e ITEM |
| AuthorityExceededBehavior | RequireControllerApproval | Refuse | ApprovalProcessSettings. Semente RequireControllerApproval: passo paralelo Role(Controladoria) · Any com controlOfStepId; Refuse recusa a submissão cuja cadeia já estoura ou cai no reserva. Vale para alçada esgotada na raiz e para reserva em processo com valor (RN-AP-58). Parâmetro crítico (RN-AP-69) |
| FallbackApproverType | User | ApprovalGroup | ApprovalProcessSettings e ApprovalPolicy, com fallbackApproverRef. Obrigatório para ativar processo com valor, proibido no BANK_ACCOUNT_CHANGE (RN-AP-03) |
| ExcludedUserReason | Requester | PreparedBy | Beneficiary | LastEditor | Proponent | RateOverrider | IdentityCorrectionLinked | ApprovalRequestExcludedUser.reason. Mandado pelo módulo dono na submissão e na reavaliação; lista sem Requester — ou sem Proponent no BANK_ACCOUNT_CHANGE — é 422 (RN-AP-60) |
| ApprovalEventAction | approval-request.submitted | decision-refused | cancelled | chain-resolved | decided | returned | resubmitted | revaluated | reopened | approved-by-policy | budget-requirement-unmet · approval-step.activated | closed | sla-expired | skipped | obsoleted | escalated | authority-exceeded | resolved-out-of-scope | resolved-empty-on-activation | resolved-by-fallback | controller-required | inherited | stalled · approval-assignment.created | decided | reminded | info-requested | info-answered | reassigned | declined | delegated | obsoleted | ApprovalEvent.action. Fechado, <entidade>.<verbo-no-passado>; 33 ações. O desfecho vai em outcome (Approved · Rejected · Returned · Cancelled · ApprovedWithProviso), não no nome da ação (RN-AP-48) |
| ApprovalSettingsChangeStatus | Pending | Applied | Discarded | ApprovalSettingsChange.status. Nenhuma escrita além desta transição (RN-AP-69) |
| ApprovalSettingsChangeTarget | TenantSettings | ProcessSettings | GroupMembers | ApprovalSettingsChange.targetType. GroupMembers cobre grupo usado como reserva, por nível de controle em política ativa ou na cadeia congelada de instância InApproval |
| TenantType | Company | Group | Plataforma. Group é eixo de acesso e nunca guarda dado |
| UserCompanyScope.origin | Grant | CompanyCreation | UserCompanyScope. Gerado, persistido como texto. CompanyCreation é a linha que POST /companies grava para quem criou a empresa (RN-EMP-20); a carga da migração e o ato de suporte da plataforma gravam Grant |
| UserCompanyScope.onGrantorChange | Cascade | Restrict | UserCompanyScope, anulável. Gerado na concessão e imutável: Cascade quando a linha de quem concedeu tinha validTo, Restrict quando não tinha; nulo em CompanyCreation e nas linhas do suporte da plataforma (RN-EMP-42) |
| CompanyScopeReason | Granted | NoGrant | Expired | Onboarding | Resposta de GET /me/company-scope — por que o conjunto resolvido é o que é. Não é coluna |
| TaxRegime | SimplesNacional | MEI | LucroPresumido | LucroReal | TaxExempt | NotApplicable | Company e Supplier — o mesmo enum dos dois lados. Do lado comprador, NotApplicable é recusado; o documento congela o regime na submissão (RN-EMP-37) |
| EstablishmentType | Headquarters | Branch | Establishment e SupplierEstablishment, gerado dos dois lados e nunca derivado da ordem 0001 — com o CNPJ alfanumérico a matriz pode ter outra ordem. No Establishment, nasce Branch e só PATCH /companies/{id}/headquarters troca (RN-EMP-38). No fornecedor, vem do identificador matriz/filial devolvido pela Receita; nulo sem verificação |
| IcmsTaxpayerStatus | Taxpayer | Exempt | NonTaxpayer | Establishment. É o indIEDest da NF-e |
| DeliveryLocationType | OwnSite | Warehouse | ConstructionSite | ThirdParty | DeliveryLocation e o snapshot da linha (deliveryLocationType). ThirdParty exige recipientCnpj e deliveryOperationType; CNPJ do próprio tenant só de outra Company em ThirdPartyProcessing ou GeneralWarehouse (RN-DL-06, RN-DL-20). ConstructionSite explica sozinho o interestadual sem recebedor distinto, com centro de custo — só no ponto cadastrado; no endereço eventual o tipo não dispensa a interstateNote (RN-DL-07, RN-DL-12) |
| DeliveryOperationType | ThirdPartyProcessing | GeneralWarehouse | DirectToCustomer | Other | DeliveryLocation.deliveryOperationType e o snapshot da linha. Obrigatório sempre que quem recebe fisicamente não é o destinatário — ThirdParty no cadastro, recebedor no eventual —, em qualquer UF; recusado sem recebedor distinto. Other exige deliveryOperationNote. DirectToCustomer é a única operação que aceita recebedor no endereço eventual (RN-DL-18). Descreve o fato e nomeia os casos reconhecidos; CFOP e a segunda NF-e são do ERP fiscal. Fechado de propósito: texto livre vira "conforme combinado" no terceiro mês |
| RegistrationStatus | Null | Active | Suspended | Unfit | ClosedDown | Situação cadastral — Establishment e SupplierEstablishment, o mesmo enum dos dois lados, com os códigos 01 · 02 · 03 · 04 · 08 da Receita; nulo para fornecedor sem CNPJ. No Establishment, gerado — pelo provedor de consulta ao CNPJ ou pela correção manual, vale a data de situação mais recente; Null não bloqueia, e Suspended, Unfit e ClosedDown bloqueiam documento novo (RN-EMP-36) |
| RegistrationSource | Provider | Manual | Establishment.registrationSource. De onde veio a situação cadastral vigente (RN-EMP-36) |
| EstablishmentLookupPurpose | Receiving | Requesting | Parâmetro obrigatório de GET /establishments/lookup: Receiving filtra allowsReceiving (para establishmentId), Requesting filtra allowsPurchasing (para requestingEstablishmentId). Não é coluna |
| AccountType | Asset | Liability | Equity | Revenue | Expense | GLAccount. Herdado do pai |
| SequenceScope | Tenant | Company | Establishment | NumberSequence. Padrão Company, imutável. Particiona os contadores: um NumberSequenceCounter no tenant, por empresa ou por estabelecimento (RN-EMP-30) |
| ResetPolicy | Never | Yearly | Monthly | NumberSequence. Padrão Yearly, imutável. O período é calculado no timeZone da configuração e só avança (RN-EMP-31) |
| NumberSequence.documentType | Requisition | QuotationRequest | PurchaseOrder | Receipt | BudgetChangeRequest | NumberSequence. Cresce com a espinha transacional; Contract fica fora porque nenhuma página o numera |
| AssignmentRole | Owner | Deputy | Watcher | CostCenterAssignment |
| PredominantUse | ProductionInput | Resale | ConsumableUse | FixedAsset | PersonalBenefit | Item e ItemType. Critério de aprovação |
| FreightTerms | ContractedBySender | ContractedByRecipient | ContractedByThirdParty | OwnTransportBySender | OwnTransportByRecipient | NoTransport | ItemSupplierPrice.freightTerms, na chave do preço. É o modFrete da NF-e (0 · 1 · 2 · 3 · 4 · 9), persistido como texto — o número é tradução na conferência e na integração. Obrigatório em mercadoria, nulo no resto; o comparativo conta como "com transporte" só 0 e 3 (RN-ITM-27) |
| ItemApprovalStatus | Draft | PendingApproval | Approved | Rejected | Item.approvalStatus. PendingApproval é instância aberta do processo ITEM; a única entrada em Approved sem instância é a confirmação da carga inicial (RN-ITM-42). Alteração material de item aprovado não muda o status: vira pendingRevision (RN-ITM-41) |
| ItemSupplierStatus | Active | Blocked | ItemSupplier.status. Nasce Active e só muda por block/unblock, com motivo (RN-ITM-15); Blocked não cota nem pede. O bloqueio do fornecedor inteiro não reescreve a relação — vale pela leitura do eixo Bloqueio do Fornecedor; é trava por item onde a relação existe, nunca o gate de gasto — esse é a qualificação vigente na categoria (RN-FOR-24) |
| RecordOrigin | Standard | InitialLoad | Item.origin e BankAccountChangeProposal.origin, com initialLoadBatchId. InitialLoad só nasce dentro da janela única de carga inicial, com dupla custódia (RN-ITM-42, RN-FOR-31) |
| FactorSource | SupplierUom | ItemConversion | StandardConversion | DeclaredAccepted | Identity | Linha de documento (a especificar com a Requisition). DeclaredAccepted exige aceite explícito na linha, com quem e quando (RN-ITM-39) |
| UomClass | Count | Weight | Volume | Length | Area | Time | Service | UnitOfMeasure. A conversão global é só intraclasse; entre classes, só por item, declarada no item (RN-UOM-07, RN-ITM-32). Service não converte nem entre si (RN-UOM-08) |
| BankAccountKind | Domestic | International | SupplierBankAccount.accountKind. Discriminador do bloco bancário: iban, swiftBic, bankCountryCode e os campos de banco intermediário são obrigatórios em International |
| RoutingCodeType | ABA | TransitNumber | SortCode | Other | SupplierBankAccount, no bloco internacional |
| ProtectedChangeKind | BankAccount | NoticeContact | ChainContact | BankAccountChangeProposal.changeKind. BankAccount e NoticeContact (e-mail e telefone de contato Finance) abrem sempre a instância BANK_ACCOUNT_CHANGE. ChainContact é a troca de canal dos contatos da cadeia de reserva do aviso — PortalAdmin, Commercial designado e contato designado do estabelecimento principal —, confirmada por segunda pessoa, e só abre a instância sem telefone confiável ou com propostas de conta travadas (RN-FOR-34, RN-FOR-38). Em proposta de contato o valor é calculado pelos papéis e pela posição do contato, nunca declarado. Trocar o canal do aviso é o primeiro passo da fraude que a proposta bancária existe para deter |
| SupplierEntityType | Company | MEI | Individual | RuralProducer | Foreign | Supplier. Governa a obrigatoriedade de todo o resto |
| CompanySize | NotInformed | ME | EPP | Other | Supplier. Indicativo, nunca verdade contratual |
| SupplierRelationship | Prospect | SpendAuthorized | Supplier, eixo Relacionamento. Gasto é autorizado por qualificação vigente na categoria, não por este eixo (RN-FOR-24) |
| SupplierRegistrationState | Draft | UnderReview | Approved | Rejected | Supplier, eixo Processo de cadastro. UnderReview é a instância SUPPLIER_REGISTRATION aberta; entityType, cpf, ownerCpf e a identidade de Foreign ficam imutáveis depois de Approved (RN-FOR-39). Não confundir com RegistrationStatus, que é a situação na Receita |
| SupplierContactRole | Commercial | Tax | Finance | Technical | Quality | PortalAdmin | SupplierContact.roles[]. Exatamente um contato ativo com PortalAdmin; e-mail e telefone de Finance e da cadeia de reserva são campos protegidos (RN-FOR-21, RN-FOR-34) |
| ContactValueSource | Manual | RegistryLookup | Import | InitialLoad | SupplierContact.phoneSource e mobileSource. Gravado pelo sistema, nunca informado por quem edita |
| BankChangeProposalStatus | Draft | Open | Applied | Rejected | Cancelled | Contested | BankAccountChangeProposal.status. Draft é editável só pelo proponente, sem aviso nem instância; submetida, não muda mais (RN-FOR-35). Rejected, Cancelled e Contested são finais; Applied só sai para Contested (RN-FOR-30, RN-FOR-38) |
| ContestChannel | Link | Internal | Callback | BankAccountChangeProposal.contestChannel. Internal exige segunda pessoa (RN-FOR-30) |
| CallbackPhoneSource | AgedContactPhone | RegistryLookup | BankAccountChangeProposal.callbackPhoneSource. Comprovante manual nunca é fonte de telefone (RN-FOR-29) |
| CallbackResult | Confirmed | Denied | BankAccountChangeProposal.callbackResult. Denied contesta a proposta |
| ComplianceAlertKind | Sanction | RegistrationStatusChanged | ErpBankDivergence | VerificationStale | BankEvidenceLost | ControlEvidenceLost | SupplierComplianceAlert.kind. Os dois de prova perdida nascem do aviso do Anexo, com source = Attachment, e só a decisão os fecha (RN-FOR-37) |
| ComplianceAlertSource | CEIS | CNEP | CEPIM | Leniency | TcuIneligible | Cnia | SlaveLabor | OfacSdn | Receita | ErpBankAccount | Attachment | SupplierComplianceAlert.source |
| ComplianceAlertStatus | Open | Decided | Resolved | SupplierComplianceAlert.status. Um aberto por fornecedor, tipo, fonte e registro de origem |
| ComplianceAlertDecision | KeepWithJustification | FalsePositive | Blocked | SupplierComplianceAlert.decision. Blocked só com o fornecedor já bloqueado pela rota própria — o alerta registra, o bloqueio é o ato |
| PolicyKind | Base | Supplemental | ApprovalPolicy. Base concorre, Supplemental empilha |
| PolicyStatus | Draft | Published | Retired | ApprovalPolicy.status. Só Published dentro da vigência é candidata; Draft só entra na simulação (RN-AP-26). Publicar exige segunda pessoa (RN-AP-55) |
| BudgetStatus | Draft | InReview | Approved | Closed | Archived | Budget. Só Approved e Closed participam da resolução |
| BudgetOrigin | Imported | Planned | Budget. Muda por onde as linhas entraram, não o ciclo de vida |
| PeriodGranularity | Monthly | Quarterly | Yearly | Budget. Imutável depois de aprovado |
| BudgetEntryReason | Post | Liquidate | Reverse | DocumentCancelled | Return | ReceiptReversal | QuantityChange | PriceRevision | DateChange | ResidueRelease | YearEndRollover | InvoicePriceVariance | ExchangeVariance | RateCorrection | Reconversion | BudgetEntry.entryReason, obrigatório em todo movimento. Enum fechado, declarado na página de Motivo e fora do catálogo ReasonCode: quem escolhe é a máquina de estados, e deixá-lo no catálogo daria ao tenant o poder de renomear ou desabilitar um valor de que o motor depende. A distinção em uma linha: entryReason responde "por que este movimento existe"; reasonCode responde "por que a pessoa fez isso" e só existe quando houve pessoa — o único valor que traz os dois juntos é InvoicePriceVariance, com o uso PriceVarianceAccept. É entryReason, a coluna fina, que entra na chave de idempotência (RN-O34). BudgetEntryOperation (Post · Liquidate · Release · Reverse · Rebucket) é a coluna grossa, derivada deste por tabela fixa, só para leitura — não é uma segunda coluna que possa divergir |
| BudgetStage | Commitment | Obligation | Actual | Adjustment | BudgetEntry. Os três primeiros consomem; Adjustment altera a verba e só nasce de BudgetChangeRequest, Import ou Rollover (RN-O28). A virada de exercício grava nos próprios estágios, com entryReason = YearEndRollover (RN-O13) |
| BudgetSignal | NotControlled | Ok | Warning | Exceeded | Uncovered | Resposta de POST /evaluate:budget-state, por fatia e por linha candidata. NotControlled é ausência de controle e não é critério de nada; os outros quatro são o contrato fechado dos sinais (RN-O39). É o que a tela pinta e o que o motor lê como critério |
| CarryForwardMode | PeriodOnly | Cumulative | BudgetPolicy. É modo de leitura do saldo, não movimento de transporte. Padrão PeriodOnly |
| OverBudgetPolicy | Warn | Block | RequireApproval | BudgetPolicy. Padrão Warn; não existe permissão que fure Block |
| YearEndCommitmentPolicy | CarryCommitmentOnly | CarryCommitmentAndBudget | CancelOpen | BudgetPolicy. Padrão CarryCommitmentOnly — o compromisso segue o documento, a verba não vai junto |
| ChangeRequestType | Supplement | Transfer | Reduction | BudgetChangeRequest. O sinal vem do tipo, não do número digitado |
| BudgetChangeRequestStatus | Draft | PendingApproval | Approved | Rejected | Cancelled | BudgetChangeRequest. A transição para Approved é do motor, e é ela que grava os movimentos de Adjustment |
| BudgetSourceType | Requisition | PurchaseOrder | Receipt | Invoice | Contract | BudgetChangeRequest | Rollover | Import | Manual | BudgetEntry. É o rastro que responde "quem comeu minha verba" |
| RateType | PtaxVenda | PtaxCompra | Commercial | Contracted | Manual | ExchangeRate, CurrencyPair, CurrencySettings e o movimento do razão. O tipo decide a origem: PtaxVenda e PtaxCompra só oficiais (tenantId nulo); Commercial, Contracted e Manual só do tenant (RN-CUR-08). O tipo de um documento vem de CurrencySettings.processRateTypes, pelo processo dele, e quem move o documento não escolhe (RN-CUR-27). Semente PtaxVenda do boletim de fechamento, com conversionDate = D−1 civil (RN-CUR-25) |
| CurrencySettingsChangeStatus | Pending | Applied | Discarded | CurrencySettingsChange. Gravado como texto; só a transição de status é escrita (RN-CUR-28) |
| NotificationChannel | Email | InApp | Push | NotificationPreference.channel e o catálogo (allowedChannels, defaultChannels, mandatoryChannels). Canal fora de allowedChannels do slug não aparece na tela e é recusado na API. SMS e WhatsApp estão fora da v1 por decisão de produto: o aviso bancário sai por e-mail, e a redundância vem do callback telefônico, que é evidência da tarefa, não canal |
| NotificationCategory | Approval | Budget | Currency | Catalog | Company | Supplier | Attachment | System | Catálogo. Só agrupa a tela — a preferência é por slug, e não existe interruptor de categoria |
| AudienceType | User | TenantRole | CostCenterOwners | PendingApprovers | SupplierFinanceContact | TenantAdmin | PermissionHolders | CompanyScopeAdmins | TenantContractContact | Catálogo (audienceType, com audienceParams). Um resolvedor por tipo, implementado pelo módulo dono do conceito — três deles pela plataforma (TenantAdmin, PermissionHolders, TenantContractContact), pedidos na demanda de plataforma; tipo sem resolvedor derruba a inicialização |
| ReasonCodeUsage | ApprovalReject | ApprovalReturn | BankAccountChangeReject | BudgetReject | BudgetChangeReject | ItemReject | ItemDuplicateOverride | SupplierReject | SupplierBlock | SupplierUnblock | SupplierManualPromotion | AttachmentRevoke | PersonalDataErase | BudgetChangeJustify | PriceVarianceAccept | ExchangeRateSupersede | DocumentCancel | SupplierAwardOverride | ReasonCode.appliesTo; ApprovalProcess.rejectReasonUsage e returnReasonUsage. Fechado: entrar na lista custa escrever onde o código é lido, gravado e exibido. Dezoito usos, dezessete com piso obrigatório; DocumentCancel e SupplierAwardOverride estão declarados para agregados que ainda não existem — o primeiro já com piso obrigatório, o segundo com o piso a definir pela spec da Cotação. Semente do produto na página de Motivo: códigos globais, com sobreposição por tenant |
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 | O mapeador do Core testa primeiro uma lista de códigos canônicos exatos e depois dez sufixos — _NOT_FOUND → 404 · _ALREADY_EXISTS, _ALREADY_ASSIGNED e _CONFLICT → 409 · _FORBIDDEN, _ACCESS_DENIED e _BLOCKED → 403 · _UNAUTHORIZED → 401 · _UNPROCESSABLE → 422 · _UNAVAILABLE → 503 · sem sufixo → 400. Código exato vence sufixo — é o que deixa USER_EMAIL_UNAVAILABLE responder 409, no GrydFiles. Erro novo usa um dos dez. |
| A versão vive só no cabeçalho e no histórico | vale | Uma citação a outra página nunca carrega número de versão — "ver o Local de Entrega", "consome o GrydFiles". Quando o texto precisa datar um fato de outra página, usa a data: "desde 04/09/2026", nunca "no Fluxo v2.7". O corpo tem só a regra vigente; o que mudou, e quando, vai à seção Histórico de revisões no fim de cada página — versão, data, o que mudou. |
| Estilo de permissão | vale | <verbo>:<recurso>[-<sub-recurso>] — um só :, verbo em kebab-case singular, recurso em kebab-case plural. Vai ao plural o termo que nomeia registro contável (create:cost-centers, read:gl-accounts, propose:supplier-banks, create:budget-change-requests, read:budget-amounts-all); qualificador que não é registro fica como está (use:delivery-locations-one-time). Ficam no singular, cada uma pela sua razão: (a) o que nomeia o módulo — as sete de *:catalog (read, create, update, delete, import, export, clone), porque o tenant tem um catálogo; (b) o que nomeia a trilha de auditoria — read:catalog-audit e read:supplier-audit. As permissões das plataformas seguem o código delas e não se renomeiam por decisão do Nexio: o GrydFiles (upload:files, read:files, retain:files, purge:files, admin:system) e o catálogo semeado do GrydAuth (read:users, update:tenants, a permissão de gestão de todos os tenants, read:user-directory, create:permissions). Inventário do Nexio, contado nas páginas donas em 19/09/2026: 96 nomes, 87 no plural e 9 no singular; quatro dos 96 são do Portal do Fornecedor, que é proposta — um deles, enter-on-behalf:quotation-responses, declarado lá para a Cotação. O PermissionCodeParser do GrydAuth corta no primeiro : e renderiza "{Ação} {Recurso}"; o ToTitleCase quebra em - e _, nunca em . — por isso o sub-recurso entra com hífen. Um endpoint declara exatamente uma permissão; nada é implicado por outra. O código vive em Permission.name, e a linha de permissão é por tenant: levar o catálogo do produto a tenant criado depois do deploy é a pendência de plataforma P5 (Pendências do Gryd.IO). |
| Estorno é sobre o grupo; o parcial é proporcional | vale | Todo ato grava um grupo (postingGroupId, determinístico por ato): o positivo do estágio novo e os negativos que ele liquida (RN-O42). Desfazer a fração f de um ato estorna f de cada entrada do grupo, na mesma transação e com reversalOfEntryId: o positivo até o seu aberto, o negativo até o valor ainda não estornado — e os negativos de liquidação viram positivos que restituem o alvo (RN-O43, RN-O29). O negativo que aponta um alvo herda dele a tupla, o balde e a taxa — companyId, costCenterId, glAccountId, competencePeriodKey, exchangeRate, quoteUnit, rateDate —; só o positivo resolve por conta própria (RN-O30). Requisição de março liquidada por pedido de maio deixaria março comprometido para sempre e maio negativo. |
| Trilha é append-only e não tem soft delete | vale | ApprovalEvent e BudgetEntry só aceitam INSERT e não herdam a entidade base com exclusão lógica — exceção declarada do Nexio, que o critério (4) da pendência de plataforma P7 preserva. A única escrita que o ApprovalEvent admite é apagar o texto de comment por eliminação LGPD — POST /approvals/{id}/events/{sequence}/erase-comment, com motivo PersonalDataErase —, gravando commentErasedAt; código de motivo, rótulo, autor, momento e decisão nunca se apagam, e o commentHash da criação fica (RN-AP-48). Isso precisa estar escrito, porque a alternativa silenciosa é uma trilha de auditoria com linhas que somem. A exclusão lógica, o autor e a unidade de trabalho de todo contexto de produto são a pendência P7. |
| Registro de dono polimórfico responde doze perguntas | vale | Cada módulo dono registra, na inicialização, os seus valores de AttachmentOwnerType e o descritor de cada um; o guard percorre o que foi registrado e exige valor do enum, um descritor só e as doze respostas — CompanyOf, StateOf, DisplayNumberOf, AllowedTypes, AllowsSupplierVisibility, PropagatesTo, MaxCount · MaxTotalBytes, CanWrite, CanRead, OpenApprovalOf, PendingControlOf, ResponsibleOf. Valor cujo módulo ainda não existe não é exigido e não aceita anexo (RN-ATT-03). Declaração e implementação são coisas separadas: as respostas dos donos com spec estão declaradas numa tabela da página de Anexo; a implementação vive no módulo dono. O enum tem dezessete valores, persistidos como texto. |
| Evento depois do commit e entrega garantida são da plataforma | pendência P1 | O Nexio escreve o problema e o requisito — efeito atômico na transação, evento só depois do commit, entrega pelo menos uma vez com consumidor idempotente, tenant restaurado, ordem só na chave declarada, "aguardando" visível, idempotência no destino externo, TokenVersion na transação da revogação — na página de Mensageria (A1.1 a A1.9, com os critérios de aceite); a escolha de mecanismo e transporte é do estudo da plataforma, e o estado dela é o P1 das Pendências do Gryd.IO. Constatação, não escolha: hoje o único contexto que despacha evento é o do GrydAuth, dentro da transação; o Core tem envelope durável (PostgresOutbox) usado só pelo GrydNotifications com DurableDelivery, desligado por padrão, e nenhum evento de domínio passa por ele. O contrato de veredito do GrydFiles (IFileOutcomeHandler) é um consumidor pós-commit como os outros: chamado pelo menos uma vez, com o tenant restaurado, por handler idempotente, e depende da mesma garantia. Continua do Nexio o ExternalRef de cada destino (Mensageria A1.6). |
| Chave de relação × chave de preço | vale | O ItemSupplier é a relação, com chave de 3 partes (itemId, supplierId, supplierEstablishmentId?), e o ItemSupplierPrice é a versão de preço, com a chave da relação mais quatro (itemSupplierId, validFrom, shipToStateCode?, companyId?, freightTerms?). A régua, que vale para o próximo par: destino e vigência são dimensão de preço; o que identifica a contraparte é dimensão de relação. Um eixo entra na chave da relação só quando muda com quem se compra — por isso supplierEstablishmentId? fica (é outro CNPJ, com homologação possivelmente própria) e a UF de entrega não (é frete e tributação). A unidade de compra é filha da relação (ItemSupplierUom), fora das duas chaves — não muda com quem se compra nem com quanto se acordou pagar —, e embalagem que muda de conteúdo é linha nova, não unidade nova. Com a UF na relação, mudar o endereço de entrega mudaria de relacionamento de fornecimento, e status, unidades de compra, tolerâncias e preferência passariam a variar por UF. |
| 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. No motor de aprovação, a resposta à decisão de quem perdeu a empresa do documento e o alcance de audit:approvals estão em decisão; até ela, vale o que o Fluxo de Aprovação declara. |
| Estado só por rota própria | vale | PATCH …/activate e PATCH …/deactivate; PUT nunca muda isActive. closedOn da empresa não mexe em isActive — documento novo com data posterior é 422 COMPANY_CLOSED_UNPROCESSABLE —; desativar a empresa desativa os estabelecimentos na mesma transação, por rota própria. Registro inativo não entra em documento novo; documento já submetido que o aponta segue até o fim, e a leitura não some. |
DELETE tem um significado só | vale | DELETE apaga de verdade, e existe só onde apagar é legítimo: registro que nunca produziu efeito — rascunho nunca submetido, relação nunca usada, ponto de entrega cadastrado errado — ou exceção que só o usuário configurou. Desativar é PATCH …/deactivate; onde apagar é proibido — arquivo, anexo, motivo, razão orçamentário, trilha — a rota não existe, e o ato é revogar ou estornar, que é registro. Na plataforma, DELETE de referência de arquivo libera a referência, não apaga o arquivo. |
| Ato que não é CRUD | vale | Ato sobre coleção é POST /<coleção>:<ato> — /cost-centers:import, /approvals:simulate, /exchange-rates:fetch, /delivery-locations:import. A validação prévia de importação não é endpoint separado nem ?mode=: é dryRun=true no mesmo ato, para que o simulado e o executado não possam divergir; a simulação e a prévia do motor são atos próprios (/approvals:simulate, /approvals:preview), com contrato próprio. Contado nas páginas donas em 19/09/2026: vinte e nove rotas com :ato — dezesseis sobre coleção ou sub-recurso, onze sobre instância (/{id}:<ato>) e duas com o verbo à frente, /evaluate:budget-state e /convert:currency —, além dos atos sobre instância escritos como /{id}/<verbo>, que é a forma especificada no Gryd.IO (/files/{id}/rescan, ainda não implementada) e a do Anexo (/attachments/{id}/revoke). Qual forma vale para ato sobre instância, e se as duas de verbo à frente mudam, está em decisão: caminho de rota publicado é contrato, e alinhá-lo é decisão, não conserto de redação. |
| Código de sucesso | em decisão | As donas divergem: o Cadastro de Item e o Fornecedor declaram 200 inclusive nos POST; o Anexo devolve 201 no pedido de eliminação; o Orçamento devolve 204 em dois PUT. DELETE devolve 204 onde existe. Até a decisão, vale o código que a rota declara na página dona; spec nova declara o código em cada rota. |
| Importação de cadastro mestre | vale | Tudo ou nada, com dry-run obrigatório antes. A exceção declarada é o catálogo: importação de item é assíncrona, parcial e nasce em rascunho, porque o item entra para ser curado — e curadoria é o processo ITEM do motor. Nenhuma importação aprova sozinha: a única entrada aprovada por planilha é a carga inicial — do catálogo (RN-ITM-42) e das contas bancárias (RN-FOR-31) —, com janela única por tenant e dupla custódia; e campo protegido do fornecedor entra por importação só como proposta em rascunho (Propose) ou é recusado (Reject). |
| Efeito na mesma transação; evento depois do commit | vale | O efeito que precisa ser atômico com o ato acontece na mesma transação — pelo próprio caso de uso, por chamada síncrona declarada entre contextos (.Contracts) ou pelo contrato de desfecho do motor; o evento de domínio é publicado depois do commit e serve para notificar, medir e integrar — nunca para produzir efeito de que o ato dependa. Exceção única declarada: a versão do token (TokenVersion) sobe na transação da revogação. No motor, a decisão que conclui uma instância — aprovação final, rejeição, cancelamento — e a reabertura invocam o contrato de desfecho, e o módulo dono devolve um de três resultados: Applied (decisão, transição e efeito commitam juntos), Refused(code) (nada é gravado além da tentativa; a tarefa continua ativa) ou Failed (rollback total) — RN-AP-38. Como o desfecho se aplica quando a instância conclui sem decisão humana — aprovação pela política ou herança total na reavaliação — está em decisão. Os requisitos de publicação e entrega estão na página de Mensageria. |
| Comparação converte para a moeda de quem limita | vale | Não existe "moeda do tenant". Converte-se o valor do documento para a moeda de quem limita — na verba, a do exercício (Budget.currency, cópia da Company.functionalCurrency, que é BRL na v1 e imutável — RN-EMP-35); na alçada, a do limite (CostCenterAssignment.approvalLimitCurrency, obrigatória com limite preenchido, inclusive no nó corporativo) —, sempre fatia a fatia e com a taxa do documento — a da submissão ou a da última rodada nova (RN-CUR-22, RN-CUR-23). Documentos, preços, cotações e limites seguem em qualquer ISO 4217. A comparação de alçada grava no ApprovalEvent o valor comparado, a taxa, o quoteUnit e a data, como prova. Sem par vigente da moeda do documento para a do limite, a submissão é recusada com APPROVAL_LIMIT_CURRENCY_NOT_CONVERTIBLE_UNPROCESSABLE (422) — nunca comparada em silêncio. O documento converte de novo só no ato que abre rodada nova — reenvio de devolvido e reabertura com rodada do zero —, e o ajuste do aprovado usa a taxa gravada (RN-CUR-22, decisão D3). Como se trata limite em moeda diferente de BRL está em decisão. |
| Resíduo tem uma regra só | vale | RN-CC-23 é a regra: ROUND_HALF_UP por fatia, diferença apurada contra o total da linha e somada à fatia de maior valor, empate pelo menor order. As demais specs referenciam, não reescrevem. E o rateio acontece antes da conversão: o valor do documento na moeda funcional é a soma das fatias convertidas, nunca o total convertido — por isso não existe um segundo resíduo para distribuir |
| Tipo numérico é o da Unidade de Medida | vale | A tabela da Unidade de Medida vale para o acervo: quantidade numeric(19,6) — a NF-e usa 4 casas em qCom e o SPED 0220 usa 6 no fator —; preço unitário numeric(21,10), o teto de vUnCom na NF-e (11 inteiros e 10 casas); valor monetário numeric(19,4), guardado já arredondado às casas da moeda (Currency.minorUnits, de 0 a 4) — nunca se assume duas casas. Percentual numeric(9,6) e taxa de câmbio numeric(18,8) são os tipos que todas as specs usam, declarados aqui porque a UoM define só os três primeiros; o percentual em (9,6) é o que deixa o limiar de materialidade exprimir o rateio que o Centro de Custo justifica. Tipo numérico divergente numa spec é defeito da spec, e o conserto é da dona — as caixas deste Modelo mostram o tipo que a dona declara hoje. Dívida aberta, contada nas páginas donas em 19/09/2026: CostAllocation.amount e CostCenterAssignment.approvalLimit em numeric(19,2), no Centro de Custo; ApprovalSettings.allocationMaterialityAmount em numeric(19,2), no Fluxo de Aprovação; Company.shareCapital em decimal(18,2), na Empresa. |
| Percentual e valor no mesmo limiar | vale | Dois pares do produto exprimem um limiar de duas formas ao mesmo tempo, e os dois valem o menor dos dois: a materialidade do rateio no motor — allocationMaterialityPercent · allocationMaterialityAmount —, em que o valor absoluto é o teto do limiar, para que num documento grande uma fatia de centenas de milhares nunca seja dispensada (RN-AP-15); e a tolerância do orçamento — tolerancePercent · toleranceAmount —, porque 5% de uma verba de R$ 10 milhões seria meio milhão de folga invisível. A reavaliação do motor não tem tolerância: toda alteração do assunto depois da submissão reavalia (RN-AP-20). A regra para o próximo par: declarar qual semântica ele segue na mesma linha em que o par nasce. Par novo sem essa palavra é dívida, não configuração. |
| Nome de ação da trilha | vale | <entidade>.<verbo-no-passado>, a mesma cauda do slug de notificação sem o prefixo do produto. Vale só para evento de negócio — o ato registrado pelo módulo, que guarda a ação em AdditionalData["action"]; a entrada automática do interceptor não tem ação — tem ChangeType. O caminho de gravação do registro de ato de controle é a pendência de plataforma P4, e a ação como coluna consultável é a P21 (Pendências do Gryd.IO). |
| Um token de concorrência | vale | version int que nasce 1 e incrementa a cada escrita; o cliente envia expectedVersion e a divergência é 409 com sufixo _CONFLICT. Vale para Company, Establishment, DeliveryLocation, NumberSequence, CostCenter, GLAccount, CategoryNode, Item, ItemSupplier, Supplier, Budget e BudgetLine (nestes dois a coluna se chama rowVersion, porque version é a revisão do exercício) e, no motor, para a raiz ApprovalRequest — toda decisão serializa na raiz, e não há token no passo nem na tarefa (RN-AP-44). O UserCompanyScope não tem version: a escrita é do conjunto, e o token é o expectedScopeHash das linhas vigentes que o GET devolveu (RN-EMP-23). Nas três árvores path é guarda de move, não token. Nenhum tipo específico de banco: é coluna inteira comum. |
| Banco de dados | ver Arquitetura | Tudo o que é de arquitetura está definido na Arquitetura, e só nela — a escolha de banco inclusive (ADR 2); esta página cita, não repete. As specs escrevem o requisito e citam recurso específico só como exemplo de implementação, com equivalente: vigência sem sobreposição (CostCenterAssignment, UserCompanyScope, NumberSequence por (tenant, documentType), Budget, BudgetPolicy); unicidade com nulo colidindo com nulo (ItemSupplier, ItemSupplierPrice, BudgetLine, CostCenter.externalCode por (tenant, companyId), FileReference); unicidade só entre os registros liberados ou ativos (ItemSupplierUom, as identidades do Supplier e do SupplierEstablishment); trava da linha nas cascatas e na publicação de política; busca aproximada no centro de custo e no item. O snapshot de entrega da linha de documento é de colunas próprias, consultáveis e indexáveis — o que qualquer banco atende. A lista do que cada exigência cobra do banco é a tabela "O que as specs exigem do banco" do ADR 2. |
| Enum persiste como texto | vale | Todo enum do Nexio é persistido como texto, por convenção declarada do Nexio — no Gryd.IO a conversão é por propriedade —, de modo que acrescentar valor não é migração. Valor de enum é identificador em inglês, em PascalCase. |
Inventário
Satélites — as classes que não têm caixa
Vinte e cinco classes especificadas que não aparecem nos diagramas, de propósito. A regra de corte é esta: ganha caixa o que se lê ao lado de outra coisa — raiz, configuração, árvore, o que participa de uma invariante entre agregados. Satélite é o que só se lê dentro do seu próprio agregado, e desenhá-lo aqui dobraria o tamanho de nove diagramas para repetir o que a spec do dono já diz melhor. O que esta tabela garante é o que faltava: que ninguém invente um campo que já existe, porque a linha diz onde procurá-lo.
| Classe | Agregado dono | Onde os campos vivem |
|---|---|---|
ItemPhoto | Item | Cadastro de Item · § Coleções filhas. Máx. 5, storedFileId no perfil nexio.item-photo |
ItemDocument | Item | Cadastro de Item · § Coleções filhas. FISPQ, certificado, laudo; tipo, versão, emissão, validade e storedFileId |
ItemCompliance | Item | Cadastro de Item · § Compliance. Vencimento bloqueia a compra quando marcado |
ItemAttributeValue | Item | Cadastro de Item · § Atributos. Colunas tipadas (valueText, valueNumber, valueBool, valueDate, valueListId) mais uomId; atributo não vira coluna do item |
ItemKeyword | Item | Cadastro de Item · keywords. Máx. 10, 50 caracteres; entra na busca |
ItemUomConversion | Item | Unidade de Medida · § Conversão, e Cadastro de Item · conversions. Nível 2 da hierarquia. Com vigência: fator usado versiona (a vigente fecha em validTo, a nova abre em validFrom) e registro nunca usado corrige-se no lugar (RN-UOM-09, RN-ITM-25). Pode ser interclasse (RN-ITM-32). Leva gtin? char(14) do nível de embalagem (RN-ITM-40). Máx. 20; nunca a unidade base como destino |
ItemLifecyclePhase | Item · configuração | Cadastro de Item · § Ciclo de vida. Fase por tenant e a matriz processo × política que ela impõe; destino de Item.lifecyclePhaseId |
AttributeDefinition | Catálogo · raiz própria | Cadastro de Item · § Atributos. É raiz e mesmo assim não tem caixa: o esquema por categoria é assunto de uma spec só, e desenhá-lo custaria a banda inteira. A lista de valores (AttributeValue) vive dentro dela |
CategoryAttribute | CategoryNode · vínculo | Cadastro de Item · § Vínculo categoria × atributo. Chave (categoryNodeId, attributeDefinitionId); isRequired, isIdentity — o que entra em attributeSignature e crossCategorySignature, e cuja mudança recalcula as assinaturas da subárvore —, usedInDescription, descriptionOrder, displayOrder |
ItemSupplierUomAlias | ItemSupplier | Cadastro de Item · § Item × Fornecedor e RN-ITM-35. Chave (itemSupplierId, sourceText), sourceText string(6) normalizado (sem espaço, caixa alta, sem acento), uomId. Máx. 20 por relação. Resolve a unidade escrita na nota (uCom/uTrib); nunca carrega fator, e texto que já é código de unidade não vira apelido |
StandardConversion | Catálogo do produto | Unidade de Medida · § Conversão. Nível 3: global, só intraclasse e só entre unidades com fator canônico; mantida pelo Nexio |
RefUneceUom | Catálogo do produto | Unidade de Medida · § Mapa. Os códigos ativos da Rec 20, só leitura, para mapeamento e integração; nunca lista de escolha |
SupplierAddress | Supplier | Fornecedor · § Endereços. Um endereço com vários papéis — não confundir com DeliveryLocation, que são vários pontos com um papel só |
SupplierContact | Supplier | Fornecedor · § Endereços e contatos. roles[] (Commercial, Tax, Finance, Technical, Quality, PortalAdmin — exatamente um ativo com PortalAdmin) e noticeChainSlot? marcam quem é canal do aviso. Cada valor de e-mail, telefone e celular grava origem, quando e por quem, e a proposta que o aplicou (*ChangeProposalId); noticeChainSince e noticeChainProposalId datam a entrada no Finance ou na cadeia. E-mail e telefone de Finance e da cadeia só mudam por proposta (RN-FOR-21, RN-FOR-34). 1 · 0..1 PortalUser |
SupplierDocument | Supplier | Fornecedor · § Documentos. Certidão com validade e storedFileId; libera a referência em vez de apagar. supplierEstablishmentId? é obrigatório quando scope = Establishment e proibido quando scope = Supplier: certidão é por CNPJ, e o fornecedor tem vários |
SupplierBankAccount | Supplier | Fornecedor · § Dados bancários. Versionada (validFrom/validTo), nunca UPDATE: muda só por BankAccountChangeProposal. accountKind (Domestic | International) discrimina o bloco; supplierEstablishmentId? (nulo = todos), currency → Currency e isDefault (uma padrão vigente por fornecedor e escopo). usableFrom materializa a carência e contestedAt tira a conta do pagamento para sempre — nenhuma das duas sai no pacote do ERP. Mascarada sob read:supplier-banks; sem máscara, com read-unmasked:supplier-banks — e o aprovador com tarefa ativa na instância vê sem máscara o diff daquela proposta, e só dela (RN-FOR-36) |
SupplierQualification | Supplier | Fornecedor · § Homologação. Por categoria (scopeCategoryIds), com riskTier (Low | Medium | High | Critical), outcome (Approved | ApprovedWithFindings | Rejected), findings[] e validTo; sem empresa — é do tenant. approvals[] é projeção da instância SUPPLIER, não coluna. É ela que autoriza o gasto, por consulta às qualificações vigentes (supplierId × categoryNode, casando por prefixo de path e respeitando validTo), e não um estado do fornecedor (RN-FOR-24) |
SupplierVerification | Supplier | Fornecedor · § Verificações em fonte oficial. Append-only: fonte, data, resultado e evidência; a de CNPJ grava a data de referência da base. Fonte ErpBankAccount (Match | Divergent | Unavailable) é a conferência diária com o ERP (RN-FOR-32) |
SupplierComplianceAlert | Supplier | Fornecedor · RN-FOR-37. Achado com dono, estado e decisão: supplierId, supplierEstablishmentId?, kind, source, verificationId, status, decision, decisionNote, decidedByUserId, decidedAt. Um aberto por fornecedor, tipo, fonte e registro de origem |
SupplierCategory | Supplier | Fornecedor · § Categorias fornecidas. Declarativa: descoberta e convite, nunca autoriza compra — quem autoriza é SupplierQualification (RN-FOR-18, RN-FOR-24) |
BankAccountChangeProposal | Supplier | Fornecedor · § Dados bancários · A proposta. Uma entidade para conta e para canal: changeKind (ProtectedChangeKind). Nasce Draft, recebe o comprovante (BANK_OWNERSHIP_PROOF) ou, sem telefone confiável, o pedido assinado (SIGNED_CONTACT_CHANGE_REQUEST, rastro em signedRequest) e, submetida, congela em dataHash. BankAccount e NoticeContact abrem instância de BANK_ACCOUNT_CHANGE com as flags do assunto — titular ≠ fornecedor (só em Foreign), chave PIX não verificada, noTrustedPhone, previousNoticeUndeliverable e contestedChannel; o Fornecedor aplica ao registro vivo pelo contrato de desfecho (RN-AP-38). Guarda a evidência do callback, a confirmação da segunda pessoa (ChainContact), o aviso (noticeSentAt/noticeBouncedAt — a janela mínima conta do último envio sem devolução) e a contestação, com token só em hash. origin = InitialLoad só na carga inicial (RN-FOR-31) |
PortalUser | Supplier · condicional | Fornecedor · § Endereços e contatos: credencial separada do contato, SupplierContact 1 · 0..1 PortalUser, com contactId, login, accountStatus, dataAccessScope, mfaEnabled e lastAccessOn. O Portal do Fornecedor propõe torná-lo raiz própria, ao lado de PortalAccessLink; enquanto a proposta não for validada, nenhuma das duas caixas entra |
CurrencySettingsChange | CurrencySettings · ato de controle | Moeda e Câmbio · § CurrencySettings e RN-CUR-28. N por tenant: items (field, processCode?, before, after), status (Pending | Applied | Discarded), quem propôs e quem efetivou ou descartou. defaultRateType, processRateTypes e maxRateAgeDays mudam só por ela, efetivada por outra pessoa; só a transição de status é escrita |
ApprovalSettingsChange | Tenant · ato de controle | Fluxo de Aprovação · § Mapa de entidades e RN-AP-69. N por tenant: alvo (configuração do tenant, do processo ou membros de grupo), diff campo a campo — valor vigente e valor novo —, proposedByUserId e quem efetivou ou descartou; status Pending · Applied · Discarded, e nenhuma escrita além dessa transição |
ApprovalRequestExcludedUser | ApprovalRequest | Fluxo de Aprovação · § Instância, passo e tarefa. userId · reason · roundNumber; declarado pelo módulo dono na submissão e acrescido a cada reavaliação — só cresce (RN-AP-60) |
Nem tudo o que tem nome é classe. Commitment, Obligation, Actual e Adjustment aparecem em toda a spec de Orçamento e não são entidades: são valores do estágio de um BudgetEntry. Procurar uma tabela Commitment é o erro que esta linha existe para evitar. SupplierFinanceContact também não é classe: é o resolvedor de audiência da Notificação que lê SupplierContact pelos papéis e pela cadeia de reserva.
Limites
O que ainda não está aqui
Seis agregados e um transversal a especificar — Receipt e Invoice dividem uma linha da tabela e são duas classes; são as mesmas seis caixas a especificar do mapa de domínio. 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 | Os três eixos — establishmentId (quem recebe, com allowsReceiving), requestingEstablishmentId (quem pediu, obrigatório, da mesma empresa, com allowsPurchasing) e companyId derivado e congelado (RN-EMP-33, RN-EMP-34); congela na submissão taxRegime da empresa e icmsTaxpayerStatus, stateRegistration e suframaCode do estabelecimento (RN-EMP-37); deliveryLocationId no cabeçalho e por linha, com o snapshot de entrega; linhas com CostAllocation sem empresa própria; número pela configuração vigente de NumberSequence, único por (tenantId, documentType, companyId, number). Na linha, itemId? — nulo é linha não catalogada, com description, categoryNodeId, uomId e predominantUse (RN-ITM-36) — e o snapshot do catálogo: itemSupplierId?, itemSupplierPriceId?, itemSupplierUomId?, convNum/convDen, priceConvNum/priceConvDen e factorSource (RN-UOM-10, RN-ITM-39) |
PurchaseOrder | Espinha transacional | Compra centralizada não é campo: é o comprador agindo, por escopo, no estabelecimento de cada empresa. Quais documentos da espinha carregam requestingEstablishmentId está em decisão |
Receipt · Invoice | Espinha transacional · three-way match | A nota chega endereçada a um CNPJ de 14, e o leiaute já tem IBS e CBS — exigidos a partir da data que a norma fixar, ausentes sem erro até lá (regra do Item) |
QuotationRequest | Espinha transacional | Depende da decisão sobre o portal do fornecedor, que tem proposta na mesa e ainda não foi validada, e da empresa da cotação multiempresa. QuotationRequestItem já está no AttachmentOwnerType, com as doze respostas declaradas como prévia que a spec da Cotação confere — para que a propagação e os tipos permitidos já sejam escritos com o nome certo; o enum é texto, e acrescentar valor não é migração |
Contract | Estrutural · antes do PurchaseOrder ou junto com ele | Nada no catálogo aponta para o contrato: o ContractItem referencia itemId ou itemSupplierId, com companyId? e vigência próprios, e sobrepõe preço, prazo e múltiplo na linha do pedido quando vigente para a empresa do documento (RN-ITM-34). Item.requiresContract já existe, reservado: só aceita false até esta spec liberá-lo. Na v1 o contrato nasce por cadastro direto; a cotação é origem opcional, gancho até a spec da Cotação |
| Transversais | ExternalRef | A referência externa de cada destino, gravada na integração (Mensageria A1.6); a entrega garantida até o ERP é requisito de plataforma (P1), não classe do Nexio. Os demais transversais já estão especificados — NumberSequence na Empresa, Currency e ExchangeRate na Moeda e Câmbio, Attachment, AttachmentType, AttachmentLegalHold e AttachmentErasureRequest no Anexo, as duas de aviso na Notificação, ReasonCode no Motivo; a trilha de ato de controle é do GrydAudit (pendência P4) |
Registro
Histórico de revisões
| Versão | Data | O que mudou |
|---|---|---|
| v1.31 | 30/10/2026 | Permissões padronizadas em read/lookup/create/update/delete (decisão de 30/10/2026). manage:currency-settings → update:currency-settings (efetivar a proposta Pending); manage:company-scopes → update:company-scopes (quem concede escopo, RN-EMP-24). a permissão de gestão de todos os tenants não muda: é do catálogo semeado do GrydAuth, não do Nexio. |
| v1.30 | 21/09/2026 | Validação de 18/09, achado 20 (decisão do Roger). O Contract deixa de ser o último: é especificado antes do PurchaseOrder ou junto com ele, com a cotação como origem opcional. Item.requiresContract fica reservado até lá (Cadastro de Item). Achado 21a: ReasonCodeUsage passa a dezessete usos com piso obrigatório — DocumentCancel é obrigatório (Motivo). Achado 22: BudgetPolicy ganha residueAlertAfterDays? int, a idade do aviso semanal de resíduo (Orçamento). Achado 16: BudgetEntryReason ganha PriceRevision, a revisão de preço do pedido aprovado (Orçamento, RN-O60). Achado 23: Item ganha handlesPersonalData bool (fica no "+ 39 campos") e o ApprovalCriteria passa a quinze critérios, com as flags do item de alcance de linha (Cadastro de Item, RN-ITM-43; Fluxo de Aprovação). |
| v1.29 | 20/09/2026 | Recuo da validação de 18/09 (achado 12). Sai “a cotação emite um pedido por empresa” da regra da fatia: como demanda de várias empresas se consolida é decisão da spec da Cotação, dentro da RN-EMP-26 da Empresa. ItemSupplierStatus: New | UnderQualification | Approved | Blocked → Active | Blocked (achados 13 e 14, Cadastro de Item RN-ITM-15). |
| v1.28 | 19/09/2026 | BudgetBalance: o total passa a totalAmount numeric(19,4), nome que o Orçamento agora dá à coluna (RN-O02); sai a marca “sem nome na dona”. Convenção “Banco de dados”: tudo o que é de arquitetura está definido na Arquitetura, e só nela (decisão D1). ApprovalRequestStatus: a alteração de documento aprovado que é ajuste não sai de Approved (Fluxo, RN-AP-71; decisão D4). BudgetEntryReason ganha Reconversion, e a convenção de moeda diz quando o documento reconverte (RN-CUR-22, RN-O59; decisão D3). |
| v1.27 | 19/09/2026 | Ressincronização com as páginas donas (T12, sessão a), depois da validação de 18/09/2026. Bandas: escopo por empresa com ausência = nenhuma empresa; fatia de rateio sem empresa; NumberSequence separada de NumberSequenceCounter; ponto de entrega com deliveryOperationType e o snapshot de entrega (DeliveryAddressSnapshot); razão por tupla concreta, com BudgetBalance no lugar de BudgetLineBalance; preço com freightTerms na chave e a unidade de compra (ItemSupplierUom); motor com fuso, reserva, retrato de critérios e ApprovalSettingsChange como satélite; anexo com AttachmentLegalHold e AttachmentErasureRequest; DeferredSubmission sai (fora da v1). Transversais: invariantes reescritas contra as donas, e uma nova — o documento tem três eixos; convenções com dez sufixos de erro, 96 permissões recontadas, vinte e nove atos de rota recontados, estorno sobre o grupo, doze perguntas do registro de dono, a moeda de quem limita, os tipos numéricos da Unidade de Medida, o menor dos dois nos limiares, e o evento pós-commit como pendência de plataforma (P1); banco, código de sucesso, forma de ato sobre instância e os eixos da espinha ficam com texto neutro até a decisão. Satélites de 14 para 25. Sai a marca "campo novo" das caixas, e as notas históricas do corpo vêm para esta seção. |
| v1.26 | 09/09/2026 | O vínculo item × fornecedor vira duas caixas (relação e versão de preço, append-only) — a quebra existe para que reajustar seja gravar versão nova. Convenções: estorno sobre a cadeia, trilha append-only sem soft delete, chave de relação × chave de preço, contagem de permissões (recurso no plural) e de atos de rota; registro de dono de nove para onze perguntas (OpenApprovalOf, ResponsibleOf) e AttachmentOwnerType de nove para quinze valores; BudgetStage de seis para quatro valores; ReasonCodeUsage de doze para dezessete usos; Line entra em ApprovalScopeMode. closedOn no estabelecimento deixa de forçar isActive. Na invariante do motivo, o corolário: motivo escolhido por máquina é enum. No fornecedor, SupplierDocument ganha supplierEstablishmentId?; SupplierBankAccount ganha accountKind e usableFrom (antes a carência existia só como frase); BankAccountChangeProposal ganha changeKind; a homologação passa a autorizar o gasto por categoria, no lugar do estado SpendAuthorized sem escopo. O jsonb do ponto de entrega foi reescrito como requisito. A pendência de banco ficou aberta, contra a recomendação da validação de 08/09/2026. |
| até v1.25 | 01–08/09/2026 | 08/09: citação sem número de versão (substitui a regra da manhã de 08/09, que mandava recorrigir as 184 citações a cada release); estilo de permissão invertido de recurso[.sub]:verbo para verbo:recurso; estado só por rota própria, sem exceção — centro de custo e categoria trocaram o DELETE que desativava, a moeda do tenant trocou o PUT; DELETE com um significado só; ato que não é CRUD; código de sucesso; efeito × evento; comparação na moeda funcional; tipos numéricos da Unidade de Medida (o percentual estava em (9,6) na fatia e (5,2) no limiar; o monetário, em (18,2) no Orçamento e (19,2) no resto); percentual × valor no limiar; o CategoryNode adere por inteiro à mecânica das árvores (até então tinha ltree e level 1-based); ApprovalSettings nasce com o que não varia por processo; ApprovalProcessSettings vira dono único do prazo; Draft e Expired saem de ApprovalRequestStatus; Item.lifecyclePhaseId ganha destino (ItemLifecyclePhase); a decisão do portal ganha proposta escrita. 04/09: toda aprovação humana passa a ser instância do motor (princípio 7), com os dez processos-semente e os tipos de nível PermissionHolder e Panel; tipo global com sobreposição por tenant no ReasonCode; edição do par de moeda global exige permissão de plataforma; nulos do fornecedor por entityType (RN-FOR-03). 03/09: version int + expectedVersion vira o token único do produto; headquartersId deixa de ser anulável; SupplierAddress e SupplierContact passam a supplierEstablishmentId; Notification e ReasonCode saem do backlog, nas bandas 8 e 9. 02/09: DeliveryLocation entra na banda 2; ItemDocument e SupplierDocument passam a guardar storedFileId em vez de URL; valores de enum do motor normalizados antes de existir dado (Obsoleto virou Obsolete, não Superseded). 01/09: Currency e ExchangeRate saem da banda do orçamento para a de moeda; ItemSupplier.establishmentId vira supplierEstablishmentId. As caixas marcavam em azul os campos nascidos ou mudados entre 01 e 03/09/2026. |