Especificação · Admin

Empresa e Estabelecimento

A entidade que faltava em quatro specs seguidas. Quem paga já tem resposta no centro de custo; quem assina e quem recebe a nota não tinha nenhuma — o tenant guardava um CNPJ solto, e grupo econômico só cabia no produto duplicando catálogo, árvore, alçada e usuário. Esta spec fecha a dupla Company + GLAccount e desbloqueia a espinha transacional inteira.

desbloqueia Requisition · PurchaseOrder · Receipt · Invoice revisou Centro de Custo, Fluxo de Aprovação e Cadastro de Item 3 agregados · 2 filhos · 2 tabelas de vínculo v1.22 · 26/10/2026

Ponto de partida

Oito decisões

Decisão 1 · Empresa

Empresa é entidade dentro do tenant, não tenant filho. Company nasce obrigatória e sempre existe, mesmo quando é uma só; o CNPJ solto do tenant morre — e TenantSettings, que esta spec tratava como entidade da plataforma, não existe no Gryd.IO. A plataforma já sabia agrupar organizações — o que ela não sabia era que uma organização tem mais de um CNPJ.

Decisão 2 · Tenancy

Os dois modos de isolamento são o mesmo modelo. Tenant com uma Company é o modo isolado; tenant com N é o consolidado. O domínio é escrito uma vez, para o consolidado — no isolado o segundo filtro recorta a única empresa que existe — e mesmo ali só alcança quem tem linha nela. Sem if, sem duas trilhas, sem dois jeitos de emitir um pedido.

Decisão 3 · Grupo

O tenant Group é eixo de acesso, nunca de dados. Nenhuma linha de nenhuma tabela pertence a um tenant do tipo grupo. No minuto em que ele guardar dado, todo filtro do sistema vira "tenant ou pai do tenant" — e um filtro que aceita o pai é um filtro que vaza.

Decisão 4 · Identidade

Dois níveis de CNPJ, espelhando o fornecedor. Company é a pessoa jurídica, chaveada pela raiz de 8; Establishment é o CNPJ de 14, matriz ou filial. É o mesmo corte de Supplier e SupplierEstablishment: um CNPJ tem um modelo mental só no produto, de dentro e de fora.

Decisão 5 · Escopo

O escopo por empresa é do Nexio, não da plataforma. UserCompanyScope guarda quais empresas cada usuário alcança, referenciando o userId do gryd sem chave estrangeira — exatamente como CostCenterAssignment já faz. Ausência de linha = nenhuma empresa, qualquer que seja o número de empresas do tenant, e não existe concessão "todas as empresas". O gryd não ganha coluna nenhuma, e não deveria: ele não sabe o que é uma Company.

Decisão 6 · Documento

O documento aponta para o estabelecimento; a empresa é derivada. Quem emite pedido e recebe nota é um CNPJ de 14, mas alçada, orçamento e conta contábil raciocinam em empresa. Um campo na interface, dois congelados no documento.

Decisão 7 · Contábil

Plano de contas no tenant, habilitação e código externo por empresa. Grupo econômico brasileiro usa plano unificado; o que varia é o código no ERP de cada CNPJ. Um plano por empresa multiplicaria manutenção por N e faria o consolidado depender de casar códigos por convenção.

Decisão 8 · Numeração

NumberSequence configurável, com empresa como padrão. Grupo brasileiro quase sempre quer pedido numerado por empresa; parte quer por filial; quase ninguém quer número global. O escopo é dado, não código.

Fronteira

O que a plataforma já resolve

A camada gryd nunca entrou no mapa de domínio, e foi por isso que empresa quase foi modelada duas vezes. Validado contra Design/Tenants Handoff, Usuarios Handoff e Roles Handoff.

Camada de plataforma · não se mexe
Já existeO que éO que o Nexio faz com isso
TenantA fronteira de isolamento. Type = Company | Group, ParentTenantId, hierarquia de um nível.Um tenant = um cliente contratante. O Nexio não usa ParentTenantId no modo consolidado.
UserGlobal — não pertence a tenant nenhum.Intacto. Nenhum campo novo.
UserTenantA matriz de acesso: uma linha por organização, com vigência, isDefault e isActive.intacto A preferência de estabelecimento padrão vive do lado do Nexio.
UserRoleO papel dentro daquele tenant. Não existe papel global.intacto O recorte por empresa é UserCompanyScope, do Nexio.
Role · PermissionCatálogo por tenant.Intactos. Os papéis são do tenant, não da empresa — o que varia é onde o papel vale.
Configuração de tenantA plataforma não tem TenantSettings. O CNPJ único por tenant que o Nexio lia era do próprio Nexio. Mas tem Tenant.Settings, e a recusa precisa dizer isso: um string? mapeado como documento semiestruturado, com UpdateSettings e leitura por JsonNode na mão — a única leitura tipada hoje é a política de MFA. O JSON genérico que a decisão recusa já está lá, a uma linha de distância, e alguém vai encontrá-lo. Recusá-lo é escolha: parâmetro do Nexio ali seria configuração sem esquema, sem validação de domínio e sem migration, num campo de outro módulo — e o primeiro a ler valida, ou não valida ninguém.removido — o CNPJ vive em Company.cnpjRoot e Establishment.cnpj. Os parâmetros por tenant do Nexio (teto de profundidade das árvores, materialidade do rateio, expediente e lista de dias não úteis) ganham entidade própria — pendência registrada no mapa de domínio. O calendário não pede entidade: virou campo do tenant no fluxo de aprovação, porque o SLA é o único consumidor de dia útil no produto.

Duas palavras, dois sentidos — e um deles muda. A tela de Tenants rotula o tenant filho como "Empresa". A partir desta spec, "empresa" significa Company e nada mais: o tenant passa a ser chamado de organização em tela, spec e conversa. É troca de rótulo, não de modelo — a tela já se chama "Tenants · Organizações" e só precisa parar de se contradizer. O valor de enum TenantType.Company permanece: é membro de enum, sempre qualificado, e não colide com a classe Company do domínio.

Tenancy

Os dois modos

Modo isolado · N organizações, uma empresa cada

Cada CNPJ vira um tenant, opcionalmente sob um tenant Group. O isolamento é estrutural: o dado da empresa A não é filtrado do dado da empresa B, ele está em outra fronteira. É o modo para quem tem restrição de verdade — empresas concorrentes dentro do mesmo grupo, sociedade com terceiros, exigência de auditoria independente.

O que custa: catálogo, fornecedores homologados, estrutura de centro de custo, papéis e alçadas são recadastrados por empresa, e não sincronizam. Aprovação consolidada não existe. Relatório do grupo exige leitura cross-tenant.

Modo consolidado · uma organização, N empresas

O tenant é o grupo econômico. Cadastro mestre é único e compartilhado; documento, orçamento, conta contábil e alçada são por empresa. O isolamento entre empresas passa a ser escopo — companyId derivado no servidor do establishmentId validado contra o escopo do usuário; nunca aceito do cliente e nunca lido do token, que não traz empresa.

O que ganha: matriz aprova pedido de filial com uma política, não com integração. Fornecedor é homologado uma vez. Consolidado do grupo é consulta comum.

O critério de qual vender

Não é tamanho do grupo nem número de CNPJs. É uma pergunta só: as empresas do grupo podem ver o cadastro mestre umas das outras? Catálogo, fornecedores, estrutura de centro de custo, tabela de preço. Sim — que é o caso da maioria dos grupos brasileiros, porque a estrutura é espelhada mesmo — consolidado. Não — tenants separados, e o cliente aceita duplicar cadastro.

A pergunta é sobre mestre, não sobre documento. Esconder as requisições da empresa B do usuário da empresa A o modo consolidado faz bem; o que ele não faz é esconder que a empresa B existe.

Na dúvida, consolidado

Um tenant consolidado consegue se comportar como isolado: escopa todo usuário a uma empresa, nunca cria registro corporativo, e na prática ninguém vê nada da outra. O inverso não existe sem cross-tenant. Migração entre os dois é dolorosa nos dois sentidos — juntar catálogos com códigos colidindo, ou partir documentos — então erra-se para o lado reversível por configuração.

O que não pode acontecer. O modo isolado não pode pular a Company e continuar lendo um CNPJ solto do tenant. Duas formas de descobrir de que CNPJ um documento é significa dois modelos, dois caminhos de código e dois conjuntos de bug. Company é obrigatória nos dois modos; no isolado ela simplesmente tem uma linha.

Modelo

Mapa de entidades

Convenção de nomes: o documento é escrito em português e todo identificador é em inglês — entidade, atributo, valor de enum, rota, permissão e código de erro. A exceção são os dados de negócio brasileiros, que ficam como são no mundo real: cnpj, stateRegistration, suframaCode, primaryCnae e os nomes de regime tributário (SimplesNacional, LucroReal) — esses são conteúdo, não código.

Tenant plataforma · gryd A fronteira de isolamento. Group agrega organizações e nunca guarda dado.
Company aggregate root · raiz de 8 A pessoa jurídica. Quem assina, quem tem regime tributário, quem tem orçamento e alçada.
Establishment N por empresa · CNPJ de 14 Matriz ou filial. Quem recebe a nota, com IE, endereço fiscal e situação cadastral próprios.
GLAccount aggregate root · árvore Conta contábil. Plano único do tenant, com caminho materializado — a terceira árvore do produto, e a mesma mecânica das outras duas.
GLAccountCompany vínculo · N×N Habilitação e código externo da conta em cada empresa. É o que a integração com o ERP consome.
NumberSequence uma vigente por tipo de documento Configuração da numeração, com escopo — tenant, empresa ou estabelecimento — e fuso próprios. O contador de cada partição é o NumberSequenceCounter.
UserCompanyScope Nexio · N por usuário Quais empresas cada pessoa alcança. Referencia o userId do gryd sem FK. Uma linha por empresa alcançada, com vigência. Ausência de linha = nenhuma empresa.
CostCenter eixo irmão legalEntityId vira companyId. A regra do nó corporativo já estava escrita e não muda.
DeliveryLocation irmão · depende desta Armazém, obra, ponto de entrega. Aponta para o estabelecimento que recebe fiscalmente. Modelado antes desta spec, nasceria errado.

Domínio · Company

Campos · Company

Legenda: obrig sempre · cond conforme a operação · opc opcional · gerado calculado pelo sistema ou pelo banco.

Identificação

CampoTipoRegra
idGuidgeradoAtribuído pelo agregado, não pelo banco.
tenantIdGuidgeradoInjetado a partir do token. Nunca aceito do cliente.
codestring(20)obrigTrim e maiúsculas, apenas [A-Za-z0-9_-], único por tenant, imutável. É o que a importação, a política de aprovação e a integração referenciam.
cnpjRootvarchar(8)obrigA raiz do CNPJ: 8 caracteres, 0–9 e A–Z, nunca numérico. Normalizada antes de validar — trim, sem máscara, letras em maiúsculas; fora do formato, 400 COMPANY_CNPJ_ROOT_INVALID. Único por tenant, imutável. Duas empresas do mesmo tenant não compartilham raiz: se compartilhassem, seriam a mesma pessoa jurídica.
legalNamestring(200)obrigRazão social. Trim; vazio é 400.
tradeNamestring(160)opcNome fantasia. É o que aparece no seletor de documento — quando ausente, cai para legalName.
externalCodestring(40)opcO código da mesma empresa no ERP do cliente. Único por tenant quando preenchido.

Natureza e regime

CampoTipoRegra
taxRegimeenumobrigSimplesNacional | MEI | LucroPresumido | LucroReal | TaxExempt | NotApplicable — o mesmo enum de Supplier.taxRegime, e não um paralelo: regime tributário é o mesmo conceito visto dos dois lados. Do lado comprador, NotApplicable é recusado. Vive na empresa, não na filial — o Simples é opção da pessoa jurídica inteira. É o campo que decide se a compra gera crédito. O documento congela o regime na submissão (RN-EMP-37): o Simples é opção anual, e mudar o regime pelo PUT vale para documento novo, sem reescrever o que já foi submetido.
legalNaturevarchar(4)opcCódigo de natureza jurídica da tabela do IBGE/Receita (2062, 2054…). Metadado; nenhuma regra depende dele hoje.
shareCapitaldecimal(18,2)opcCapital social. Informativo, e usado por alguns clientes como referência de porte na alçada.
functionalCurrencychar(3)geradoISO 4217. Na v1 é sempre BRL: a criação grava BRL, outro valor no POST é 422 COMPANY_FUNCTIONAL_CURRENCY_UNPROCESSABLE, e o PUT ignora o campo — ele é imutável, e continuará imutável depois do primeiro Budget quando outra moeda funcional for admitida. É coerente com o CNPJ e com a escrituração, que são em reais. Documentos, preços, limites de alçada e cotações seguem em qualquer ISO 4217: a comparação converte para a moeda do limite, com a taxa congelada na submissão — regra da Moeda e Câmbio, e o limite carrega a própria moeda no Centro de Custo. A spec de Orçamento copia este valor para Budget.currency na criação do exercício e o congela.
foundedOndateopcData de abertura.
closedOndateopcData de baixa. Fato de calendário: documento novo com data posterior é recusado com 422 COMPANY_CLOSED_UNPROCESSABLE, e o campo não mexe em isActive — quem desativa é PATCH /deactivate, e só ele. Preenchido em empresa ainda ativa, o PUT grava e a resposta avisa que falta desativar.

Estado e derivados

CampoTipoRegra
isActiveboolgeradoAlterado só por /activate e /deactivate, nunca por PUT. A desativação cascateia para os estabelecimentos ativos, na mesma transação. Desativar não emite documento novo; documento em andamento conclui. Com isActive = false, a empresa não é oferecida nem aceita em documento novo — criar ou submeter documento em estabelecimento dela é recusado —, e todo documento já submetido nela segue até o fim: aprovação, pedido, recebimento, nota, devolução e estorno. A desativação não é recusada por haver documento em aberto. O que a empresa emitiu continua legível para quem a alcança. Ver Escopo e segurança.
headquartersIdGuidgeradoO estabelecimento matriz. Derivado, não digitado — é o único com type = Headquarters. Nunca nulo: a matriz nasce na mesma transação que a empresa, então não existe empresa sem matriz nem por uma chamada. Regravado pela troca de matriz (RN-EMP-38).
establishmentCountintgeradoExibido na lista. Ativos e inativos separados no detalhe.
versionintgeradoToken de concorrência. PUT, /activate, /deactivate e /headquarters exigem expectedVersion; divergente, 409 COMPANY_CONCURRENT_CHANGE_CONFLICT (RN-EMP-40).

O que não está aqui, de propósito. Certidões, quadro societário, CNAE secundário e regime de retenção são do fornecedor, não da empresa compradora: o Nexio faz diligência de quem vende, não de si mesmo. A exceção é a situação cadastral do estabelecimento, porque ela decide se um documento pode ser emitido em nome dele — por isso está aqui, com a mesma fonte do fornecedor. Se um dia o produto virar portal e a empresa compradora precisar se apresentar a alguém, os demais campos entram no mesmo formato que já existe em Supplier.

Domínio · Establishment

Campos · Establishment

A empresa é quem assina; o estabelecimento é quem recebe a nota. Tudo o que muda de filial para filial vive aqui.

Identificação

CampoTipoRegra
idGuidgerado—
tenantIdGuidgeradoDo token. Denormalizado para caber no filtro de linha sem join.
companyIdGuidobrigDono. Imutável — um estabelecimento não muda de pessoa jurídica; isso seria baixa e abertura.
codestring(20)obrigÚnico por empresa, imutável. Cliente vindo de Protheus digita 0001, 0002; cliente novo digita MATRIZ, CD-REC. As duas convenções cabem.
cnpjvarchar(14)obrigCNPJ completo. Normalizado antes de validar: trim, sem máscara (saem ., / e -), letras em maiúsculas. Depois, 14 caracteres — 12 em 0–9A–Z e 2 dígitos verificadores — e DV conferido; formato ou DV inválido é 400 ESTABLISHMENT_CNPJ_INVALID. Único por tenant. Os 8 primeiros caracteres têm que ser o cnpjRoot da empresa — divergência é 422, não 400: o dado é bem formado e semanticamente impossível.
cnpjRootvarchar(8)geradoOs 8 primeiros caracteres do cnpj. Existe para a chave estrangeira composta (tenantId, companyId, cnpjRoot) → companies(tenantId, id, cnpjRoot), que faz o banco recusar estabelecimento com raiz de outra empresa (RN-EMP-03). Coluna gerada é exemplo de implementação; o equivalente é a aplicação preencher e a chave conferir.
typeenumgeradoHeadquarters · Branch, persistido como texto. Exatamente um Headquarters por empresa, sempre. A matriz nasce com a empresa; estabelecimento criado depois nasce Branch; o PUT ignora o campo; trocar a matriz é só PATCH /companies/{id}/headquarters, atômico (RN-EMP-38). Não se deriva da ordem do CNPJ: a matriz pode ter outra ordem que não 0001 — transferência de sede sempre permitiu, e com o CNPJ alfanumérico a ordem nem é numérica. O identificador matriz/filial da Receita, quando a consulta ao CNPJ o traz, é comparado: divergência gera aviso na tela e não troca o cadastro.
namestring(160)obrigComo a filial é chamada internamente. É o que aparece no seletor, sob o nome da empresa.
externalCodestring(40)opcO código da filial no ERP. Único por (tenant, empresa) quando preenchido.

Fiscal

CampoTipoRegra
icmsTaxpayerStatusenumobrigTaxpayer · Exempt · NonTaxpayer. É o indIEDest da NF-e (1 · 2 · 9) visto do lado do destinatário. Decide se stateRegistration é exigida e como o fornecedor tributa a operação.
stateRegistrationvarchar(20)condInscrição estadual. Obrigatória em Taxpayer (indIEDest = 1) — 422 ESTABLISHMENT_STATE_REGISTRATION_REQUIRED_UNPROCESSABLE; opcional em NonTaxpayer (indIEDest = 9, que pode ou não possuir IE); recusada em Exempt (indIEDest = 2, isento de inscrição) — 422 ESTABLISHMENT_STATE_REGISTRATION_NOT_ALLOWED_UNPROCESSABLE. Formato varia por UF — validação de formato é por estado e fica na camada fiscal, não aqui.
municipalRegistrationvarchar(20)opcInscrição municipal. Necessária para tomar serviço com retenção de ISS.
suframaCodevarchar(9)opcInscrição Suframa. Presente, muda a tributação de entrada — e é justamente o campo que some quando alguém modela filial como endereço.
primaryCnaevarchar(7)opcCNAE principal do estabelecimento. Pode divergir entre filiais da mesma empresa.
registrationStatusenumgeradoRegistrationStatus = Null · Active · Suspended · Unfit · ClosedDown — os códigos 01 · 02 · 03 · 04 · 08 da Receita, o mesmo enum do SupplierEstablishment, persistido como texto. Situação do estabelecimento: uma filial pode estar baixada com a matriz ativa. Fonte: o mesmo gancho de provedor de consulta ao CNPJ que o Fornecedor usa — na v1, espelho dos dados abertos da Receita, com defasagem de semanas —, consultado na criação do estabelecimento e na varredura periódica em lote; ou a correção manual com comprovante (RN-EMP-36). Null = ainda não verificado. Bloqueia documento novo em Suspended, Unfit e ClosedDown.
registrationStatusDatedate?geradoA data da situação cadastral, como a Receita a informa ou como consta no comprovante. É ela que decide entre a consulta e a correção manual: vale a de data mais recente.
registrationSource · registrationCheckedAtenum · instante?geradoProvider · Manual, persistido como texto, e o instante da última verificação.

Endereço fiscal

CampoTipoRegra
zipCodevarchar(8)obrigSem máscara.
street · number · complement · districtstringcondLogradouro e bairro obrigatórios; número aceita S/N.
cityIbgeCodevarchar(7)obrigCódigo do município, não o nome. É o que a NF-e carrega e o que resolve homônimo entre estados. O nome é derivado dele, nunca digitado livre.
stateCodechar(2)geradoDerivado do código IBGE. É a dimensão que muda o ICMS interestadual — por isso não pode ser digitada em separado e divergir do município.
countryCodechar(2)opcISO 3166-1, padrão BR. Reservado; estabelecimento fora do Brasil está fora de escopo.

Operação

CampoTipoRegra
allowsPurchasingboolopcPadrão true. Filtra quem pede: falso, o estabelecimento não pode ser requestingEstablishmentId de documento novo. Não impede receber — isso é o allowsReceiving. Escritório administrativo que não compra é caso real.
allowsReceivingboolopcPadrão true. Filtra quem recebe: falso, o estabelecimento não pode ser establishmentId de documento novo nem destinatário de ponto de entrega. Passar a falso com ponto de entrega ativo pendurado é recusado (RN-EMP-17). Não impede pedir — isso é o allowsPurchasing. Um centro de distribuição que só recebe tem allowsPurchasing = false e allowsReceiving = true, e os pontos de entrega dele funcionam.
isActiveboolgeradoSó por /activate e /deactivate. Desativar não emite documento novo; documento em andamento conclui. Com isActive = false, o estabelecimento não é oferecido nem aceito em documento novo — criar ou submeter documento que o aponte é recusado —, e todo documento já submetido que o aponta segue até o fim: aprovação, pedido, recebimento, nota, devolução e estorno. A desativação não é recusada por haver documento em aberto. Desativar o último estabelecimento ativo de uma empresa ativa é 409: empresa sem estabelecimento não consegue emitir nada e vira um registro morto que ninguém percebe.
versionintgeradoToken de concorrência. PUT, /activate, /deactivate e /registration-status exigem expectedVersion; divergente, 409 ESTABLISHMENT_CONCURRENT_CHANGE_CONFLICT. O PUT tem efeito fiscal em cascata sobre os pontos de entrega, e duas edições concorrentes não podem se sobrepor em silêncio. A cascata da empresa incrementa a versão de cada estabelecimento que desativa.

Transversal

Escopo e segurança

A pergunta que esta seção responde: no modo consolidado, o que impede o usuário da empresa A de ver o documento da empresa B.

A garantia muda de natureza — e isso precisa ser dito

No modo isolado a separação é estrutural: é outro tenant. No consolidado ela vira filtro, e filtro é aplicação. É um rebaixamento real, e o desenho abaixo existe para que o filtro falhe fechado — não para fingir que a garantia continua estrutural.

O conjunto permitido é resolvido no servidor, a cada requisição

O token é do GrydAuth e não muda — ele traz userId, tenantId, papéis e permissões, e não traz empresa. A partir desses dois primeiros, o nexio-service resolve o conjunto de empresas permitidas no mesmo ponto em que já monta o contexto do tenant: uma leitura indexada em UserCompanyScope, dentro da requisição, sem cache entre requisições até que se prove gargalo.

O cliente pode mandar um establishmentId — ele precisa, porque o usuário escolhe no formulário. O que ele não pode é que o valor seja aceito: em escrita, é validado contra o conjunto resolvido e fora dele responde 404; em listagem, o conjunto resolvido é o filtro, e um companyId na query apenas intersecta com ele. Nunca substitui.

Resolução que não roda não libera. Sem contexto de tenant, ou com a leitura do escopo indisponível, a requisição falha com 500 — nunca segue com um conjunto mais amplo, nem com o de uma requisição anterior. Revogar vale na requisição seguinte (RN-EMP-22, RN-EMP-25).

O recorte é filtro de aplicação — e espelha as garantias da leitura de grupo da plataforma

O Gryd.IO isola tenant por filtro global de consulta (HasQueryFilter, em TenantQueryFilterExtensions), não por política de linha no banco. O recorte por empresa é, portanto, sempre filtro de aplicação — conferido no código em 10/09/2026, commit 585dbfc8.

A plataforma já tem um recorte de leitura com as garantias certas, a leitura de grupo do ADR 0006 (?scope=group), e o UserCompanyScope as espelha uma a uma. (1) Conjunto vazio é nada: lá, conjunto vazio "nunca é tudo" (TenantContextAccessor); aqui, ausência de linha e união vazia não alcançam empresa nenhuma. (2) Escrita nunca sob recorte amplo: lá, gravar com escopo de grupo ativo é recusado (TenantSaveChangesInterceptor); aqui, toda escrita resolve uma empresa (RN-EMP-26). (3) O cliente não envia a autoridade: lá, o cliente nunca manda id de tenant; aqui, companyId vindo do cliente só intersecta. (4) O que não foi declarado é recusado, não ignorado: lá, ?scope=group em rota não declarada é recusado; aqui, toda entidade com companyId ou establishmentId é registrada no filtro, e a entidade que não estiver quebra o build (RN-EMP-25).

Fora de escopo responde 404, não 403

Empresa que existe mas não está no escopo do usuário se comporta como empresa que não existe. É a mesma decisão já tomada em COST_CENTER_NOT_FOUND — "não existe neste tenant, inclusive quando existe em outro". 403 confirmaria a existência do registro, que é metade do que um atacante quer.

Empresa inativa não emite documento novo, e não some da leitura

Desativar não emite documento novo; documento em andamento conclui. Com isActive = false, a empresa não é oferecida nem aceita em documento novo — criar ou submeter documento em estabelecimento dela é recusado —, e todo documento já submetido nela segue até o fim: aprovação, pedido, recebimento, nota, devolução e estorno. A desativação não é recusada por haver documento em aberto. O lookup não oferece os estabelecimentos dela, e a criação ou a submissão contra um deles é recusada com 422 COMPANY_INACTIVE_UNPROCESSABLE.

A resolução de leitura do UserCompanyScope ignora isActive — quem alcançava a empresa continua lendo tudo o que ela emitiu, a auditoria inclusive. Incorporação e encerramento são rotina em grupo brasileiro, e o produto existe justamente para guardar essa história. Empresa inativa aparece marcada como inativa; ela não some.

Desativar empresa é um ato só, e ele cascateia

PATCH /companies/{id}/deactivate desativa a empresa e os estabelecimentos ativos dela na mesma transação, com um evento por estabelecimento, e a resposta lista o que caiu junto. Não existe o passo “primeiro zere os estabelecimentos”, nem “primeiro conclua os documentos”: documento em andamento em qualquer um deles conclui (RN-EMP-05). PATCH /activate não cascateia de volta: reativar a empresa reabre a empresa, e quais filiais voltam é decisão de quem reativa. Cascata assimétrica, a mesma das duas portas do plano de contas.

A tabela só estreita — nunca concede

UserCompanyScope não dá organização, não dá papel e não dá permissão: quem decide isso continua sendo o token do GrydAuth. Ela só responde em quais empresas daquele tenant o que o token já concedeu tem efeito. A consequência prática importa: a linha de quem foi removido da organização no gryd já não dá nada, porque o token nunca mais traz aquele tenant — e a remoção a encerra, para que a readmissão não a reviva (RN-EMP-24). Não existe escalonamento de organização, papel ou permissão por este caminho, e por isso a ausência de chave estrangeira para o gryd não é uma fragilidade. O escalonamento entre empresas existe, e é o que as regras de concessão fecham (RN-EMP-19 a RN-EMP-24).

O que precisa ser garantido é o outro lado: companyId tem que ser de uma empresa do mesmo tenant da linha. Aí a chave é composta e local, dentro do esquema do Nexio: (tenantId, companyId) referenciando companies(tenantId, id), com índice único do lado da empresa. O banco recusa; ninguém precisa lembrar de validar.

Para acesso, ausência é nada

UserCompanyScope não tem nulo nem ausência que libere: sem linha vigente, a pessoa não alcança empresa nenhuma. Nulo com significado continua existindo onde descreve abrangência de cadastro, não acesso — CostCenter.companyId nulo é nó corporativo, e GLAccountCompany ausente é conta habilitada.

A diferença é a consequência do erro. Um cadastro abrangente demais aparece numa tela e se corrige; um acesso abrangente demais entrega o dado de outra empresa sem que ninguém veja. Por isso a regra do acesso é a fechada.

UserCompanyScope

A tabela que responde "quais empresas esta pessoa alcança"
CampoTipoRegra
idGuidgerado—
tenantIdGuidgeradoDo token. Nunca aceito do cliente.
userIdGuidobrigO userId do GrydAuth, sem chave estrangeira — é outro serviço. Validado contra o diretório do GrydAuth embutido, lido em processo, na escrita, nunca na leitura: leitura com id órfão simplesmente não casa com token nenhum.
companyIdGuidobrigObrigatória: não existe linha "todas as empresas". FK composta (tenantId, companyId) → companies(tenantId, id): empresa de outra organização é recusada pelo banco.
validFrominstante (UTC)geradoO instante em que a linha é gravada, pelo servidor — no ato de suporte da plataforma, o fim da carência (RN-EMP-24). Concessão com início futuro não existe.
validToinstante (UTC)?opcFim da vigência, exclusivo: a linha vale em [validFrom, validTo). Informado na concessão para acesso temporário — posterior ao instante da concessão, senão 422, e nunca posterior ao da linha de quem concede quando esta tem fim (RN-EMP-42) — ou gravado pelo encerramento, com o instante do ato. Instante e não data, para que revogar valha na requisição seguinte e não à meia-noite. Linha encerrada não reabre: reconceder é linha nova.
originenumgeradoGrant · CompanyCreation. Persistido como texto. CompanyCreation é a linha que POST /companies grava para quem criou a empresa (RN-EMP-20).
grantedByUserIdGuidgeradoQuem concedeu, do token. Em CompanyCreation, quem criou a empresa.
onGrantorChangeenum?geradoCascade · Restrict, persistido como texto. O que acontece com esta linha quando a vigência de quem a concedeu diminui (RN-EMP-42): Cascade quando a linha de quem concedeu, na mesma empresa, tinha validTo no instante da concessão — a linha é cortada junto —; Restrict quando não tinha — o ato exige decisão sobre ela. Gravado na concessão porque não se deriva depois: encerrar uma linha sobrescreve o validTo dela. Nulo nas linhas sem quem conceda dentro do produto: CompanyCreation e as do suporte da plataforma. Imutável; a linha reaberta por corte conserva o valor.
endedByUserId · endedAtGuid? · instante?geradoQuem encerrou a linha por ato, e quando. No encerramento de sistema pela remoção da pessoa da organização, endedAt é preenchido e endedByUserId fica nulo (RN-EMP-24). Linha que vence pelo validTo informado na concessão não os preenche: nada aconteceu.

A linha não se apaga nem se reescreve — só validTo, endedByUserId e endedAt são preenchidos, uma vez, no encerramento — e por isso ela é o próprio registro do ato. Unicidade por vigência sem sobreposição: para o mesmo (tenantId, userId, companyId) não existem duas linhas cujos intervalos [validFrom, validTo) se cruzem, a mesma mecânica do CostCenterAssignment (RN-EMP-22). É requisito, não recurso de banco: uma restrição de exclusão por faixa é exemplo de implementação; o equivalente na aplicação é serializar a escrita por (tenantId, userId) e conferir a sobreposição sob essa serialização. Sem a vigência na unicidade, reconceder a empresa A depois de uma linha encerrada exigiria apagar o histórico.

Como o conjunto é resolvido

Tabela de decisão. Cada linha é um caso de teste: o estado do tenant e as linhas do usuário determinam o conjunto, e o conjunto determina a resposta. GET /me/company-scope devolve o conjunto com a razão — reason, persistida como texto: Granted · NoGrant · Expired · Onboarding.

Estado do tenantLinhas do usuárioConjunto resolvidoResultado e erro
Sem empresa cadastrada (onboarding)QualquerVazioSó o cadastro da primeira empresa, para quem tem create:companies e update:company-scopes. Rota de documento responde 404 ESTABLISHMENT_NOT_FOUND. reason = Onboarding
Uma empresa ou N empresasNenhuma linhaVazio — o número de empresas não muda nadaNão opera documento nem dado de empresa. Listagem de agregado por empresa devolve página vazia (200); leitura por id responde 404 COMPANY_NOT_FOUND ou 404 ESTABLISHMENT_NOT_FOUND; escrita contra estabelecimento, 404 ESTABLISHMENT_NOT_FOUND. reason = NoGrant, e a interface diz que falta concessão. Cadastro do tenant, que não tem empresa — item, fornecedor, unidade —, segue só a permissão
Uma empresa ou N empresasSó linhas encerradas ou vencidasVazioIgual à linha anterior, com reason = Expired
Uma empresa ou N empresasLinhas vigentes em A e B{A, B}Lê tudo de A e B, empresa inativa incluída; documento em andamento conclui em qualquer uma; documento novo só nas ativas — em inativa, 422 COMPANY_INACTIVE_UNPROCESSABLE (RN-EMP-05). Qualquer outra empresa responde como inexistente, 404. reason = Granted
QualquerLinhas de antes da remoção da pessoa da organização no GrydAuthNão se aplica; vazio se readmitidaFora da organização o token não traz o tenant, e a remoção encerra as linhas. A readmissão não as revive: linha com validFrom anterior ao início do vínculo vigente não é lida, reason = Expired até concessão nova. Não sustenta a invariante (RN-EMP-24)
QualquerA resolução não rodou ou falhou — sem contexto de tenant, leitura indisponívelNenhumA requisição falha com 500. Nunca segue com conjunto mais amplo nem com o de outra requisição (RN-EMP-25)
QualquerExecução fora de requisição (job, consumidor)Não se aplicaNenhum job executa comando em nome de usuário; o job lê pelo tenant do próprio payload e não usa escopo de pessoa

Por que ausência de linha é nada, mesmo com uma empresa só. O padrão permissivo tinha um defeito que nenhuma tela compensa: negar tudo e liberar tudo viravam a mesma operação. Esvaziar o escopo de alguém — PUT com lista vazia, ou a última linha vencida — dava a essa pessoa todas as empresas do tenant. E "permissivo enquanto houver uma empresa" não resolve: a segunda empresa chega por um cadastro comum, e a regra muda de sentido para todo mundo no mesmo instante.

O custo do caminho fechado é operacional, e está pago em três lugares. Quem cria a empresa já a alcança (RN-EMP-20). O convite de usuário feito pelo Nexio pede as empresas antes de enviar. E a tela de criação de empresa lista quem alcançava todas as empresas até então — responsáveis de nó corporativo e aprovadores de reserva incluídos — para concessão explícita, pessoa por pessoa (RN-EMP-27). Nada é concedido sozinho.

Estabelecimento padrão — na preferência da plataforma

A pré-seleção do cabeçalho do documento é preferência de interface, e vive na UserPreference da plataforma — chave nexio.default-establishment-id, valor o Guid em texto. O Nexio não cria tabela nenhuma para isto: lê a linha da UserPreference em processo, pelo repositório do módulo (o GrydAuth está embutido no host), e a tela escreve por PUT /auth/me/preferences/{key}, rota do próprio host.

Preferência de tela é conceito da plataforma. "Preferências do usuário" é conceito da plataforma, e criar uma tabela homônima do lado do produto convidaria duas fontes para a mesma pergunta — por isso mesmo o lugar é a tabela da plataforma. Ela existe, conferida no código: UserPreference : TenantScopedEntity, par chave-valor único por (userId, tenantId, key), com controlador, rota em lote e as chaves conhecidas como constantes abertas — theme, language, sidebarCollapsed —, não enum. Acrescentar chave é a prática corrente da plataforma, não uma exceção.

O motivo que decidiu não foi conveniência, foi correção. localStorage é do navegador e não conhece tenant: quem opera em dois tenants — contador, BPO, o próprio time — teria um padrão só, apontando para um estabelecimento que no outro tenant responde 404. A UserPreference é escopada por (usuário, tenant), que é exatamente o grão do problema. E ela acompanha quem troca de máquina.

O que o Nexio paga por isso, e como. A UserPreference não tem esquema nem validação de domínio — o valor é texto livre — e não cascateia: desativar o estabelecimento, ou revogar o escopo da pessoa, não apaga a linha. Logo quem lê valida: se o Guid não aponta mais para um estabelecimento ativo que aquela pessoa alcança, a tela ignora a preferência e cai no comportamento sem padrão. O localStorage precisaria exatamente da mesma guarda, então não é custo novo — é o mesmo custo, num lugar que funciona.

A chave é do Nexio e está declarada aqui, com o prefixo do produto e em minúsculas, porque a plataforma normaliza a chave para lowercase na gravação — defaultEstablishmentId viraria defaultestablishmentid em silêncio. É a mesma disciplina do perfil de arquivo no GrydFiles: o produto declara o que usa do espaço da plataforma, em vez de usar e não contar. E a porta de saída continua aberta, com outro gatilho: se um dia a preferência precisar de esquema, integridade referencial ou consulta por valor, nasce UserDefaultEstablishment no Nexio — não porque a plataforma não tenha onde guardar, mas porque o dado terá deixado de ser preferência de tela.

Onde a empresa entra, e onde não entra

A dimensionalidade de cada agregado — a tabela que evita a próxima discussão
AgregadoEmpresaPor quê
Item · ItemType · CategoryNodenenhumaCatálogo é vocabulário do grupo. Um item que existe só numa empresa é um item que ninguém mais acha.
UnitOfMeasurenenhumaQuilo é quilo.
Supplier · SupplierEstablishment · ItemSuppliernenhumaDiligência é sobre quem vende, não sobre quem compra. Cadastrar o mesmo fornecedor N vezes é o custo que o modo consolidado existe para eliminar.
Role · PermissionnenhumaO catálogo de papéis é do tenant. O que varia é onde o papel vale — e isso está em UserRole.
GLAccountnenhumaPlano unificado. A variação por empresa está em GLAccountCompany.
CostCenteranulávelNó corporativo ou nó de uma empresa. Regra já escrita no Centro de Custo.
ApprovalPolicycritérioSem coluna de empresa: o recorte é o critério Empresa em lista, que já existia esperando o campo do documento. Uma coluna criaria um segundo mecanismo para a mesma pergunta.
UserRole · UserTenantnenhumaSão do gryd e ficam intactos. O recorte por empresa mora em UserCompanyScope, do Nexio
UserCompanyScopeobrigatóriaUma linha por empresa alcançada, com vigência. Ausência de linha = nenhuma empresa.
GLAccountCompany · BudgetobrigatóriaSão, por definição, o recorte por empresa.
CostAllocation — a fatia de rateiodo documentoSem coluna de empresa: a fatia usa sempre a empresa do documento, e é contra ela que a invariante do centro de custo e a escolha do Budget são lidas. Não existe rateio entre empresas na v1. Ver Compra centralizada.
Requisition · PurchaseOrder · Receipt · InvoiceestabelecimentoApontam para o estabelecimento e congelam a empresa derivada. Documento não muda de significado depois de emitido — nem se o cadastro mudar.

Domínio · GLAccount

Plano de contas

O eixo que apareceu como pendência explícita na spec de Item ("mapeamento categoria → conta contábil … provavelmente deveria estar na próxima") e como fatia do rateio na de Centro de Custo. Fecha aqui.

A terceira árvore — e a mesma mecânica

Centro de custo e categoria já são árvores com caminho materializado, código imutável e ancestrais derivados sem recursão. Plano de contas é a terceira, e reusa a mecânica inteira: parentId, path com separador nas duas pontas, depth, duas portas de status com cascata assimétrica, path como guarda de move e version int + expectedVersion como token de concorrência de toda escrita — o mesmo token de Item, Fornecedor e Centro de Custo. Inventar uma quarta forma de fazer árvore neste produto seria erro por escolha, não por descuido.

GLAccount
CampoTipoRegra
codestring(30)obrigÚnico por tenant, imutável. Contas contábeis brasileiras já são hierárquicas no próprio código (3.1.01.001) — o produto não deriva o pai do código: quem manda é parentId, e o código é rótulo.
namestring(160)obrigÚnico entre irmãos.
parentIdGuid?opcNulo = raiz. Ciclo é recusado. Movimentação só por PATCH /{id}/parent.
path · depthvarchar · intgeradoIdênticos aos do centro de custo, inclusive o escape do separador.
accountTypeenumobrigAsset · Liability · Equity · Revenue · Expense. Herdado do pai por padrão; divergir do pai é 422.
versionintgeradoToken de concorrência de toda escrita, como no Centro de Custo: expectedVersion no PUT, no PATCH /parent e na habilitação por empresa; divergência é 409 GL_ACCOUNT_CONCURRENT_CHANGE_CONFLICT. path fica como guarda de move.
allowsPostingboolopcVerdadeiro em folha, falso em nó com filhos — a mesma regra do centro de custo. Nó intermediário existe para consolidar, não para receber lançamento.
validFrom · validTodate?opcPlano de contas muda de exercício para exercício. Conta fora de vigência não é oferecida em documento novo e continua legível em documento antigo.
isActiveboolgeradoDuas portas, cascata assimétrica.
GLAccountCompany · o vínculo que a integração consome
CampoTipoRegra
glAccountId · companyIdGuidobrigChave composta única.
isEnabledboolobrigConta habilitada naquela empresa. Ausência de linha = habilitada — o padrão é o plano inteiro valer para todas, e a linha existe para restringir ou para carregar o código externo. O ponto de aplicação é um só: a escolha da conta na fatia de rateio, nos três degraus da cascata, no lookup e de novo na submissão — sempre contra a empresa do documento.
externalCodestring(40)opcO código da mesma conta no ERP daquela empresa. É a razão de existir desta tabela: plano unificado no Nexio, códigos diferentes lá fora. Resolução do código enviado ao ERP (RN-EMP-39): este campo, se preenchido; senão GLAccount.externalCode; sem nenhum dos dois, o movimento não sai e fica visível como aguardando código externo — nunca enviado sem código.

De onde vem a conta da linha

Sugerida em cascata, escolhida por gente

A conta da fatia de rateio é sugerida por, nesta ordem: 1 Item.defaultGlAccountId, 2 CategoryNode.defaultGlAccountId do nó do item, subindo a árvore de categoria até achar, 3 vazio — e vazio significa o usuário escolhe: a fatia aceita conta nula só em rascunho, e a submissão do documento a exige (422 ALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE, RN-CC-29 do Centro de Custo). Sempre editável por quem tem permissão, e sempre com a origem visível na interface — "sugerida pela categoria Materiais de escritório" é a diferença entre um campo que o usuário confere e um campo que ele ignora.

E a cascata é filtrada pela empresa. Nos três degraus, conta com GLAccountCompany.isEnabled = false para aquela empresa não é sugerida; escolhida à mão, é recusada com 422 GL_ACCOUNT_NOT_ENABLED_FOR_COMPANY_UNPROCESSABLE — na fatia e outra vez na submissão, pela mesma razão de a conta obrigatória ser conferida duas vezes. O GET /gl-accounts/lookup exige companyId na query: lookup que não sabe a empresa devolve conta que o documento vai recusar, e devolver opção inválida é pior do que não devolver nada. A empresa lida é a do documento — a fatia não tem empresa própria; ver Compra centralizada.

Nenhuma das duas árvores manda na outra

Centro de custo responde quem paga; conta contábil responde que natureza é o gasto. A linha carrega as duas, e derivar uma da outra é o erro que transforma relatório contábil em relatório gerencial errado. Os dois eixos se cruzam na fatia de rateio e em nenhum outro lugar.

O Nexio classifica, não escritura

A conta existe para que o documento saia classificado e a integração com o ERP não precise adivinhar. Partida dobrada, encerramento, razão e balancete continuam do outro lado. Essa fronteira precisa estar escrita porque é a primeira coisa que um controller pede na demo.

Transversal

Numeração

Duas peças. NumberSequence é a configuração — uma só vigente por tipo de documento no tenant; NumberSequenceCounter é o contador de cada partição que o escopo cria — um no tenant, um por empresa ou um por estabelecimento. Trocar a forma de numerar é substituir a configuração, nunca editá-la (RN-EMP-28 a RN-EMP-32).

NumberSequence
CampoTipoRegra
documentTypeenumobrigRequisition · QuotationRequest · PurchaseOrder · Receipt · BudgetChangeRequest. Cresce com a espinha transacional. O BudgetChangeRequest está na lista porque o Orçamento numera o pedido de verba por aqui, com escopo Company — sequência que uma spec usa e a outra não declara é numeração que ninguém consegue configurar na tela. Contract continua fora, e de propósito: nenhuma página o numera. O escopo de numeração da QuotationRequest é fixado pela spec da Cotação, junto com a consolidação multiempresa.
scopeenumobrigTenant · Company · Establishment, persistido como texto. Padrão Company. Diz como os contadores se particionam. Imutável.
prefixstring(10)obrigTexto fixo no início do número — PC- —, de 1 a 10 caracteres. Imutável: prefixo novo é configuração nova, e não pode ser começo de prefixo anterior do mesmo documentType nem começar com um (RN-EMP-29). Obrigatório porque o prefixo vazio é começo de qualquer outro: a configuração sem prefixo nunca poderia trocar de prefixo.
paddingintopcDígitos mínimos do contador, com zeros à esquerda, de 1 a 21 — senão 422 NUMBER_SEQUENCE_PADDING_UNPROCESSABLE. Padrão 6. O teto é o que cabe no número: varchar(60) menos prefixo 10, código do estabelecimento 20, período 7 e dois hífens (39). O único campo editável: vale do próximo número em diante, e não cria colisão porque o contador continua.
resetPolicyenumopcNever · Yearly · Monthly, persistido como texto. Padrão Yearly, o hábito do mercado. Imutável.
timeZonestring(64)obrigFuso IANA em que o período é calculado. Semente America/Sao_Paulo. Imutável. Sem ele o período sairia em UTC, e a virada do ano aconteceria às 21h de 31/12 em Brasília.
effectiveFrominstante (UTC)obrigA partir de quando a configuração emite. Padrão: o instante da criação. Futuro só na substituição por período novo (RN-EMP-29); passado é 422.
retiredAtinstante (UTC)?geradoGravado pela substituição com o effectiveFrom da sucessora. A configuração vigente num instante é a de effectiveFrom ≤ instante < retiredAt; o estado — agendada, vigente, aposentada — é derivado, não coluna.
replacesSequenceIdGuid?condObrigatório quando já existe configuração vigente ou agendada para o documentType: aponta para a mais recente delas. FK para a própria tabela.
versionintgeradoToken de concorrência. A substituição manda o expectedVersion da substituída.
NumberSequenceCounter
CampoTipoRegra
sequenceIdGuidobrigA configuração dona. Contador não passa de uma configuração para outra: a sucessora começa os seus.
companyIdGuid?condPreenchido em Company e Establishment; nulo em Tenant. FK composta à empresa do tenant.
establishmentIdGuid?condPreenchido só em Establishment. FK ao estabelecimento. Nenhuma das duas chaves usa valor sentinela.
partitionKeyvarchar(40)geradotenant, ou o companyId, ou o establishmentId, em texto — nunca nulo. Unicidade em (sequenceId, partitionKey): é ela que faz duas emissões simultâneas da mesma partição caírem no mesmo contador, sem depender de como o banco compara nulos.
currentPeriodKeyvarchar(7)geradoO período do último número emitido pela partição: 2026 em Yearly, 2026-03 em Monthly, vazio em Never. Só avança (RN-EMP-31).
nextValuebigintgeradoO próximo valor do contador na partição.

A emissão é uma operação atômica sobre o contador. Calcula o período pelo relógio do servidor no timeZone da configuração; se ele for maior que currentPeriodKey, grava o período e usa 1; senão, mantém o período guardado e usa nextValue; incrementa e devolve período e valor — tudo numa operação só, nunca leitura seguida de escrita na aplicação. Exemplo de implementação: um UPDATE … RETURNING com a condição dentro; equivalente: trava da linha do contador, leitura e escrita numa transação curta. A emissão roda em transação própria, confirmada antes da transação do documento: a trava do contador não fica presa enquanto o documento grava, e o número consumido continua consumido se o documento falhar — é a lacuna permitida (RN-EMP-32). O contador nasce na primeira emissão da partição, por inserção protegida pela unicidade; quem perde a corrida relê.

Como o número é composto

prefix + código do estabelecimento e hífen, só em Establishment + período e hífen, só com resetPolicy diferente de Never + contador com padding. PC-2026-000001 em Company ou Tenant anual; PC-SP01-2026-03-000001 em Establishment mensal; PC-000001 em Never. O número cabe em varchar(60) porque padding vai até 21. O código do estabelecimento está no número porque o número emitido é único por empresa, em qualquer escopo (RN-EMP-32): sem ele, PC-2026-000001 sairia igual em duas filiais da mesma empresa. Número emitido é imutável e nunca é renumerado.

Exemplo 1 · troca de escopo

Empresa ALFA, com os estabelecimentos SP01 e RJ01, e a empresa BETA. Configuração S1 de PurchaseOrder: Company, PC-, Yearly, America/Sao_Paulo. Em 15/11/2026 o último pedido da ALFA é PC-2026-000150. O grupo decide numerar todos os pedidos numa série só.

QuandoAtoResultado
15/11/2026 10:00S2: Tenant, PC-, Yearly, mesmo fuso, início imediato, substituindo S1Recusado — 422 NUMBER_SEQUENCE_REPLACEMENT_UNPROCESSABLE. Mesmo prefixo sem período novo: o contador único do tenant começaria em 1 e emitiria PC-2026-000001 de novo na ALFA
15/11/2026 10:02S2: Tenant, prefixo PC-2026-, Never, início imediato, substituindo S1Recusado — 422 NUMBER_SEQUENCE_PREFIX_OVERLAP_UNPROCESSABLE. O prefixo é novo, mas começa com PC-, de S1: sem período no número, o contador emitiria PC-2026-000001, que S1 já deu à ALFA
15/11/2026 10:05S2: Tenant, prefixo PG-, início imediato, substituindo S1 com o expectedVersion delaAceito. S1 recebe retiredAt = 15/11/2026 10:05. O próximo pedido da ALFA é PG-2026-000001; o seguinte, da BETA, PG-2026-000002
15/11/2026 10:05 — alternativaS2: Tenant, PC-, Yearly, America/Sao_Paulo, effectiveFrom = 01/01/2027 00:00 em São Paulo, substituindo S1Aceito, agendado. S1 recebe retiredAt = aquele instante e continua emitindo até lá (PC-2026-000151…); a primeira emissão de 2027 já é de S2: PC-2027-000001, sem colisão — nenhuma configuração de PurchaseOrder com PC- e Yearly emitiu 2027 (RN-EMP-29)
20/11/2026Nova substituição apontando para S1, com S2 agendada409 NUMBER_SEQUENCE_CONCURRENT_CHANGE_CONFLICT: a substituição aponta sempre para a configuração mais recente — S2

Exemplo 2 · virada de período no fuso

Configuração Company, PC-, Yearly, America/Sao_Paulo. Contador da ALFA com currentPeriodKey = 2026 e o último número PC-2026-000151.

Relógio do servidor (UTC)Em São PauloPeríodo calculadoNúmero emitido
01/01/2027 02:30:0031/12/2026 23:30:002026 — igual ao guardadoPC-2026-000152. Calculado em UTC, o período já seria 2027 e o contador zeraria três horas antes
01/01/2027 03:00:0501/01/2027 00:00:052027 — maior que o guardadoPC-2027-000001. O período avançou: grava 2027 e zera
01/01/2027 02:59:58, num servidor com o relógio atrasado, emitindo logo depois31/12/2026 23:59:582026 — menor que o guardadoPC-2027-000002. Não zera nem volta: usa o período guardado
02/01/2027 12:00:0002/01/2027 09:00:002027PC-2027-000003 para um pedido com data de 20/12/2026. A data do documento não escolhe o período; a emissão, sim

O número pode ter buracos, e isso não é defeito. Transação abortada consome o número e não o devolve; colisão recusada também. Prometer sequência sem furo obrigaria a serializar a emissão do documento inteiro, e o cliente que precisa de sequência contínua está pensando em NF-e — que o Nexio não emite: quem emite é o fornecedor. O número do Nexio não é fiscal.

Operação

Compra centralizada

Compra centralizada é rotina em grupo brasileiro, e chega em dois arranjos que o produto precisa distinguir. No primeiro a matriz compra e recebe — e aí a diferença entre suportar e não suportar são dois campos criados agora, contra uma mudança de chave em agregado já povoado, depois. No segundo a nota tem que cair na filial e quem opera é a central de compras — e isso não é dimensão do documento, é escopo de usuário. Os dois estão escritos abaixo, e nenhum dos dois pede campo além dos três da tabela.

Campo do documentoO que respondeRegra
establishmentIdQuem recebe fiscalmenteObrigatório, com allowsReceiving. Define a empresa do documento, a numeração e a política de aprovação. Quem opera a compra é usuário, não campo: sai de UserCompanyScope, e o lookup devolve só o que aquele usuário pode usar.
requestingEstablishmentIdQuem pediuObrigatório, nunca nulo: igual ao establishmentId quando quem pede é quem recebe — a interface preenche assim. Da mesma empresa do establishmentId, com allowsPurchasing. É o que faz o pedido aparecer para quem solicitou.
companyIdEmpresa do documentogerado — derivada de establishmentId e congelada. Documento não muda de dono porque o cadastro mudou.

Caso 1 · A matriz compra e recebe; a filial da mesma empresa pediu

establishmentId da matriz, requestingEstablishmentId da filial. A nota chega no CNPJ da matriz e o pedido aparece para quem solicitou. É o caso que os dois campos existem para registrar.

Matriz e filial são a mesma empresa, e por isso este arranjo não pede nada da fatia de rateio: a fatia usa a empresa do documento, e a invariante do centro de custo — linha da empresa A só aponta para nó corporativo ou nó de A — é lida contra ela. Quem escreve a mecânica do rateio é o Centro de Custo.

Caso 2 · A nota cai na filial; a central de compras opera

establishmentId da filial, requestingEstablishmentId igual a ele — quem pede e quem recebe são o mesmo estabelecimento. O comprador lotado na matriz age por UserCompanyScope: como tem escopo nas duas empresas, o GET /establishments/lookup já lhe devolve o estabelecimento da filial. Nenhum campo novo, e nenhum é necessário — centralizar compra é arranjo de trabalho, não dimensão do documento.

A política de aprovação segue quem recebe

Nos dois casos o fluxo é o da empresa do establishmentId do documento, pelo critério Empresa em lista — nunca o da empresa de quem operou a compra. É coerente com o resto: orçamento, conta contábil e alçada raciocinam em empresa, e a empresa que responde pelo gasto é a que recebe a nota.

Quem pede e quem recebe são da mesma empresa

requestingEstablishmentId de outra empresa é recusado com 422 REQUESTING_ESTABLISHMENT_COMPANY_MISMATCH_UNPROCESSABLE. Não existe rateio entre empresas na v1: a fatia usa sempre a empresa do documento, e a coluna de empresa na fatia não existe. Como demanda de várias empresas se consolida é decisão da spec da Cotação, que tem de respeitar a RN-EMP-26 — nenhum comando recebe conjunto de empresas — e a RN-EMP-34.

A transferência contábil fica fora

Cada documento é de uma empresa só, e o Nexio não cria acerto entre empresas. O que o grupo fizer por fora — nota de transferência, rateio intercompany, lançamento manual — é ERP e continua sendo ERP. Criar rateio entre empresas depois é coluna nova na fatia, preenchida a partir do documento.

Não existe buyerEstablishmentId. O documento tem três eixos e só três: quem recebe (establishmentId), quem pediu (requestingEstablishmentId) e a empresa derivada (companyId). Quem opera é usuário, resolvido por escopo — e por isso não aparece no esquema. Um quarto eixo pelo lado do comprador é o erro que esta seção existe para evitar: depois do agregado povoado, é mudança de chave.

Contexto

Gancho fiscal

Duas mudanças de 2026 que tornam o estabelecimento obrigatório, e não uma conveniência de modelagem.

CNPJ alfanumérico — em produção desde 31/07/2026

Coluna varchar, nunca numérica, dos dois lados do produto. Os 12 primeiros caracteres aceitam A–Z; os 2 dígitos verificadores continuam numéricos e o cálculo usa ASCII − 48. Já era regra em Supplier; entra idêntica em Company e Establishment.

IBS e CBS obrigatórios na NF-e desde 03/08/2026

Os campos de IBS e CBS entraram no leiaute do documento fiscal eletrônico, com a alíquota de teste de 1% (0,1% IBS e 0,9% CBS) durante 2026. O Nexio não emite NF-e — mas recebe, e o módulo de nota e o three-way match nascem tendo que saber lê-los. Ausência não é erro: durante 2026 as notas chegam com IBS/CBS incompletos ou ausentes e ainda assim autorizadas, porque o Ato Técnico Conjunto RFB/CGIBS nº 1/2026 adiou as validações, não a obrigação. A regra do produto é a que o Cadastro de Item já escrevia, e é ela que vale nas três páginas que tocam no assunto: os campos existem no leiaute, o preenchimento só é exigido a partir da data que a norma fixar, e o parser não trata ausência como erro.

Para esta spec a consequência é uma só, e é a razão de o estabelecimento existir: os campos que decidem tributação e crédito estão espalhados nos dois níveis — regime na empresa, contribuinte de ICMS, Suframa e município IBGE no estabelecimento. Modelar filial como endereço perde metade deles.

O que continua fora

Parametrização fiscal completa — CFOP, CST, substituição tributária, benefícios estaduais, regra de crédito por operação — é spec própria, junto com a nota fiscal. Aqui nascem apenas os portadores de identidade fiscal, e eles nascem certos.

Integração

Relação com os demais domínios

O que muda em cada spec já fechada
DomínioMudançaDetalhe
Centro de CustofeitolegalEntityId → companyId no nó e em legalEntityOf(nodeId) → companyOf(nodeId). A regra — nó corporativo, nó de empresa, filho não muda de empresa, linha da empresa A só aponta para corporativo ou A — é a do Centro de Custo, só com o nome novo. Rename de coluna sem dado para migrar. O nó aponta para a empresa, não para o estabelecimento: apontar para o estabelecimento multiplicaria a árvore por CNPJ, que é o que o nó corporativo existe para evitar.
Fluxo de AprovaçãofeitoO critério "Empresa em lista" lê companyId do documento, derivado do estabelecimento. A política não ganha coluna de empresa: o critério já faz o recorte, e uma Base do grupo convive com uma Supplemental de uma empresa só. Novo critério irmão por establishmentId, e um nível que resolve para aprovador sem escopo na empresa do documento passa a cair no fallback.
Cadastro de ItemfeitoItem.defaultGlAccountId e CategoryNode.defaultGlAccountId, ambos opcionais. Dão ao item o eixo contábil. Nenhuma mudança de dimensionalidade: item continua sem empresa.
FornecedornenhumaFornecedor é do tenant. Fica declarado o gancho SupplierQualification.companyId anulável — homologar um fornecedor só para uma empresa do grupo é pedido plausível, e o campo tem o lugar reservado sem ser especificado agora.
Plataforma grydnenhumaTenant, User, UserTenant, UserRole, Role e Permission ficam intactos. A plataforma não conhece Company e não deve conhecer — colocar companyId num agregado do gryd inverteria a dependência, fazendo a plataforma depender de uma tabela de um produto que roda sobre ela. O único consumo novo é de leitura: GET /users/directory, que o nexio-service já usa no seletor de gestor de centro de custo.

Escopo por papel × empresa ficou fora, sem coluna reservada. Ele exigiria o Nexio consumir e cachear o catálogo role → permission do GrydAuth, porque o SessionDto entrega permissions já achatado no tenant. O caso que o justificaria — aprovar numa empresa e requisitar em outra — é resolvido por CostCenterAssignment, que é por nó, e o nó tem empresa, mais escopo na empresa do nó (RN-CC-30). Uma coluna reservada que o sistema recusa preencher não economiza migração nenhuma e contradiz "a tabela só estreita": se o escopo por papel vier, nasce como coluna nova, com a regra dela.
Unidade de MedidanenhumaEscopo global ou de tenant, como está.
Poda do webappacrescentaEmpresa e estabelecimento entram na lista do que o admin precisa ter na primeira versão — sem eles, o cadastro de centro de custo por empresa não tem o que oferecer no seletor.

Implementado: a coluna “feito” da tabela acima registra que a spec do outro domínio foi revisada. No código, esses contextos (Approval, Budget, Catalog, Procurement, Reference, Sourcing) ainda são esqueletos; CostCenter e DeliveryLocation não existem. O que a Empresa entrega a eles são as portas publicadas da CO-19, em Nexio.Organization.Contracts (namespace Documents, mais INumberIssuer em Numbering), registradas nos quatro hosts por AddOrganization(): a regra mora aqui, uma vez, e o consumidor não a reescreve. As portas partem do estabelecimento e usam a mesma leitura filtrada pelo escopo da pessoa: estabelecimento inexistente, de outro tenant ou fora do conjunto responde igual, ESTABLISHMENT_NOT_FOUND; num fluxo sem escopo a porta falha fechada com CompanyScopeUnavailableException, e um fluxo tenant-wide (job) não alcança empresa nenhuma. Os resultados vêm em OrganizationOutcome (isSuccess, errorCode, message já na cultura da requisição, value). Nenhuma assinatura expõe tipo do Core, e nenhuma porta publica evento.

Ordem das recusas de IEstablishmentEligibility (a primeira que falha vence): no eixo de recebimento, COMPANY_INACTIVE → ESTABLISHMENT_INACTIVE → COMPANY_CLOSED (documento com data posterior ao closedOn; no próprio dia ainda vale) → ESTABLISHMENT_RECEIVING_NOT_ALLOWED → ESTABLISHMENT_REGISTRATION_STATUS; depois REQUESTING_ESTABLISHMENT_COMPANY_MISMATCH; no eixo de quem pede, ESTABLISHMENT_INACTIVE → ESTABLISHMENT_PURCHASING_NOT_ALLOWED → ESTABLISHMENT_REGISTRATION_STATUS (todos _UNPROCESSABLE). A empresa vem antes do estabelecimento de propósito: a cascata inativa as filiais, e quem vê empresa inativa recebe COMPANY_INACTIVE. A recusa por situação cadastral (Suspended, Unfit, ClosedDown; Null e Active não bloqueiam) é a mesma Specification que o lookup usa para ocultar.

Emissão e pool. O emissor usa conexão e transação próprias, confirmadas antes da transação do documento (a lacuna é permitida): cada requisição que emite ocupa duas conexões do pool. A unicidade (tenantId, documentType, companyId, number) é do índice do documento; DocumentNumberViolation traduz a violação desse índice para DOCUMENT_NUMBER_ALREADY_EXISTS, e o consumidor informa o nome do índice dele. Quem chama as portas ao criar ou submeter documento é a Requisição (RN-EMP-05, 12, 13, 33, 34, 36, 37 e 39 estão provadas até a porta); RN-EMP-06, 09, 10 e 11 são regras do Centro de Custo, da Requisição e do Fluxo de Aprovação, sem teste nomeado aqui.

Portas publicadas para a espinha transacional (Nexio.Organization.Contracts)
PortaMétodoDevolveRecusas
ICompanyResolverCompanyOfAsync(establishmentId)OrganizationOutcome<Guid>: a empresa derivada no servidor (RN-EMP-08). Empresa ou estabelecimento inativo ainda resolvemESTABLISHMENT_NOT_FOUND
IEstablishmentEligibilityCheckAsync(establishmentId, requestingEstablishmentId, documentDate)EligibleDocumentEstablishments (companyId, establishmentId, requestingEstablishmentId). requestingEstablishmentId nunca é nulo: quando quem pede é quem recebe, é igual a establishmentIdESTABLISHMENT_NOT_FOUND e as da ordem abaixo
IFiscalSnapshotProviderSnapshotAsync(establishmentId)FiscalSnapshot (taxRegime, icmsTaxpayerStatus, stateRegistration, suframaCode) lido na hora (RN-EMP-37). Congelar no documento é ato da RequisiçãoESTABLISHMENT_NOT_FOUND
IGLAccountEligibilityCheckAsync(glAccountId, companyId) e CheckAtSubmissionAsync(glAccountId?, companyId)OrganizationOutcome (RN-EMP-12 e RN-EMP-13); conta nula na submissão é recusada, rascunho não chamaGL_ACCOUNT_NOT_FOUND, GL_ACCOUNT_POSTING_NOT_ALLOWED_UNPROCESSABLE, COMPANY_NOT_FOUND, GL_ACCOUNT_NOT_ENABLED_FOR_COMPANY_UNPROCESSABLE, ALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE
IGLAccountExternalCodeResolverResolveAsync(glAccountId, companyId)ExternalCodeResolutionDto (code, source, accountCode, companyCode), source ∈ Company, Account, Missing. Missing é sucesso com code nulo (“aguardando código externo”) (RN-EMP-39)GL_ACCOUNT_NOT_FOUND, COMPANY_NOT_FOUND
INumberIssuerIssueAsync(documentType, establishmentId)IssuedNumber (number, sequenceId, periodKey, value) (RN-EMP-30, 31, 32). Falha como NexioDomainExceptionESTABLISHMENT_NOT_FOUND, NUMBER_SEQUENCE_NOT_CONFIGURED_UNPROCESSABLE

Camada API

Endpoints

Prefixo api/v{version}/nexio. Todas exigem autenticação e declaram exatamente uma permissão; em POST /companies o caso de uso confere também update:company-scopes (RN-EMP-20). Toda listagem já vem recortada pelo escopo do usuário — não há parâmetro para pedir fora dele.

Empresa
RotaPermissãoRetornoErros
GET /companiesread:companiesPagedResult<CompanyDto>400
GET /companies/lookupread:companiesarray puro400
GET /companies/{id}read:companiesCompanyDetailDto404
POST /companiescreate:companiesCompanyDetailDto. Exige também update:company-scopes, conferida antes de gravar — sem ela, 403 COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN (RN-EMP-20). O corpo traz a matriz inline — headquarters com o CNPJ completo e os campos do estabelecimento — e empresa e Establishment Headquarters nascem na mesma transação, junto com a linha de escopo de quem criou (origin = CompanyCreation, RN-EMP-20). Sem headquarters é 400. É também a tela do onboarding de um tenant novo400 · 403 · 409 · 422
PUT /companies/{id}update:companiesCompanyDetailDto400 · 404 · 409
PATCH /companies/{id}/activateupdate:companiesCompanyDetailDto404 · 409
PATCH /companies/{id}/deactivateupdate:companiesCompanyDetailDto. Cascateia: desativa os estabelecimentos ativos da empresa na mesma transação, um evento por estabelecimento, e devolve a lista do que caiu junto. Documento em andamento conclui (RN-EMP-05)404 · 409
PATCH /companies/{id}/headquartersupdate:companiesCompanyDetailDto. Corpo { establishmentId, expectedVersion }: troca a matriz num ato só (RN-EMP-38)404 · 409 · 422
Estabelecimento
RotaPermissãoRetornoErros
GET /companies/{id}/establishmentsread:companiesarray puro404
GET /establishments/lookupread:companiesagrupado por empresa. purpose obrigatório na query: Receiving · Requesting400
GET /establishments/{id}read:companiesEstablishmentDetailDto404
POST /companies/{id}/establishmentscreate:companiesEstablishmentDetailDto. Nasce Branch; a situação cadastral é consultada na criação400 · 404 · 409 · 422
PUT /establishments/{id}update:companiesEstablishmentDetailDto. Alterar município, UF ou allowsReceiving revalida todos os pontos de entrega deste estabelecimento na mesma transação — a UF é recalculada só nos que herdam o endereço. O corpo aceita deliveryLocationInterstateNotes[], com deliveryLocationId e interstateNote, para os pontos que passam a ser interestaduais. A edição que deixaria algum ponto interestadual sem explicação, ou ponto ativo num estabelecimento que deixa de receber, é recusada com 422 ESTABLISHMENT_ADDRESS_CHANGE_BREAKS_DELIVERY_LOCATIONS_UNPROCESSABLE e a lista nomeada. Exige expectedVersion. Ver Local de Entrega400 · 404 · 409 · 422
PATCH /establishments/{id}/activateupdate:companiesEstablishmentDetailDto404 · 409
PATCH /establishments/{id}/deactivateupdate:companiesEstablishmentDetailDto404 · 409
PATCH /establishments/{id}/registration-statusupdate:companiesEstablishmentDetailDto. Correção manual da situação cadastral: { status, statusDate, evidenceNote, expectedVersion } (RN-EMP-36)404 · 409 · 422

GET /establishments/lookup é o endpoint que a interface inteira consome. Devolve só o que aquele usuário pode usar, já agrupado por empresa, já filtrado por isActive e pelo propósito do campo — purpose=Receiving, para o establishmentId, filtra allowsReceiving; purpose=Requesting, para o requestingEstablishmentId, filtra allowsPurchasing —, com defaultEstablishmentId marcado. Sem purpose é 400 ESTABLISHMENT_LOOKUP_PURPOSE_REQUIRED: um lookup que escolhe o filtro sozinho é o defeito que esta regra fecha. Um item na resposta significa que o formulário não mostra o campo — a decisão de exibir o seletor é derivada do tamanho desta lista, nunca de uma configuração que alguém precisa lembrar de ligar.

Plano de contas e numeração
RotaPermissãoRetornoErros
GET /gl-accountsread:gl-accountsPagedResult<GLAccountDto>400
GET /gl-accounts/lookupread:gl-accountscompanyId obrigatório na query. Devolve só as contas com allowsPosting, vigentes e habilitadas naquela empresa — linha ausente em GLAccountCompany ou isEnabled = true400 · 404
GET /gl-accounts/{id}/childrenread:gl-accountsPagedResult<GLAccountDto>400 · 404
POST /gl-accountscreate:gl-accountsGLAccountDetailDto400 · 409 · 422
PUT /gl-accounts/{id}update:gl-accountsGLAccountDetailDto400 · 404 · 409
PATCH /gl-accounts/{id}/parentupdate:gl-accountsGLAccountDetailDto404 · 409 · 422
PUT /gl-accounts/{id}/companies/{companyId}update:gl-accountshabilitação e código externo404 · 422
GET /number-sequencesread:number-sequencesarray puro — vigentes, agendadas e aposentadas, com o estado derivado400
POST /number-sequencescreate:number-sequencesNumberSequenceDto. Cria a configuração. Havendo vigente ou agendada para o documentType, exige replacesSequenceId e expectedVersion e aposenta a substituída na mesma transação (RN-EMP-28, RN-EMP-29)400 · 409 · 422
PUT /number-sequences/{id}update:number-sequencesNumberSequenceDto. Só padding, com expectedVersion. Os demais campos são a identidade da configuração — mudar é substituir404 · 409 · 422
Escopo por empresa e preferência
RotaPermissãoRetornoErros
GET /users/{userId}/company-scopesread:company-scopesUserCompanyScopeSetDto: scopeHash e as linhas, vigentes e encerradas, só das empresas que quem lê alcança400 · 404
PUT /users/{userId}/company-scopesupdate:company-scopesUserCompanyScopeSetDto. Corpo { expectedScopeHash, companies: [{ companyId, validTo? }], dependentGrantDecisions?: [{ scopeId, decision }] }: substitui o conjunto vigente dentro do escopo de quem concede; decision = Keep · End · Trim, uma por linha Restrict que o ato alcança (RN-EMP-42)400 · 404 · 409 · 422
GET /me/company-scopeautenticadoo conjunto resolvido do usuário atual, com reason—

PUT substitui o conjunto, não faz merge — e só dentro do escopo de quem concede. O servidor compara o corpo com as linhas vigentes daquela pessoa nas empresas que quem concede alcança: empresa que está no corpo e não tem linha vigente ganha linha nova; linha vigente cuja empresa saiu do corpo é encerrada no instante do ato; empresa que ficou com outro validTo tem a linha encerrada e reaberta no mesmo instante; o resto não é tocado. Linhas de empresas que quem concede não alcança não entram na comparação, não aparecem na resposta e não mudam (RN-EMP-19). Lista vazia encerra todo o acesso que quem concede alcança (RN-EMP-23). É a semântica da tela de Papéis do gryd — "salvar substitui" —, e é o que torna a tela uma lista de checkboxes honesta.

Cinco guardas antes de gravar. userId igual ao de quem concede é 403 USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN, com ou sem mudança no corpo (RN-EMP-41). expectedScopeHash tem de ser o hash das linhas vigentes que o GET devolveu a quem concede, conferido sob as travas da pessoa e de cada empresa tocada (RN-EMP-23) — senão 409 USER_COMPANY_SCOPE_CONCURRENT_CHANGE_CONFLICT, e nada é gravado. Linha aberta para outra pessoa não passa do validTo da linha de quem concede na mesma empresa — senão 422 USER_COMPANY_SCOPE_BEYOND_GRANTOR_UNPROCESSABLE (RN-EMP-42). Encerrar ou encurtar a linha de quem concedeu corta, na mesma transação e em cadeia, as linhas Cascade que ela concedeu, e exige dependentGrantDecisions cobrindo exatamente as Restrict que o novo fim alcança, cada linha uma vez — senão 409 USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT, com a lista (RN-EMP-42). E, sobre o resultado do ato inteiro, cortes incluídos: empresa — ativa ou inativa — que tinha linha vigente sem validTo de pessoa ativa com update:company-scopes continua tendo; nenhuma fica sem linha vigente de ninguém; e a que já não tinha não perde a última linha sem fim, de quem quer que seja que continue na organização — senão 409 USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT, listando as empresas (RN-EMP-24).

Implementado: as 29 rotas desta seção existem, com a mesma permissão por action, e estão conferidas uma a uma, com corpo, retorno e erros, em Contrato para o frontend. O texto acima fica; o que o código acrescenta ou muda:

  • GET /companies, GET /gl-accounts e GET /gl-accounts/{id}/children respondem PagedResult<T> (items, totalCount, pageNumber, pageSize, totalPages, hasNext, hasPrevious, scope). Os lookups, a lista de estabelecimentos da empresa e GET /number-sequences são array puro. includeDeleted=true é 400 LIST_INCLUDE_DELETED_UNSUPPORTED.
  • GET /number-sequences nunca responde 400. POST /number-sequences responde também 404 NUMBER_SEQUENCE_NOT_FOUND (replacesSequenceId inexistente). PUT /gl-accounts/{id}/companies/{companyId} responde também 409 por versão vencida, e a versão é a da conta. GET /users/{userId}/company-scopes nunca responde 404: pessoa sem linha recebe 200 com conjunto vazio.
  • PATCH /companies/{id}/activate e PUT /users/{userId}/company-scopes exigem só a permissão da própria rota: o diretório do GrydAuth embutido é lido em processo, e read:users deixou de ser pedida do chamador (DEC-CO-33-3). GET /establishments/lookup oculta também as situações cadastrais que bloqueiam documento novo.
  • POST /companies/{companyId}/establishments não leva expectedVersion, mas sobe a versão da empresa: dois POST simultâneos podem dar 409 COMPANY_CONCURRENT_CHANGE_CONFLICT, que se resolve repetindo. PUT /companies/{id} ignora code, cnpjRoot e functionalCurrency.
  • PUT /establishments/{id} aceita deliveryLocationInterstateNotes[] e chama a cadeia de revalidação dos pontos de entrega na mesma transação, mas o registro da cadeia está vazio: a edição nunca é recusada por esse motivo até o contexto de pontos de entrega registrar o seu tratador.
  • Rotas que não existem: GET /gl-accounts/{id}, ativar e desativar conta contábil, leitura das habilitações por empresa, GET /number-sequences/{id}. A emissão de número não é rota: é a porta INumberIssuer.

Contrato

Contrato de erros

O status HTTP é derivado do sufixo do código, nunca da mensagem: o mapeador do Core reconhece 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 canônico exato vence o sufixo. Conferido no código da plataforma em 10/09/2026 (ErrorCodeSuffixes, commit e0eab4e5).

CódigoHTTPQuando
COMPANY_NOT_FOUND404Não existe neste tenant — ou não está no escopo do usuário. Os dois casos respondem igual, de propósito. Vale também para empresa, no corpo do PUT de escopo, fora do escopo de quem concede
COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN403POST /companies de quem tem create:companies mas não update:company-scopes (RN-EMP-20)
COMPANY_CODE_ALREADY_EXISTS409Código já usado no tenant. A mensagem carrega o código
COMPANY_CNPJ_ROOT_ALREADY_EXISTS409Já existe empresa com essa raiz. Duas empresas com a mesma raiz seriam a mesma pessoa jurídica
COMPANY_CNPJ_ROOT_INVALID400Raiz que, normalizada, não tem 8 caracteres em 0–9A–Z
COMPANY_CONCURRENT_CHANGE_CONFLICT409expectedVersion divergente na empresa
COMPANY_CLOSED_UNPROCESSABLE422Documento novo com data posterior ao closedOn da empresa
COMPANY_INACTIVE_UNPROCESSABLE422Criação ou submissão de documento novo em estabelecimento de empresa inativa. Documento já submetido não recebe este código: conclui. Leitura nunca o devolve
ESTABLISHMENT_INACTIVE_UNPROCESSABLE422Criação ou submissão de documento novo em estabelecimento inativo de empresa ativa. Mesma regra: documento já submetido conclui
ESTABLISHMENT_RECEIVING_NOT_ALLOWED_UNPROCESSABLE422Estabelecimento com allowsReceiving = false como establishmentId de documento novo
ESTABLISHMENT_PURCHASING_NOT_ALLOWED_UNPROCESSABLE422Estabelecimento com allowsPurchasing = false como requestingEstablishmentId de documento novo
REQUESTING_ESTABLISHMENT_COMPANY_MISMATCH_UNPROCESSABLE422requestingEstablishmentId de outra empresa que não a do establishmentId. Sem rateio nem pedido entre empresas na v1
COMPANY_FUNCTIONAL_CURRENCY_UNPROCESSABLE422functionalCurrency diferente de BRL na criação da empresa
ESTABLISHMENT_NOT_FOUND404Idem — fora de escopo responde como inexistente
ESTABLISHMENT_CNPJ_ALREADY_EXISTS409CNPJ de 14 repetido no tenant
ESTABLISHMENT_CODE_ALREADY_EXISTS409Código repetido dentro da mesma empresa
ESTABLISHMENT_CNPJ_ROOT_MISMATCH_UNPROCESSABLE422O CNPJ não pertence à raiz da empresa. O dado é bem formado e semanticamente impossível — por isso 422 e não 400
ESTABLISHMENT_CNPJ_INVALID400CNPJ que, normalizado, não tem 14 caracteres no formato, ou com DV inválido — também no headquarters inline do POST /companies
ESTABLISHMENT_CONCURRENT_CHANGE_CONFLICT409expectedVersion divergente no estabelecimento
ESTABLISHMENT_LOOKUP_PURPOSE_REQUIRED400GET /establishments/lookup sem purpose
ESTABLISHMENT_ADDRESS_CHANGE_BREAKS_DELIVERY_LOCATIONS_UNPROCESSABLE422PUT /establishments/{id} que muda município, UF ou allowsReceiving e deixaria algum ponto de entrega pendurado no estabelecimento — com ou sem endereço próprio — interestadual sem explicação (sem nota em deliveryLocationInterstateNotes[] no mesmo PUT, fora de canteiro e sem recebedor distinto; ou canteiro sem centro de custo), ou ativo e apontando para estabelecimento que deixou de receber. A mensagem lista os pontos pelo código e pelo nome (RN-DL-19, RN-EMP-17)
ESTABLISHMENT_REGISTRATION_STATUS_UNPROCESSABLE422Documento novo com estabelecimento — em qualquer dos dois eixos — em situação Suspended, Unfit ou ClosedDown. A mensagem carrega a situação e a data dela
ESTABLISHMENT_HEADQUARTERS_REQUIRED_UNPROCESSABLE422Guarda do agregado: operação que deixaria a empresa sem matriz. A troca de matriz é só pelo PATCH /companies/{id}/headquarters
ESTABLISHMENT_LAST_ACTIVE_CONFLICT409Desativação do último estabelecimento ativo de uma empresa ativa, pela rota do estabelecimento. Não se aplica à cascata de /companies/{id}/deactivate, que desativa as duas coisas na mesma transação
ESTABLISHMENT_STATE_REGISTRATION_REQUIRED_UNPROCESSABLE422icmsTaxpayerStatus = Taxpayer sem inscrição estadual
ESTABLISHMENT_STATE_REGISTRATION_NOT_ALLOWED_UNPROCESSABLE422icmsTaxpayerStatus = Exempt com inscrição estadual
GL_ACCOUNT_NOT_FOUND404—
GL_ACCOUNT_CODE_ALREADY_EXISTS409—
GL_ACCOUNT_POSTING_NOT_ALLOWED_UNPROCESSABLE422Fatia de rateio apontando para conta que não aceita lançamento
GL_ACCOUNT_NOT_ENABLED_FOR_COMPANY_UNPROCESSABLE422Fatia de rateio apontando para conta desabilitada em GLAccountCompany na empresa do documento. Verificado na fatia e na submissão; a mensagem carrega o código da conta e o da empresa
GL_ACCOUNT_TYPE_MISMATCH_UNPROCESSABLE422accountType divergente do pai
GL_ACCOUNT_CONCURRENT_CHANGE_CONFLICT409expectedVersion divergente — ou, num move, o path mudou desde a leitura
USER_COMPANY_SCOPE_NOT_FOUND404Linha inexistente, ou de um tenant que não é o do token
USER_COMPANY_SCOPE_DUPLICATE_CONFLICT409A mesma empresa duas vezes no corpo, ou vigência que se sobreporia a outra linha da mesma pessoa e empresa
USER_COMPANY_SCOPE_CONCURRENT_CHANGE_CONFLICT409expectedScopeHash diferente do hash das linhas vigentes que quem concede alcança. Nada é gravado; a tela recarrega e mostra o conjunto atual
USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT409O ato deixaria uma empresa, ativa ou inativa, sem linha vigente sem validTo de pessoa ativa que tem update:company-scopes — no PUT de escopo, quando ela a tinha antes, inclusive pelo corte em cadeia; no PATCH /companies/{id}/activate, quando ela não a tem —, sem nenhuma linha vigente, de ninguém, ou, já sem a invariante, sem a última linha sem fim, de quem quer que seja que continue na organização. A mensagem lista as empresas. Conceda antes linha sem fim a outra pessoa com a permissão (RN-EMP-24)
USER_COMPANY_SCOPE_VALIDITY_UNPROCESSABLE422validTo que não é posterior ao instante da concessão
USER_COMPANY_SCOPE_BEYOND_GRANTOR_UNPROCESSABLE422Linha concedida sem validTo, ou com validTo posterior, na empresa em que a linha de quem concede tem fim (RN-EMP-42). A mensagem nomeia a empresa
USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT409O PUT encerra ou encurta linha de quem concedeu linhas Restrict que o novo fim alcança, em cadeia, e dependentGrantDecisions falta ou não cobre exatamente essas linhas. Nada é gravado; a resposta lista cada linha (pessoa, empresa, validTo) para a decisão Keep, End ou Trim; linha já alcançada pelo ato não entra de novo (RN-EMP-42)
USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN403PUT /users/{userId}/company-scopes com o userId de quem concede
COMPANY_SCOPE_SUPPORT_GRANT_BENEFICIARY_UNPROCESSABLE422Ato de suporte da plataforma: beneficiário sem identidade verificada, ou com o CPF de quem pede o ato havendo outra pessoa ativa com update:company-scopes — a conta é por CPF verificado, não por usuário. Conferido no pedido e de novo no fim da carência (RN-EMP-24). Regra da demanda de plataforma; esta página cita
USER_NOT_IN_TENANT_UNPROCESSABLE422O userId não tem vínculo ativo com esta organização no GrydAuth. Verificado só na escrita. O PUT de escopo com lista vazia é aceito sobre quem saiu da organização: é o ato que trata a remoção (RN-EMP-24)
NUMBER_SEQUENCE_REPLACEMENT_UNPROCESSABLE422Substituição sem prefixo novo nem período novo — inclusive período que não passa do maior já emitido por configuração com o mesmo prefixo e a mesma resetPolicy (RN-EMP-29); effectiveFrom no passado; ou effectiveFrom futuro que não cai no início de um período posterior, no fuso e com a resetPolicy da substituída
NUMBER_SEQUENCE_PREFIX_OVERLAP_UNPROCESSABLE422Prefixo diferente do de uma configuração já criada para o documentType que começa com ele ou é começo dele (RN-EMP-29). A mensagem nomeia a configuração anterior
NUMBER_SEQUENCE_PADDING_UNPROCESSABLE422padding fora de 1 a 21, na criação ou no PUT
NUMBER_SEQUENCE_TIME_ZONE_UNPROCESSABLE422timeZone que não é identificador IANA conhecido
NUMBER_SEQUENCE_CONCURRENT_CHANGE_CONFLICT409expectedVersion divergente, ou replacesSequenceId que não é a configuração mais recente do documentType
NUMBER_SEQUENCE_ALREADY_EXISTS409Já existe configuração vigente ou agendada para o documentType, e o corpo não traz replacesSequenceId
DOCUMENT_NUMBER_ALREADY_EXISTS409O número composto já foi emitido na empresa para o mesmo tipo de documento — a guarda final da RN-EMP-32. Nada é gravado, o número consumido não volta, e repetir o comando emite o próximo
COMPANY_EXTERNAL_CODE_ALREADY_EXISTS409Implementado. externalCode de empresa repetido no tenant, inclusive de linha apagada logicamente (DEC-CO-07-B)
ESTABLISHMENT_EXTERNAL_CODE_ALREADY_EXISTS409Implementado. externalCode de estabelecimento repetido (DEC-CO-07-B)
ALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE422Implementado. Fatia de rateio sem conta na submissão (RN-EMP-13); rascunho não chama. Sai só pela porta IGLAccountEligibility.CheckAtSubmissionAsync (DEC-CO-19-3)
GL_ACCOUNT_PARENT_CYCLE_UNPROCESSABLE422Implementado. Mover a conta para baixo de si mesma ou de um descendente (só ocorre no move; DEC-CO-08-1)
GL_ACCOUNT_SIBLING_NAME_ALREADY_EXISTS409Implementado. Nome já usado entre irmãos (comparação exata, depois de Trim), na criação, na revisão e no move (DEC-CO-08-1)
LIST_INCLUDE_DELETED_UNSUPPORTED400Implementado. includeDeleted=true em qualquer listagem: a exclusão lógica é global e não há como listar apagados
NUMBER_SEQUENCE_NOT_FOUND404Implementado. Configuração inexistente ou de outro tenant: replacesSequenceId do POST ou id do PUT (DEC-CO-09-1, DEC-CO-14-1)
NUMBER_SEQUENCE_NOT_CONFIGURED_UNPROCESSABLE422Implementado. Emissão de número sem configuração vigente para o tipo de documento. Sai só pela porta INumberIssuer (DEC-CO-09-1)

Implementado: o catálogo OrganizationErrorCodes tem 62 códigos: os 49 da tabela original, 8 pedidos por itens, acrescentados acima e marcados, e os 5 da consulta de CNPJ, CEP e município (REGISTRY_CNPJ_INVALID, REGISTRY_ZIP_CODE_INVALID, REGISTRY_CNPJ_NOT_FOUND, REGISTRY_ZIP_CODE_NOT_FOUND, REGISTRY_LOOKUP_UNAVAILABLE). USER_DIRECTORY_UNAVAILABLE (503) existiu até a v1.20 e não existe mais: o diretório é lido em processo (DEC-EMB-6), e uma falha de leitura da identidade é erro de plataforma, 500. O status sai do sufixo (e o catálogo nunca digita o sufixo como texto, só a constante); a mensagem existe em en-US, pt-BR e es-ES e vem na cultura da requisição. Fora do catálogo ficam os erros da plataforma: VALIDATION_ERROR (400, com os erros por campo, inclusive errors.sortBy para ordenação fora do mapa), 401, 403 de permissão e tenant, e 500 de falha fechada. Em toda resposta de erro o ProblemDetails traz code e traceId.

Quem produz. Um código não tem produtor, de propósito: COMPANY_SCOPE_SUPPORT_GRANT_BENEFICIARY_UNPROCESSABLE (ato de suporte da plataforma, fora da entrega). Os códigos de elegibilidade de documento — COMPANY_CLOSED, COMPANY_INACTIVE, ESTABLISHMENT_RECEIVING_NOT_ALLOWED, ESTABLISHMENT_PURCHASING_NOT_ALLOWED, REQUESTING_ESTABLISHMENT_COMPANY_MISMATCH, ESTABLISHMENT_REGISTRATION_STATUS e GL_ACCOUNT_NOT_ENABLED_FOR_COMPANY (todos _UNPROCESSABLE) — e DOCUMENT_NUMBER_ALREADY_EXISTS são devolvidos pelas portas publicadas, não por rota HTTP; a rota de troca de matriz devolve ESTABLISHMENT_INACTIVE_UNPROCESSABLE por si. O código de pontos de entrega existe e é produzido pela cadeia, mas com o registro vazio nunca dispara.

Os erros que listam itens (USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT, USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT, ESTABLISHMENT_ADDRESS_CHANGE_BREAKS_DELIVERY_LOCATIONS_UNPROCESSABLE) trazem a lista no texto da mensagem. O código por rota está em Contrato para o frontend.

Autorização

Permissões

PermissãoAlcanceObservação
read:companiesLer empresas e estabelecimentosRecortada pelo escopo do usuário. Quase todo papel operacional precisa dela para o seletor funcionar
create:companiesCriar empresa e estabelecimentoAdministrativa. Criar empresa muda a forma da interface para todo mundo do tenant. Criar empresa exige também update:company-scopes (RN-EMP-20): o papel que cria empresa tem as duas
update:companiesEditar, ativar e desativarAdministrativa
read:gl-accountsLer o plano de contasNecessária para preencher a fatia de rateio
create:gl-accountsCriar contaControladoria
update:gl-accountsEditar, mover, habilitar por empresaControladoria
read:company-scopesLer o escopo por empresa dos usuáriosAdministrativa. Não é necessária para operar — GET /me/company-scope não exige permissão
update:company-scopesDefinir quais empresas cada pessoa alcança — só entre as empresas que quem concede alcançaAdministrativa e sensível: é o que separa as empresas do grupo umas das outras. Todo uso é ato de controle, com evento e registro (RN-EMP-21)
read:number-sequences, create:number-sequences e update:number-sequencesCriar e configurar numeraçãoAdministrativa. Mudar a numeração com documento emitido é permitido por substituição — a anterior é aposentada, a nova exige prefixo ou período novo, e nada é renumerado

Escopo não é permissão — é o alcance dela. A permissão, que vem do gryd, diz o que a pessoa pode fazer; o UserCompanyScope, que é do Nexio, diz onde. Confundir os dois leva a criar read:companies:alfa no catálogo do tenant, que é a forma mais rápida de tornar um catálogo de papéis impossível de manter — e a razão de o recorte por empresa não ter virado permissão.

Implementado: são exatamente estas permissões (NexioPermissions = arquivo de importação docs/operations/nexio-permissions.json = o que as actions exigem). O Nexio não escreve as linhas de permissão: o GrydAuth embutido não tem registrador de catálogo do host (o catálogo dele é estático e a Permission é por tenant). O passo operacional é importar o arquivo em cada tenant pela rota do próprio host, POST /api/v1/permissions/import?mode=Catalog (ou /permissions/import/file), que exige create:permissions, depois de todo deploy que mude o catálogo.

  • As rotas de estabelecimento usam as permissões de empresa (read:companies, create:companies, update:companies); as de plano de contas, as três de gl-accounts.
  • numeração tem uma permissão por verbo: read:number-sequences abre a leitura, create:number-sequences o POST e update:number-sequences o PUT. Nenhuma implica outra.
  • GET /me/company-scope não exige permissão — só autenticação.
  • Uma exigência fora do catálogo que o operador precisa conceder: update:company-scopes no token de quem cria empresa, além de create:companies (senão 403 COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN e nada é gravado). read:users não é mais exigida de quem ativa empresa ou escreve escopo: o diretório é lido em processo. As permissões implícitas do GrydAuth (por exemplo read:users implicando read:user-email) não são expandidas pelo Nexio.

Conformidade

Estado da implementação

O backend de Empresa, Estabelecimento, Plano de contas, Numeração e Escopo por empresa está entregue e conferido contra o código; o que falta depende da plataforma, de outra especificação ou de um provedor externo, e está dito linha a linha abaixo. O contexto é o Nexio.Organization (src/contexts/Nexio.Organization.*; controllers em src/hosts/Nexio.Api/Controllers/Organization; persistência em src/shared/Nexio.Persistence), na branch de integração feature/organization-embedded-auth, sobre o Gryd.IO 10.0.0, com o GrydAuth embutido: o Nexio.Api é o emissor de identidade (emite e valida os próprios tokens, com banco próprio nexio-identity), e o Worker, o Portal e o Migrator leem a identidade em processo — o Nexio não consome a autenticação de outro serviço (DEC-EMB-0). A tabela regra → teste está em docs/definitions/development/company/conformidade.md: 901 métodos Rn_emp_*, os 846 da conferência de 4a5b0be mais os que a CO-19 (guardas), a CO-21 (envelope das listagens e outbox) e os lotes 8 e 9 (identidade embutida, CO-30 a CO-35) acrescentaram; a suíte da solução está verde na entrega do PR (o HEAD conferido e os totais ficam no corpo do PR); o contrato HTTP exato está em api-empresa.md, na mesma pasta. Não há tela: o backend sai antes do frontend, e o que a tela consome está em Contrato para o frontend. Os outros contextos do repositório (Approval, Budget, Catalog, Procurement, Reference e Sourcing) ainda são esqueletos: CostCenter, DeliveryLocation, Requisition e os demais documentos não existem.

Código × spec, item a item
ItemNo códigoSituaçãoO que bloqueia
Autenticação e identidade (DEC-EMB-0)O Nexio.Api é o emissor: AddGrydAuth e AddGrydAuthApiCookieSecurity, todas as rotas do GrydAuth.API no host (DEC-EMB-3), UseGrydAuth (CORS único em GrydAuth:Cors:AllowedOrigins, limite de taxa e sessão de tenant) e a guarda do X-Tenant-ID logo depois. Banco próprio nexio-identity (ConnectionStrings:DefaultPostgresConnection, DEC-EMB-2), migrado pelo Migrator junto dos outros três. Worker e Portal leem a identidade em processo (AddNexioIdentityReaders), sem validar token e sem JwtSettings. O modelo do NexioDbContext não ganha entidade do Auth, e a visibilidade de linhas sem tenant segue CurrentTenantOnly (nenhuma entidade do módulo é opcional por tenant; há teste no host composto)ConformeOperacional: o admin semeado do módulo (admin@local.com) existe em todo ambiente; trocar a senha ou desativá-lo e criar o administrador real é pré-requisito do corte em produção (runbook em docs/operations/backend.md). Chaves, segredos e SMTP por ambiente: docs/operations/configuracao-identidade.md
Entidades e migrationsCompany, Establishment (a matriz é uma linha de establishments), GLAccount, GLAccountCompany, NumberSequence, NumberSequenceCounter, UserCompanyScope e a tabela de referência municipalities (5.571 linhas do IBGE, semeadas pela migration), mais as três tabelas do outbox (outbox, outbox_payload, outbox_delivery_state): 11 tabelas no esquema nexio, em três migrations — Organization_Lote1_ReferenciaMunicipios (23/10), Organization_Lote2_Agregados (24/10) e Organization_Lote6_Outbox (25/10). CostCenter e DeliveryLocation não existem (specs próprias). Nenhuma migration toca tabela da plataforma (RN-EMP-16, com teste de arquitetura)Conforme—. Operacional: o usuário do Migrator precisa poder criar a extensão btree_gist
Campos e derivadoscnpjRoot por CHECK (cnpj_root = left(cnpj, 8)) mais chave estrangeira composta (tenant_id, company_id, cnpj_root); headquartersId não nulo; establishmentCount; type e UF derivados (a UF sai sempre do código IBGE do município, nunca é digitada); version int como token de concorrência em empresa, estabelecimento, conta e configuração de numeração. code, cnpjRoot, cnpj e externalCode ficam reservados para sempre, mesmo com a linha apagada logicamente (D-12)Conforme (o CHECK é acréscimo; a reserva perpétua é decisão do mantenedor)—
Matriz única (RN-EMP-02)Restrição de exclusão adiada (ex_establishments_single_headquarters, DEFERRABLE INITIALLY DEFERRED), no lugar do índice único filtrado: a troca de matriz atualiza duas linhasConforme — a spec chama o índice de exemplo de implementação (DEC-CO-07-2, ratificada em DEC-CO-07-C)—
PermissõesNove permissões em NexioPermissions = arquivo de importação nexio-permissions.json = o que as actions exigem (PermissionCatalogTests). O Nexio não escreve as linhas de permissão: o GrydAuth não tem registrador de catálogo do host (o catálogo dele é estático e a Permission é por tenant), então vale o arquivo de importação (DEC-CO-03-1, mantida pela DEC-EMB-9)Conforme; passo operacional por tenantImportar nexio-permissions.json em cada tenant pela rota do próprio host (POST /api/v1/permissions/import?mode=Catalog, que exige create:permissions), depois de todo deploy que mude o catálogo
Rotas32 rotas em sete controllers (api/v1/nexio/...), uma permissão por action; GET /me/company-scope não exige permissão. Tabela completa em Contrato para o frontendConforme à tabela de endpoints, com as divergências decididas listadas abaixo—
Envelope das listagensPagedResult<T> — items, totalCount, pageNumber, pageSize, totalPages, hasNext, hasPrevious, scope (sempre nulo) — desde a CO-21 e o Gryd.IO 10.0.0 (DEC-V10-A). sortBy fora do mapa responde 400 VALIDATION_ERROR com errors.sortBy; o desempate por Id é do próprio QuerySortMap. A resposta não ecoa sortBy nem sortDirectionConforme (antes da CO-21 era { data, meta }; DEC-CO-13-A fechada)—
Contrato de erros62 códigos em OrganizationErrorCodes (49 da spec, 8 pedidos por itens e 5 da consulta de cadastro; USER_DIRECTORY_UNAVAILABLE saiu, DEC-EMB-6), status pelo sufixo, mensagem em en-US, pt-BR e es-ES pela cultura da requisição. Os códigos de elegibilidade de documento, de numeração na emissão e ALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE só saem pelas portas publicadas, não por rota HTTPConforme; um código sem produtor, de propósito: COMPANY_SCOPE_SUPPORT_GRANT_BENEFICIARY_UNPROCESSABLE (ato de suporte da plataforma, fora da entrega)—
Escopo por empresa — resoluçãoTabela de decisão com as sete linhas, falha fechada (500, nunca um conjunto mais amplo), reason, filtro global por ICompanyScoped/IEstablishmentScoped com registro de entidades que quebra o build (teste de arquitetura), escrita que resolve exatamente uma empresa, “escopo completo” calculado. A leitura é feita a cada requisição pelas linhas de UserCompanyScope; o token não traz empresaConforme, inclusive a linha 5 da tabela de resolução (linha anterior ao início do vínculo vigente): o início do vínculo é a criação da linha UserTenant não apagada no GrydAuth embutido, lida em processo; readmitir cria linha nova, e renovar o período da mesma linha não move o início (DEC-CO-33-1). Sem vínculo, o início é desconhecido e só a linha 5 deixa de se aplicar—
Escrita de escopoPUT que substitui, cinco guardas na ordem, travas pg_advisory_xact_lock por pessoa e depois por empresa (ordem crescente), lock_timeout de 10 s, hash conferido sob as travas, cascata Cascade/Restrict em cadeia, ataque da RN-EMP-23 provado com duas conexões sobre o ato real. Não exige read:users: o diretório do GrydAuth é lido em processo, sem o bearer do chamador (DEC-CO-33-3)Conforme—
Invariante do administrador permanente (RN-EMP-24)Uma só Specification (PermanentAdministratorSpecification), reaproveitada pela rota de escopo, pela ativação de empresa, pela rotina diária e pelo encerramento; teste de arquitetura impede reescrevê-la em outro lugar. Vale para empresa ativa e inativaConforme—
Rotina diária e cadeia de avisosJob por tenant organization.company-administrator-watch (06:00 UTC), cadeia de três degraus — quem tem linha vigente e update:company-scopes na empresa → administradores do tenant (membros elegíveis com o papel Admin do GrydAuth; GroupAdmin não conta) → canal de suporte, que só recebe o aviso —, lida em processo no GrydAuth embutido, com o tenant da rodada e sem credencial; OperationId determinístico por aviso, destaque “terminam em até N dias” (7 por padrão), lista das Restrict pendentes, modelos do GrydNotifications copiados por tenant em três culturas (NoticeTemplateCatalog.Version 2)Conforme — cada degrau é provado contra o Postgres real sobre o Worker de produção, e a rotina não falha quando o degrau 1 está vazio e há administrador no degrau 2. Falha de leitura da identidade interrompe a rotina daquele tenant (nada é avisado naquele dia) e ela repete no dia seguinte (DEC-CO-34-3)Operacional: SMTP configurado no Worker (sem GrydNotifications:Email:SmtpHost o envio cai no NoOpEmailSender, que descarta, e o aviso não chega a ninguém) e Platform:SupportChannel:Address preenchido (vazio, o degrau 3 falha alto). O módulo não tem a cultura do destinatário: Organization:Notices:Culture, padrão pt-BR
Remoção da organização (RN-EMP-24)Três caminhos sobre regras comuns: (1) PUT /users/{userId}/company-scopes com lista vazia, ato de quem concede (cause = Act); (2) EndScopeOfRemovedUserCommand, ato de sistema acionado pela remoção do vínculo no GrydAuth embutido: o módulo publica UserRemovedFromTenantEvent antes do commit do Auth, o ouvinte só anota, o commit libera o sinal e um consumidor em segundo plano (só no Api; fila em memória, até três repetições) encerra o escopo no tenant do evento — exceção do Nexio nunca desfaz a remoção no Auth; (3) varredura organization.scope.removed-users-sweep, uma vez ao dia (03:30 UTC), ato de sistema, que é a rede para o sinal perdido (DEC-EMB-5). Os caminhos 2 e 3 deixam endedByUserId nulo e cause = UserRemovedFromTenantConforme — remover o usuário no GrydAuthDbContext real encerra as linhas, a linha concedida na janela e o administrador fantasma da readmissão são provados com o adaptador real, e rollback no Auth não libera nada (DEC-CO-35-1, 35-3). Limite assumido: a fila é de 256 sinais e a parada do host com sinais na fila os deixa para a varredura—. O módulo não oferece gancho pós-commit; o DbTransactionInterceptor do Nexio faz essa ponte (pedido à plataforma)
NumeraçãoConfiguração imutável, substituição por Specification (NumberSequenceReplacement), contador por partição criado na primeira emissão (UPDATE ... RETURNING mais INSERT ... ON CONFLICT DO NOTHING), emissão atômica em conexão e transação próprias, lacuna permitida, exemplos 1 e 2 da spec como teste. INumberIssuer está registrado nos quatro hosts, com o adaptador de IEstablishmentNumberingSource; DocumentNumberViolation traduz a violação do índice do documento para DOCUMENT_NUMBER_ALREADY_EXISTSConforme. Cada requisição que emite ocupa duas conexões do pool—. O índice único (tenantId, documentType, companyId, number) é do documento, que ainda não existe (Requisição)
Plano de contasÁrvore por caminho materializado, tipo herdado do pai, allowsPosting só em folha, cascata assimétrica, move com guarda de path, habilitação por empresa, ordem do código externo (RN-EMP-39). A escolha de conta à mão (GLAccountChoice) é exposta como IGLAccountEligibilityConforme à spec; três rotas que a tela vai querer não foram pedidas — ver Lacunas—
Situação cadastral (RN-EMP-36)Correção manual (PATCH /establishments/{id}/registration-status), consulta na criação, varredura organization.establishment-registration-sweep (04:30 UTC, do mais desatualizado ao teto da janela), vale a data de situação mais recente, e a Specification RegistrationAllowsDocuments, que o lookup e a elegibilidade compartilhamParcial — sem provedor concreto de consulta ao CNPJ: UnavailableCnpjRegistryProvider devolve Null (não conclusivo), então a varredura só renova registration_checked_at e a situação só muda por correção manual (D-10)Provedor concreto de consulta ao CNPJ (fora desta entrega)
MunicípioIMunicipalityCatalog sobre a tabela municipalities; município IBGE desconhecido é VALIDATION_ERROR no campo cityIbgeCode (mensagem em inglês); a UF é derivadaConforme—
Pontos de entrega (RN-EMP-17)Porta IEstablishmentAddressChangeHandler com registro vazio, chamada na mesma transação do PUT /establishments/{id}; o corpo já aceita deliveryLocationInterstateNotes[]Parcial — a porta não tem alvo: hoje a edição de município, UF ou allowsReceiving nunca é recusada por pontos de entrega, e ESTABLISHMENT_ADDRESS_CHANGE_BREAKS_DELIVERY_LOCATIONS_UNPROCESSABLE, embora produzido pela cadeia, nunca dispara com o registro vazio (D-08)DeliveryLocation (spec própria) registrar o seu tratador
Eventos e outboxSeis eventos de integração pela porta IIntegrationEventPublisher, adaptador TransactionalOutbox do Gryd.IO 10.0.0: o evento é enfileirado na mesma transação do fato (nexio.outbox) e a publicação é sempre antes do último SaveChanges do ato; rollback remove fato e evento. A guarda (tenant, aggregateId, sequence) vira 409 de alteração concorrenteParcial — nenhum consumidor e nenhum worker: as linhas ficam Pending, nada é entregue. Pendência de comportamento: a documentação do Gryd.IO 10.0.0 não garante o que acontece com o backlog gravado antes de existir consumidor quando o worker for ligado; a pergunta ao time do Gryd.IO não tem resposta. Até lá, não ligar o worker sem consumidor e monitorar o crescimento de nexio.outbox (D-06)Primeiro consumidor (entra em AddNexioOutboxDrain e o Worker ganha AddOutboxWorker) e a resposta do Gryd.IO sobre backlog, replay e descarte
Trilha de auditoria (RN-EMP-21)A própria linha de UserCompanyScope (não se apaga nem se reescreve) os eventos granted/revoked e o registro no GrydAudit, todos na transação do ato. Nenhuma tabela de auditoria no Nexio (D-07)Atendido — IAuditTrail.Stage, Gryd.IO 11.1.0; o registro fica legível depois da entrega pelo Worker—
Portas para a espinha transacional (CO-19)Em Nexio.Organization.Contracts: ICompanyResolver, IEstablishmentEligibility, IFiscalSnapshotProvider, IGLAccountEligibility (CheckAsync e CheckAtSubmissionAsync), IGLAccountExternalCodeResolver e INumberIssuer, registradas nos quatro hosts. Resultado em OrganizationOutcome; escopo de empresa aplicado (fora do conjunto responde ESTABLISHMENT_NOT_FOUND; sem escopo no fluxo, falha fechada com CompanyScopeUnavailableException). Nenhuma assinatura expõe tipo do Core (PublishedPortsTests)Conforme — provadas até a porta, em unidade e contra o PostgresQuem as chama: Requisição, Pedido, Recebimento e Nota (specs próprias). RN-EMP-06, 09, 10 e 11 seguem sem teste nomeado porque a regra é do Centro de Custo, da Requisição e do Fluxo de Aprovação
Escopo completo (RN-EMP-27)FullScopeService.HasCompleteScopeAsync existe e é provado, mas não tem chamador: nenhuma rota lista as pessoas que alcançavam todas as empresas antes de uma criaçãoParcial — cálculo pronto, sem superfícieRota ou tela de criação de empresa que use o cálculo (a spec pede a lista na tela de criação)
Onboarding e migraçãoTenant sem empresa registrada responde reason = Onboarding e conjunto vazio (uma empresa apagada logicamente não encerra o onboarding; DEC-CO-10-A); não há carga implícita de escopoConforme; a carga inicial de escopo é ato de suporte da plataforma, fora da API de negócio—

Situação: Conforme — o código faz o que a spec diz, com teste. Parcial — falta uma dependência fora do contexto (plataforma, provedor, outra spec); o que existe está provado. Divergente — o código faz diferente da letra da spec por decisão registrada; nenhuma linha acima está Divergente sem decisão, e as decisões estão na tabela seguinte.

Divergências da letra da spec, decididas pelo mantenedor
OndeA spec diziaO código fazDecisão
RN-EMP-02índice único filtrado da matriz por empresarestrição de exclusão adiada (DEFERRABLE INITIALLY DEFERRED)a troca de matriz atualiza duas linhas; a spec chama o índice de “exemplo de implementação” (DEC-CO-07-2, DEC-CO-07-C)
RN-EMP-03chave estrangeira compostaa chave composta mais CHECK (cnpj_root = left(cnpj, 8))garante que a raiz derivada é a do próprio CNPJ (validação do lote 2)
RN-EMP-04 · cascatareativar a empresa e as filiaisreativar a empresa não reativa filiais; empresa ativa pode ficar sem estabelecimento ativo até alguém reabrir umaa spec: “quais filiais voltam é decisão de quem reativa” (DEC-CO-07-4, DEC-CO-07-C)
Cascata de desativação“um evento por estabelecimento” (sem nome)establishment.deactivated v1, um por estabelecimento que caiu, e também na desativação individualdecisão do mantenedor (DEC-CO-07-A)
externalCode repetidosem códigoCOMPANY_EXTERNAL_CODE_ALREADY_EXISTS e ESTABLISHMENT_EXTERNAL_CODE_ALREADY_EXISTS (409)DEC-CO-07-B
Plano de contasciclo e nome repetido sem códigoGL_ACCOUNT_PARENT_CYCLE_UNPROCESSABLE (422) e GL_ACCOUNT_SIBLING_NAME_ALREADY_EXISTS (409)DEC-CO-08-1
Numeraçãosem código para “sem configuração vigente”NUMBER_SEQUENCE_NOT_FOUND (404) e NUMBER_SEQUENCE_NOT_CONFIGURED_UNPROCESSABLE (422)DEC-CO-09-1
POST /number-sequences400 · 409 · 422também 404 NUMBER_SEQUENCE_NOT_FOUND quando replacesSequenceId não existeDEC-CO-14-1
GET /number-sequenceslinha com 400nunca responde 400 (não há parâmetro)DEC-CO-14-2
PUT /gl-accounts/{id}/companies/{companyId}404 · 422também 409 por versão vencida (a versão é a da conta)DEC-CO-13-6
GET /users/{userId}/company-scopes400 · 404nunca 404: pessoa sem nenhuma linha devolve 200 com conjunto vazioDEC-CO-15-7
GET /establishments/lookupfiltro por purposetambém oculta situação cadastral que bloqueia documento novo (Suspended, Unfit, ClosedDown)decisão do mantenedor (DEC-CO-12-A)
Correção manual de situação cadastral—correção mais antiga que a gravada responde 200 sem mudança, sem evento; a nota do comprovante é exigida mas não tem coluna e não vai na carga do eventoDEC-CO-12-A (orquestrador); minimização de dados no outbox
ListagensPagedResult<T>PagedResult<T>conforme desde a CO-21 (DEC-V10-A, Gryd.IO 10.0.0); DEC-CO-13-A fechada
RN-EMP-17cascata sobre pontos de entregaporta IEstablishmentAddressChangeHandler com registro vazioDeliveryLocation não existe (D-08)
RN-EMP-36provedor de consulta ao CNPJUnavailableCnpjRegistryProvider (devolve Null, não conclusivo)D-10: provedor concreto fora desta entrega
RN-EMP-13código não listado na tabela de errosALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE (422) e IGLAccountEligibility.CheckAtSubmissionAsyncDEC-CO-19-3; catálogo de 57 códigos
RN-EMP-21 · evento de escopopublica user-company-scope.revoked “depois do commit”grava o evento no outbox na mesma transação do fato, antes do último SaveChanges; a entrega, quando houver worker, é que acontece depois do commitD-06 (outbox transacional do Gryd.IO 10.0.0, CO-21)
Moeda—functionalCurrency só aceita BRLRN-EMP-35
RN-EMP-29—a sucessora precisa começar depois do início da substituída; “início imediato” é effectiveFrom == now exatoconsequência de retiredAt = effectiveFrom da sucessora (DEC-CO-09-4, DEC-CO-09-2)
Unicidade perpétua—code, cnpjRoot, cnpj e externalCode seguem únicos mesmo com a linha apagada logicamenteD-12 (mantenedor: “os quatro reservados”)
CNPJ de dígitos repetidos—CNPJ com os 14 caracteres iguais é recusado como inválido, mesmo que o dígito verificador bataDEC-CO-05-4 (mantenedor)
Onboarding (RN-EMP-01)—qualquer empresa não apagada encerra o onboarding; empresa apagada logicamente não o encerraDEC-CO-10-A (mantenedor)
Invariante do administradordois casossó os dois casos da spec: a regra (iii) que a sessão acrescentara (encerrar a última linha sem fim) foi removidaDEC-CO-10-A (mantenedor)
Remoção da pessoa (guarda 5)—a guarda do administrador permanente não se aplica às linhas da pessoa removida; corte de linha de outro administrador ativo continua sob a guardaDEC-CO-15-A (mantenedor)
Estabelecimento padrão—sem rota nova no Nexio para escrever a preferência nexio.default-establishment-id: a tela escreve na rota de preferências da plataformaDEC-CO-12-A

Riscos de produção que o frontend e a operação herdam. Os eventos são gravados e não entregues (o backlog de nexio.outbox cresce até haver consumidor). O admin semeado do GrydAuth (admin@local.com, senha de bootstrap pública no código do módulo) existe em todo ambiente: trocar a senha ou desativá-lo, criar o administrador real e conceder o AppId do Nexio é pré-requisito do corte em produção. O e-mail dos avisos só sai com SMTP configurado no Worker; sem ele, o envio cai no NoOpEmailSender e o aviso não chega a ninguém. O módulo não tem CPF verificado (HasVerifiedCpf segue nulo), nem cultura do destinatário. O cache de permissões do módulo atrasa a revogação, e, com mais de uma instância da API, ele precisa de Redis. Cada requisição que emite número usa duas conexões do pool. A lista completa, com a origem de cada risco, está em docs/operations/backend.md.

Integração

Contrato para o frontend

Esta seção é o contrato HTTP exato do que está implementado — conferido contra os sete controllers de src/hosts/Nexio.Api/Controllers/Organization, os contratos de Nexio.Organization.Contracts e os testes HTTP — e a orientação de tela que dele decorre. Onde ela difere do texto da spec acima, vale esta; as razões estão em Estado da implementação. A referência de desenvolvimento equivalente é docs/definitions/development/company/api-empresa.md.

Autenticação

O Nexio é o emissor de identidade. O Nexio.Api traz o GrydAuth embutido (DEC-EMB-0): o login, a renovação, o encerramento de sessão, o perfil e as preferências do usuário são rotas da própria API, e o token que ela emite é o que as 32 rotas de negócio validam. Não há mais um serviço de autoridade à parte. As rotas e os campos exatos das rotas de autenticação são os do GrydAuth 10.0.0 (docs/modules/auth do Gryd.IO); a tabela abaixo fixa o que muda para a tela.

Contrato de autenticação do frontend
TemaContrato
URL baseA própria API, na mesma origem das rotas de negócio: autenticação em api/v1/auth/..., negócio em api/v1/nexio/.... Em desenvolvimento local, http://localhost:5000. Some a URL de identidade separada.
AplicaçãoIssuer, Audience e AppId são novos e próprios do Nexio (JwtSettings), definidos pela operação em cada ambiente (em desenvolvimento: NexioDev, NexioDev.Clients e NEXIO_LOCAL). O login informa o AppId no cabeçalho X-App-Id (ou requestedAppId no corpo; o cabeçalho tem prioridade), e o usuário precisa estar autorizado para ele.
Rotas que a tela usaPOST /api/v1/auth/login (devolve o token do tenant quando o usuário tem um tenant só ou um padrão; com vários e nenhum padrão devolve um token global de 2 minutos e requiresTenantSelection); POST /auth/switch-tenant (escolha inicial e troca de tenant); POST /auth/refresh; POST /auth/logout (204, idempotente, encerra todas as sessões do usuário); GET /auth/me; GET|PUT|DELETE /auth/me/preferences/{key} (inclusive nexio.default-establishment-id); POST /auth/request-password-reset, /auth/reset-password, /auth/change-password, /auth/complete-first-login e GET /auth/password-policy; MFA em api/v1/mfa/.... Não há autocadastro: AllowUserRegistration é false.
Cookie de refreshO refresh token não vem no corpo: vai no cookie __Secure-gryd.refresh (HttpOnly, Secure, SameSite=Strict, Path=/api/v1/auth), que o navegador só devolve às rotas de autenticação. A tela envia credentials: include (ou withCredentials) no login, no refresh e no logout.
CSRFCookie legível gryd.csrf; POST /auth/refresh exige o cabeçalho X-Gryd-Csrf com o mesmo valor (envio duplo). Sem o cookie de refresh, 401; cabeçalho ausente ou divergente, 403. A rotação é de uso único: o cookie é reescrito a cada refresh, e reusar o antigo, além da tolerância RefreshReuseGraceSeconds (30 s), é recusado.
DuraçãoAccess token de 10 minutos (ExpirationMinutes, no máximo 15); refresh de 7 dias, ou 12 horas sem “manter conectado”. O token expirado responde 401 e a tela renova antes de refazer a chamada.
Token e claimsMesmo formato de antes: Authorization: Bearer <access token>, claim tenant_id e as demais. O token continua sem empresa. X-Tenant-ID é aceito quando concorda com o claim; divergente, 403.
CORSUma só lista, GrydAuth:Cors:AllowedOrigins, para as rotas de autenticação e as de negócio, com credenciais. Origem fora da lista não recebe cabeçalho CORS. A lista é de cada ambiente: peça à operação a origem do frontend.
Tokens antigosTokens emitidos pela autoridade anterior (outro Issuer, Audience, AppId e chave) deixam de valer: 401, e a tela leva ao login. Data de corte: a definir pelo mantenedor.
Troca de senha pendenteSe o usuário precisa trocar a senha (primeiro acesso, ou troca exigida por um administrador: o login devolve MustChangePassword; a política de senha também liga a expiração, 90 dias com aviso aos 7, mas só o MustChangePassword gera a claim no módulo 10.0.0), o token emitido carrega a claim pwd_change=required e o módulo responde 403 PASSWORD_CHANGE_REQUIRED em toda rota que não esteja marcada para esse estado: as 29 rotas de negócio e quase todas as do módulo. Ficam livres POST /auth/complete-first-login, GET /auth/validate, GET /auth/session, GET /auth/me e as leituras de /auth/me/preferences, além das anônimas (refresh, logout, GET /auth/password-policy). Esse 403 não é falta de permissão: a tela de negócio o recebe, deve reconhecê-lo pelo code e levar à tela de troca de senha, e só depois repetir a chamada com o token novo, que não traz a claim.
Limite de taxaO módulo limita as rotas sensíveis de autenticação (por IP e, em algumas, por conta) e responde 429 em application/problem+json com o cabeçalho Retry-After em segundos (60 quando o limitador não informa). A tela espera esse tempo antes de tentar de novo; não é erro de permissão nem de negócio.
Chaves públicasGET /.well-known/jwks.json é rota pública, fora de api/v1, com as chaves que validam os tokens do host. A tela não precisa dela; quem valida o token fora da API (gateway, outro serviço) usa esta.
Demais rotas do móduloO host serve todas as rotas do GrydAuth.API (DEC-EMB-3): usuários, papéis, permissões (inclusive a importação do catálogo), organizações (/tenants), segurança, favoritos de menu, diretório de usuários (GET /users/directory) e demais, cada uma sob as permissões do próprio GrydAuth. A federação não está configurada na v1: as rotas anônimas nunca respondem 5xx (start e exchange recusam; discover responde “sem federação”).

Nota de migração para o frontend

O que o frontend troca: (1) a URL base de autenticação passa a ser a da API; (2) o login informa o novo AppId e a aplicação adota o novo Issuer e Audience quando valida o token do lado do cliente; (3) os cookies de refresh e de CSRF passam a ser da origem da API, e o credentials: include aponta para ela; (4) a origem do frontend entra em GrydAuth:Cors:AllowedOrigins; (5) todo usuário refaz o login no corte; (6) PATCH /companies/{id}/activate e PUT /users/{userId}/company-scopes não exigem mais read:users e o 503 USER_DIRECTORY_UNAVAILABLE não existe mais (catálogo de 57 códigos): a tela remove o tratamento especial dele, e uma falha de leitura da identidade é um 500 comum.

O que não muda: as 29 rotas de negócio e os caminhos api/v1/nexio/..., o envelope PagedResult<T>, o formato dos erros (ProblemDetails com code e traceId), as nove permissões de empresa e a regra de que o token não traz empresa. Datas de corte: a definir pelo mantenedor.

Convenções

Convenções que valem para as 32 rotas
TemaContrato
Prefixo e versãoToda rota é api/v1/nexio/... (versão 1). Os caminhos das tabelas abaixo omitem o prefixo.
Autenticação e tenantBearer emitido pelo próprio host (GrydAuth embutido, ver Autenticação), validado em cada chamada. O tenant vem do claim tenant_id; cabeçalho X-Tenant-ID ou query que nomeie outro tenant é recusado com 403, e o corpo nunca escolhe tenant. O token não traz empresa: a empresa em que se opera é sempre um dado que a tela lê (GET /me/company-scope) e envia como parâmetro de rota ou de corpo. Sem token, 401; sem a permissão da rota, 403 da plataforma.
Enums por nomeEnums do Nexio trafegam como texto em PascalCase ("icmsTaxpayerStatus": "Taxpayer"), na resposta e no corpo. Valores: TaxRegime (SimplesNacional, MEI, LucroPresumido, LucroReal, TaxExempt, NotApplicable; o último é recusado em empresa); RegistrationStatus (Null, Active, Suspended, Unfit, ClosedDown); IcmsTaxpayerStatus (Taxpayer, Exempt, NonTaxpayer); EstablishmentType (Headquarters, Branch); RegistrationSource (Provider, Manual); GLAccountType (Asset, Liability, Equity, Revenue, Expense); DocumentType (Requisition, QuotationRequest, PurchaseOrder, Receipt, BudgetChangeRequest); NumberSequenceScope (Tenant, Company, Establishment); ResetPolicy (Never, Yearly, Monthly); NumberSequenceState (Scheduled, Current, Retired); CompanyScopeOrigin (Grant, CompanyCreation); OnGrantorChange (Cascade, Restrict); DependentGrantDecision (Keep, End, Trim); CompanyScopeReason (Granted, NoGrant, Expired, Onboarding); EstablishmentPurpose (Receiving, Requesting). Valor fora do enum, ou corpo que não é JSON, é 400 da plataforma: só o status está provado por teste; a forma do corpo desse 400 não.
Datas e instantesDateOnly em yyyy-MM-dd; instantes em ISO 8601 com offset (UTC por padrão). Em POST /number-sequences, effectiveFrom exige offset.
Concorrência otimistaEscrita em empresa, estabelecimento, conta contábil e configuração de numeração exige expectedVersion: o version que a tela leu. Ausente, 0 ou negativo → 400 por campo. Versão bem formada e vencida → 409 com o código *_CONCURRENT_CHANGE_CONFLICT da entidade (alguém gravou depois da leitura: recarregar e refazer). Atualização sem mudança real não grava e não sobe a versão. Cada ato devolve o objeto com a versão nova; a tela deve substituir o que tem. Na escrita de escopo o equivalente é o scopeHash; no move de conta, o expectedPath também.
Erro (ProblemDetails)Toda falha devolve ProblemDetails (RFC 7807) com code e traceId. O status vem do sufixo do código: _NOT_FOUND 404; _ALREADY_EXISTS e _CONFLICT 409; _FORBIDDEN 403; _UNPROCESSABLE 422; _UNAVAILABLE 503; sem sufixo 400. A mensagem vem na cultura da requisição (en-US, pt-BR, es-ES): a tela exibe o texto do servidor e decide o comportamento pelo code. Falha de forma do corpo é 400 VALIDATION_ERROR com os erros por campo (chave = nome do campo do corpo, como headquarters.cityIbgeCode). Não conte com dado estruturado nos erros de domínio: quando a mensagem lista itens (empresas sem administrador, linhas Restrict pendentes), a lista está no texto.
404 em vez de 403Empresa ou estabelecimento fora do escopo da pessoa responde 404 (COMPANY_NOT_FOUND / ESTABLISHMENT_NOT_FOUND), nunca 403, igual ao inexistente. 403 é só permissão ausente, mais COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN e USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN. O 403 do tenant trocado também é da plataforma.
Falha fechadaSe a resolução do escopo falha ou não há contexto de tenant, a requisição responde 500; nunca um conjunto mais amplo. A tela trata como erro genérico.

Listagens, ordenação e lookups

Listagens paginadas — GET /companies, GET /gl-accounts, GET /gl-accounts/{parentId}/children
ParâmetroContrato
EnvelopePagedResult<T>: items, totalCount, pageNumber, pageSize, totalPages, hasNext, hasPrevious, scope. scope é sempre null. A resposta não ecoa sortBy nem sortDirection: a tela guarda o que enviou.
page, pageSizePágina e tamanho (pageSize padrão 20, teto 100: acima do teto é reduzido a 100, não é erro).
sortBy, sortDirectionsortBy fora do mapa da listagem → 400 VALIDATION_ERROR com errors.sortBy (texto na cultura da requisição). sortDirection aceita asc, desc e Ascending, Descending, sem distinguir caixa. Toda ordenação desempata por Id.
searchTexto contido, sem distinção de caixa. Em empresas: código, razão social, nome fantasia e código externo; uma raiz (8 caracteres) ou um CNPJ inteiro (14), com ou sem máscara, casa pela raiz. Em contas: código e nome.
Filtros por rotaEmpresas: isActive, taxRegime. Contas (e filhos): accountType, isActive, allowsPosting.
includeDeletedincludeDeleted=true é 400 LIST_INCLUDE_DELETED_UNSUPPORTED (a exclusão lógica é global; não há como listar apagados).
EscopoA listagem de empresas já vem recortada pelo escopo da pessoa; quem não tem linha vigente recebe 200 com items vazio. Empresas inativas aparecem, marcadas por isActive.
Campos ordenáveis (sortBy) de cada listagem
ListagemCampos aceitosPadrão
GET /companiescode, legalName, tradeName, cnpjRoot, taxRegime, isActive, establishmentCount, createdAtcode
GET /gl-accountscode, name, path, depth, accountType, isActive, validFrom, validTopath (ordem de árvore)
GET /gl-accounts/{parentId}/childrenos mesmos de /gl-accountscode
Lookups — o que cada um oferece
RotaParâmetrosO que devolve e o que oculta
GET /companies/lookupsearch, isActiveArray puro, ordenado por nome fantasia (ou razão social) e código, sem corte e sem paginação. Recortado pelo escopo.
GET /establishments/lookuppurpose obrigatório: Receiving (filtra allowsReceiving) ou Requesting (filtra allowsPurchasing)Agrupado por empresa. Oferece só estabelecimento ativo, de empresa ativa, e cuja situação cadastral não bloqueia documento novo: Suspended, Unfit, ClosedDown ficam de fora; Null, Active entram. Inclui defaultEstablishmentId (abaixo). Sem purpose válido: 400 ESTABLISHMENT_LOOKUP_PURPOSE_REQUIRED.
GET /gl-accounts/lookupcompanyId obrigatório; search, onDate (padrão: hoje em America/Sao_Paulo)Array puro em ordem de árvore. Só conta ativa, que aceita lançamento (allowsPosting), vigente em onDate e habilitada na empresa (ausência de linha = habilitada). Conta desabilitada em outra empresa continua sendo oferecida. 404 COMPANY_NOT_FOUND se a empresa está fora do escopo.

Estabelecimento padrão do lookup. O defaultEstablishmentId vem da preferência nexio.default-establishment-id do usuário na plataforma (UserPreference, lida em processo no GrydAuth embutido, a cada chamada, para o usuário e o tenant da requisição). O valor é o Guid do estabelecimento em texto, e a chave é minúscula porque a plataforma normaliza a chave. Só vale se for um dos itens do próprio lookup: preferência que aponta para estabelecimento inativo, irregular ou fora do alcance é ignorada, e o campo volta nulo; se a leitura da preferência falha (banco de identidade fora), o lookup responde 500, e não mais sem padrão (DEC-CO-33-4). O Nexio não tem rota para escrever a preferência: a tela escreve na rota da plataforma (PUT /auth/me/preferences/nexio.default-establishment-id) e deve oferecer a escolha entre os itens do próprio lookup.

As 32 rotas

Todas exigem autenticação (401 sem token) e a permissão da coluna (403 sem ela); os erros de domínio são os da coluna final, e toda rota de escrita pode devolver também 400 VALIDATION_ERROR por campo. Os caminhos omitem o prefixo api/v1/nexio. Os campos em negrito são obrigatórios.

Empresa (8 rotas)
RotaPermissãoCorpo ou queryRetornoErros de domínio
GET /companiesread:companiesquery: page, pageSize, sortBy, sortDirection, search, isActive, taxRegime200 PagedResult<CompanyDto>400 LIST_INCLUDE_DELETED_UNSUPPORTED, VALIDATION_ERROR
GET /companies/lookupread:companiesquery: search (≤ 100), isActive200 array puro de CompanyLookupDto, sem paginação e sem corte400 VALIDATION_ERROR
GET /companies/{id}read:companies—200 CompanyDetailDto404 COMPANY_NOT_FOUND (inclui fora do escopo)
POST /companiescreate:companies e update:company-scopesEmpresa: obrigatórios code, cnpjRoot, legalName, taxRegime, headquarters; opcionais functionalCurrency (só BRL), tradeName, externalCode, legalNature, shareCapital, foundedOn, closedOn. Limites: code 1–20 (A–Z, 0–9, _, -), legalName ≤ 200, tradeName ≤ 160, externalCode ≤ 40, legalNature ≤ 4 caracteres; shareCapital ≥ 0 com até 2 casas. Estabelecimento (corpo da matriz e de POST /companies/{companyId}/establishments): obrigatórios code, cnpj, name, icmsTaxpayerStatus, zipCode, street, number, district, cityIbgeCode; opcionais externalCode, stateRegistration, municipalRegistration, suframaCode, primaryCnae, complement, countryCode e allowsPurchasing, allowsReceiving (padrão true). Formato: code 1–20 de A–Z, 0–9, _ ou - (gravado em maiúsculas); zipCode 8 dígitos sem máscara; cityIbgeCode 7 dígitos; countryCode 2 letras (padrão BR); name ≤ 160, externalCode ≤ 40, stateRegistration e municipalRegistration ≤ 20, suframaCode ≤ 9, primaryCnae ≤ 7, endereço ≤ 60 por campo. A inscrição estadual é obrigatória com icmsTaxpayerStatus = Taxpayer e proibida com Exempt. A UF não é enviada: sai do cityIbgeCode.201 CompanyDetailDto + Location → GET /companies/{id}. Grava empresa, matriz e a linha de escopo de quem criou (origin = CompanyCreation) numa transação400 COMPANY_CNPJ_ROOT_INVALID, ESTABLISHMENT_CNPJ_INVALID, VALIDATION_ERROR
403 COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN
409 COMPANY_CODE_ALREADY_EXISTS, COMPANY_CNPJ_ROOT_ALREADY_EXISTS, COMPANY_EXTERNAL_CODE_ALREADY_EXISTS, ESTABLISHMENT_CNPJ_ALREADY_EXISTS
422 COMPANY_FUNCTIONAL_CURRENCY_UNPROCESSABLE, ESTABLISHMENT_CNPJ_ROOT_MISMATCH_UNPROCESSABLE, ESTABLISHMENT_STATE_REGISTRATION_REQUIRED_UNPROCESSABLE, ESTABLISHMENT_STATE_REGISTRATION_NOT_ALLOWED_UNPROCESSABLE
PUT /companies/{id}update:companiesexpectedVersion (inteiro ≥ 1, o version lido); obrigatórios legalName, taxRegime (NotApplicable é recusado); opcionais tradeName, externalCode, legalNature, shareCapital, foundedOn, closedOn. Substitui o perfil inteiro: opcional omitido vai como nulo. code, cnpjRoot e functionalCurrency são ignorados (não recusados). Sem mudança real não grava e não sobe a versão. closedOn não mexe em isActive200 CompanyDetailDto400 VALIDATION_ERROR
404 COMPANY_NOT_FOUND
409 COMPANY_CONCURRENT_CHANGE_CONFLICT, COMPANY_EXTERNAL_CODE_ALREADY_EXISTS
PATCH /companies/{id}/activateupdate:companiesexpectedVersion (inteiro ≥ 1, o version lido). Reativa a empresa sem reativar filiais. Ativar o que já está ativo não faz nada200 CompanyDetailDto400 VALIDATION_ERROR
404 COMPANY_NOT_FOUND
409 COMPANY_CONCURRENT_CHANGE_CONFLICT, USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT
PATCH /companies/{id}/deactivateupdate:companiesexpectedVersion (inteiro ≥ 1, o version lido). Cascateia: todo estabelecimento ativo cai junto200 CompanyDeactivationDto = CompanyDetailDto + deactivatedEstablishments[]400 VALIDATION_ERROR
404 COMPANY_NOT_FOUND
409 COMPANY_CONCURRENT_CHANGE_CONFLICT
PATCH /companies/{id}/headquartersupdate:companiesexpectedVersion (inteiro ≥ 1, o version lido); establishmentId (a nova matriz, da mesma empresa). Indicar a matriz atual devolve 200 sem mudança200 CompanyDetailDto400 VALIDATION_ERROR
404 COMPANY_NOT_FOUND, ESTABLISHMENT_NOT_FOUND
409 COMPANY_CONCURRENT_CHANGE_CONFLICT
422 ESTABLISHMENT_INACTIVE_UNPROCESSABLE
Estabelecimento (8 rotas)
RotaPermissãoCorpo ou queryRetornoErros de domínio
GET /companies/{companyId}/establishmentsread:companies—200 array puro de EstablishmentDto, inativos incluídos, matriz primeiro404 COMPANY_NOT_FOUND
GET /establishments/lookupread:companiesquery: purpose obrigatório, Receiving ou Requesting (sem distinção de caixa)200 EstablishmentLookupDto, agrupado por empresa, com defaultEstablishmentId400 ESTABLISHMENT_LOOKUP_PURPOSE_REQUIRED (ausente, em branco ou desconhecido)
GET /establishments/{id}read:companies—200 EstablishmentDetailDto404 ESTABLISHMENT_NOT_FOUND
POST /companies/{companyId}/establishmentscreate:companiesEstabelecimento (corpo da matriz e de POST /companies/{companyId}/establishments): obrigatórios code, cnpj, name, icmsTaxpayerStatus, zipCode, street, number, district, cityIbgeCode; opcionais externalCode, stateRegistration, municipalRegistration, suframaCode, primaryCnae, complement, countryCode e allowsPurchasing, allowsReceiving (padrão true). Formato: code 1–20 de A–Z, 0–9, _ ou - (gravado em maiúsculas); zipCode 8 dígitos sem máscara; cityIbgeCode 7 dígitos; countryCode 2 letras (padrão BR); name ≤ 160, externalCode ≤ 40, stateRegistration e municipalRegistration ≤ 20, suframaCode ≤ 9, primaryCnae ≤ 7, endereço ≤ 60 por campo. A inscrição estadual é obrigatória com icmsTaxpayerStatus = Taxpayer e proibida com Exempt. A UF não é enviada: sai do cityIbgeCode. Sem expectedVersion. Nasce Branch. A situação cadastral é consultada na criação201 EstablishmentDetailDto + Location → GET /establishments/{id}400 ESTABLISHMENT_CNPJ_INVALID, VALIDATION_ERROR
404 COMPANY_NOT_FOUND
409 ESTABLISHMENT_CODE_ALREADY_EXISTS, ESTABLISHMENT_CNPJ_ALREADY_EXISTS, ESTABLISHMENT_EXTERNAL_CODE_ALREADY_EXISTS, COMPANY_CONCURRENT_CHANGE_CONFLICT
422 ESTABLISHMENT_CNPJ_ROOT_MISMATCH_UNPROCESSABLE, ESTABLISHMENT_STATE_REGISTRATION_REQUIRED_UNPROCESSABLE, ESTABLISHMENT_STATE_REGISTRATION_NOT_ALLOWED_UNPROCESSABLE
O COMPANY_CONCURRENT_CHANGE_CONFLICT aqui não é erro do cliente: o ato sobe a versão da empresa, e dois POST simultâneos disputam; repetir resolve
PUT /establishments/{id}update:companiesexpectedVersion (inteiro ≥ 1, o version lido); os campos do estabelecimento sem code (imutável; type e empresa também), mais cnpj (obrigatório e editável; a raiz continua a da empresa) e deliveryLocationInterstateNotes[] (opcional: deliveryLocationId + interstateNote, sem repetir ponto). Substitui o perfil inteiro. Mudar município, UF ou allowsReceiving revalida os pontos de entrega (registro hoje vazio)200 EstablishmentDetailDto400 VALIDATION_ERROR
404 ESTABLISHMENT_NOT_FOUND
409 ESTABLISHMENT_CONCURRENT_CHANGE_CONFLICT, ESTABLISHMENT_CNPJ_ALREADY_EXISTS, ESTABLISHMENT_EXTERNAL_CODE_ALREADY_EXISTS, COMPANY_CONCURRENT_CHANGE_CONFLICT
422 ESTABLISHMENT_ADDRESS_CHANGE_BREAKS_DELIVERY_LOCATIONS_UNPROCESSABLE, ESTABLISHMENT_CNPJ_ROOT_MISMATCH_UNPROCESSABLE, ESTABLISHMENT_STATE_REGISTRATION_REQUIRED_UNPROCESSABLE, ESTABLISHMENT_STATE_REGISTRATION_NOT_ALLOWED_UNPROCESSABLE
PATCH /establishments/{id}/activateupdate:companiesexpectedVersion (inteiro ≥ 1, o version lido)200 EstablishmentDetailDto400 VALIDATION_ERROR
404 ESTABLISHMENT_NOT_FOUND
409 ESTABLISHMENT_CONCURRENT_CHANGE_CONFLICT, COMPANY_CONCURRENT_CHANGE_CONFLICT
PATCH /establishments/{id}/deactivateupdate:companiesexpectedVersion (inteiro ≥ 1, o version lido). Recusa desativar o último estabelecimento ativo de empresa ativa200 EstablishmentDetailDto400 VALIDATION_ERROR
404 ESTABLISHMENT_NOT_FOUND
409 ESTABLISHMENT_CONCURRENT_CHANGE_CONFLICT, COMPANY_CONCURRENT_CHANGE_CONFLICT, ESTABLISHMENT_LAST_ACTIVE_CONFLICT
PATCH /establishments/{id}/registration-statusupdate:companiesexpectedVersion (inteiro ≥ 1, o version lido); status (Active, Suspended, Unfit, ClosedDown; Null é recusado); statusDate (não pode estar no futuro); evidenceNote (obrigatória). Vale a data mais recente: correção mais antiga que a gravada responde 200 sem mudança e sem evento200 EstablishmentDetailDto400 VALIDATION_ERROR
404 ESTABLISHMENT_NOT_FOUND
409 ESTABLISHMENT_CONCURRENT_CHANGE_CONFLICT
Plano de contas (7 rotas)
RotaPermissãoCorpo ou queryRetornoErros de domínio
GET /gl-accountsread:gl-accountsquery: page, pageSize, sortBy, sortDirection, search, accountType, isActive, allowsPosting. Lista o plano inteiro, todos os níveis200 PagedResult<GLAccountDto>400 VALIDATION_ERROR, LIST_INCLUDE_DELETED_UNSUPPORTED
GET /gl-accounts/lookupread:gl-accountsquery: companyId obrigatório; search, onDate (padrão: hoje em America/Sao_Paulo)200 array puro de GLAccountLookupItemDto, em ordem de árvore400 VALIDATION_ERROR
404 COMPANY_NOT_FOUND (400 sem companyId; 404 inclui fora do escopo)
GET /gl-accounts/{parentId}/childrenread:gl-accountsquery como a listagem; só os filhos diretos200 PagedResult<GLAccountDto>400 VALIDATION_ERROR, LIST_INCLUDE_DELETED_UNSUPPORTED
404 GL_ACCOUNT_NOT_FOUND
POST /gl-accountscreate:gl-accountsobrigatórios code, name e, na raiz, accountType; opcionais parentId, allowsPosting, validFrom, validTo, externalCode. code ≤ 30, name ≤ 160, externalCode ≤ 40; validTo não anterior a validFrom. Tipo herdado do pai. code é único no tenant e reservado para sempre; name é único entre irmãos201 GLAccountDetailDto + Location → a listagem400 VALIDATION_ERROR
404 GL_ACCOUNT_NOT_FOUND
409 GL_ACCOUNT_CODE_ALREADY_EXISTS, GL_ACCOUNT_SIBLING_NAME_ALREADY_EXISTS, GL_ACCOUNT_CONCURRENT_CHANGE_CONFLICT
422 GL_ACCOUNT_TYPE_MISMATCH_UNPROCESSABLE
404 é o pai inexistente; todo filho criado versiona o pai
PUT /gl-accounts/{id}update:gl-accountsexpectedVersion (inteiro ≥ 1, o version lido); name; opcionais allowsPosting, validFrom, validTo, externalCode. code, parentId, accountType não mudam. Pedir allowsPosting para nó com filho é recusado200 GLAccountDetailDto400 VALIDATION_ERROR
404 GL_ACCOUNT_NOT_FOUND
409 GL_ACCOUNT_CONCURRENT_CHANGE_CONFLICT, GL_ACCOUNT_SIBLING_NAME_ALREADY_EXISTS
422 GL_ACCOUNT_POSTING_NOT_ALLOWED_UNPROCESSABLE
PATCH /gl-accounts/{id}/parentupdate:gl-accountsexpectedVersion (inteiro ≥ 1, o version lido); parentId ou toRoot: true (nenhum, ou os dois, é 400); expectedPath (o path lido). Reescreve o caminho dos descendentes numa transação200 GLAccountDetailDto400 VALIDATION_ERROR
404 GL_ACCOUNT_NOT_FOUND
409 GL_ACCOUNT_CONCURRENT_CHANGE_CONFLICT, GL_ACCOUNT_SIBLING_NAME_ALREADY_EXISTS
422 GL_ACCOUNT_PARENT_CYCLE_UNPROCESSABLE, GL_ACCOUNT_TYPE_MISMATCH_UNPROCESSABLE
409 de concorrência vale para versão ou path mudado desde a leitura
PUT /gl-accounts/{id}/companies/{companyId}update:gl-accountsisEnabled; opcional externalCode (≤ 40); expectedVersion é a versão da conta. O código externo da empresa vence o da conta200 GLAccountCompanyDto (glAccountId, companyId, isEnabled, externalCode, accountVersion)400 VALIDATION_ERROR
404 COMPANY_NOT_FOUND, GL_ACCOUNT_NOT_FOUND
409 GL_ACCOUNT_CONCURRENT_CHANGE_CONFLICT
Numeração (3 rotas)
RotaPermissãoCorpo ou queryRetornoErros de domínio
GET /number-sequencesread:number-sequences—200 array puro de NumberSequenceDtonunca responde 400
POST /number-sequencesread:number-sequencesobrigatórios documentType, prefix (1 a 10 caracteres, sem espaço); opcionais scope (padrão Company), padding (padrão 6, de 1 a 21), resetPolicy (padrão Yearly), timeZone (IANA; padrão America/Sao_Paulo), effectiveFrom (com offset, Z ou ±hh[:mm]; sem offset é 400; padrão: agora), replacesSequenceId e expectedVersion (obrigatório com replacesSequenceId). Sem replacesSequenceId: abre a primeira do tipo. Com ele: substitui, e a substituída recebe retiredAt = effectiveFrom da sucessora201 NumberSequenceDto + Location → a listagem (não existe GET /number-sequences/{id})400 VALIDATION_ERROR
404 NUMBER_SEQUENCE_NOT_FOUND
409 NUMBER_SEQUENCE_ALREADY_EXISTS, NUMBER_SEQUENCE_CONCURRENT_CHANGE_CONFLICT
422 NUMBER_SEQUENCE_REPLACEMENT_UNPROCESSABLE, NUMBER_SEQUENCE_PREFIX_OVERLAP_UNPROCESSABLE, NUMBER_SEQUENCE_PADDING_UNPROCESSABLE, NUMBER_SEQUENCE_TIME_ZONE_UNPROCESSABLE
404 é replacesSequenceId inexistente; 409 de concorrência vale para quem não é a mais recente, versão vencida e perda de corrida
PUT /number-sequences/{id}update:number-sequencespadding e expectedVersion (inteiro ≥ 1, o version lido). Só o padding edita; o resto é identidade da configuração (mudar é substituir)200 NumberSequenceDto400 VALIDATION_ERROR
404 NUMBER_SEQUENCE_NOT_FOUND
409 NUMBER_SEQUENCE_CONCURRENT_CHANGE_CONFLICT
422 NUMBER_SEQUENCE_PADDING_UNPROCESSABLE
Escopo por empresa (3 rotas)
RotaPermissãoCorpo ou queryRetornoErros de domínio
GET /users/{userId}/company-scopesread:company-scopes—200 UserCompanyScopeSetDto. Só as linhas das empresas que quem lê alcança; vigentes primeiro, encerradas depois. Pessoa sem linha: 200 com conjunto vazionunca 404
PUT /users/{userId}/company-scopesupdate:company-scopesexpectedScopeHash (o scopeHash lido); companies: lista de { companyId, validTo? } (lista vazia é válida); opcional dependentGrantDecisions: lista de { scopeId, decision } com Keep, End, Trim. Substitui, não faz merge200 o UserCompanyScopeSetDto novo400 VALIDATION_ERROR
403 USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN
404 COMPANY_NOT_FOUND, USER_COMPANY_SCOPE_NOT_FOUND
409 USER_COMPANY_SCOPE_CONCURRENT_CHANGE_CONFLICT, USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT, USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT, USER_COMPANY_SCOPE_DUPLICATE_CONFLICT
422 USER_COMPANY_SCOPE_BEYOND_GRANTOR_UNPROCESSABLE, USER_COMPANY_SCOPE_VALIDITY_UNPROCESSABLE, USER_NOT_IN_TENANT_UNPROCESSABLE
Guardas nesta ordem: própria pessoa (403) → hash (409) → além da vigência de quem concede (422) → decisões das Restrict (409) → último administrador (409)
GET /me/company-scopenenhuma (só autenticação)—200 MyCompanyScopeDto = { reason, companies: [{ id, code, name, isActive }] }sem escopo no fluxo, 500 (falha fechada)

Formas das respostas

TipoCampos
CompanyDtoid, code, cnpjRoot, legalName, tradeName, taxRegime, isActive, closedOn, headquartersId, establishmentCount, version, createdAt
CompanyLookupDtoid, code, name, cnpjRoot, isActive
CompanyDetailDtoid, code, cnpjRoot, legalName, tradeName, externalCode, taxRegime, legalNature, shareCapital, functionalCurrency, foundedOn, closedOn, isActive, headquartersId, establishmentCount, version, createdAt, activeEstablishments[], inactiveEstablishments[], warnings[]
CompanyEstablishmentDto (resumo dentro da empresa)id, code, cnpj, name, type, isActive, allowsPurchasing, allowsReceiving, registrationStatus, version
CompanyDeactivationDtoCompanyDetailDto + deactivatedEstablishments[] (lista de CompanyEstablishmentDto)
EstablishmentDto (lista da empresa)id, companyId, code, name, cnpj, type, cityIbgeCode, stateCode, isActive, allowsPurchasing, allowsReceiving, registrationStatus, version
EstablishmentDetailDtoid, companyId, code, cnpj, cnpjRoot, type, name, externalCode, icmsTaxpayerStatus, stateRegistration, municipalRegistration, suframaCode, primaryCnae, registrationStatus, registrationStatusDate, registrationSource, registrationCheckedAt, zipCode, street, number, complement, district, cityIbgeCode, stateCode, countryCode, allowsPurchasing, allowsReceiving, isActive, version
EstablishmentLookupDto{ defaultEstablishmentId, companies: [{ companyId, code, name, establishments: [{ id, code, name, type, stateCode, registrationStatus }] }] }
GLAccountDto (e GLAccountDetailDto, que acrescenta createdAt, updatedAt)id, code, name, parentId, path, depth, accountType, allowsPosting, validFrom, validTo, isActive, externalCode, version, hasChildren
GLAccountLookupItemDtoid, code, name, accountType, path
NumberSequenceDtoid, documentType, scope, prefix, padding, resetPolicy, timeZone, effectiveFrom, retiredAt, replacesSequenceId, version, state
UserCompanyScopeSetDto{ userId, scopeHash, lines: [{ id, companyId, companyCode, companyName, validFrom, validTo, origin, grantedByUserId, onGrantorChange, endedByUserId, endedAt, isLive }] }

Implementado: as tabelas de Endpoints e Contrato de erros continuam sendo a especificação; esta é a contraprova do código. Em caso de dúvida sobre um código de erro por rota, vale a coluna acima.

Preenchimento por consulta

O cadastro de empresa e de estabelecimento deixa de exigir que a pessoa digite CEP, endereço e código IBGE: a tela consulta o backend, que consulta provedores públicos (BrasilAPI; ViaCEP como reserva do CEP). A tela nunca fala com provedor nenhum e nada do que a consulta devolve é obrigatório: é sugestão para preencher o formulário, que continua válido digitado à mão. As três rotas usam read:companies, não gravam nada e não dependem do escopo por empresa.

Consulta de cadastro (3 rotas)
RotaPermissãoEntradaRetornoErros
GET /registry/cnpj/{cnpj}read:companiescnpj: 14 caracteres (dígitos e letras) sem máscara200 CompanyRegistryDto400 REGISTRY_CNPJ_INVALID · 404 REGISTRY_CNPJ_NOT_FOUND · 503 REGISTRY_LOOKUP_UNAVAILABLE
GET /registry/zip-codes/{zipCode}read:companieszipCode: 8 dígitos, sem hífen200 ZipCodeAddressDto400 REGISTRY_ZIP_CODE_INVALID · 404 REGISTRY_ZIP_CODE_NOT_FOUND · 503 REGISTRY_LOOKUP_UNAVAILABLE
GET /registry/municipalitiesread:companiesquery: search (2 a 100 caracteres), stateCode (opcional, UF), limit (1 a 50, padrão 20)200 array puro de { ibgeCode, name, stateCode }, sem paginação400 VALIDATION_ERROR (errors.search, errors.stateCode, errors.limit)
Formas das respostas da consulta
TipoCampos
CompanyRegistryDtocnpj, cnpjRoot, isHeadquarters, legalName, tradeName, primaryCnae (7 dígitos), legalNature, foundedOn, registrationStatus (Active, Suspended, Unfit, ClosedDown; null quando o provedor não diz), registrationStatusDate, address (zipCode, street, number, withoutNumber, complement, district, cityName, cityIbgeCode, stateCode, countryCode), source, checkedAt. Todo campo, exceto cnpj, cnpjRoot, source e checkedAt, pode ser null
ZipCodeAddressDtozipCode, street, district (null em CEP geral), cityName, cityIbgeCode, stateCode, countryCode, source

O que a tela faz

  • CNPJ. Ao sair do campo com um CNPJ completo, chama GET /registry/cnpj/{cnpj} (sem máscara: tire os pontos, a barra e o hífen antes). Preenche razão social, nome fantasia, CNAE principal, natureza jurídica, data de início, endereço e a situação cadastral. Nunca sobrescreve em silêncio um campo que a pessoa já digitou: pergunta antes. Em empresa, isHeadquarters: false é um aviso (o CNPJ é de filial), não um bloqueio.
  • Número. withoutNumber: true marca "sem número"; number vem "SN" (ou null quando o provedor deixou em branco). A tela mostra a caixa "sem número" marcada e envia o número que a pessoa confirmar.
  • CEP. Ao completar os 8 dígitos (sem hífen na chamada; a máscara é só visual), chama GET /registry/zip-codes/{zipCode} e preenche rua, bairro, município e UF. street e district null (CEP geral) deixam esses dois campos para a pessoa.
  • Município. O campo de município é um autocomplete sobre GET /registry/municipalities?search=&stateCode=&limit=, sem acento e sem diferença de caixa; comece a buscar a partir de 2 caracteres e use debounce. Escolher uma linha grava o ibgeCode em cityIbgeCode; a UF nunca é digitada, vem do stateCode da resposta (D-09).
  • 404 (REGISTRY_CNPJ_NOT_FOUND, REGISTRY_ZIP_CODE_NOT_FOUND): mensagem curta no campo ("não encontrado"); a pessoa segue digitando.
  • 503 (REGISTRY_LOOKUP_UNAVAILABLE): nenhuma base respondeu. A tela não bloqueia o cadastro: avisa que a consulta está fora do ar e deixa o formulário à mão. Não repita a chamada em laço.
  • limit que não é inteiro (abc) é recusado pelo binder, antes do validador: 400 da plataforma sem code e com a chave errors.Limit. Trate qualquer 400 desta rota como erro de campo.
  • 400 (REGISTRY_CNPJ_INVALID, REGISTRY_ZIP_CODE_INVALID): a tela deveria ter validado antes (14 caracteres com dígitos verificadores; 8 dígitos); trate como erro de campo.
  • source (brasilapi, viacep) e checkedAt dizem de onde veio e de quando é o dado; a resposta pode vir de um cache do servidor (CNPJ por até 1 dia, CEP por até 7 dias), e checkedAt é o instante em que o provedor respondeu, não o da leitura.
  • O registrationStatus desta consulta é informativo: o campo que vale para o bloqueio de documento continua sendo o do estabelecimento (RN-EMP-36), atualizado pela varredura diária — que só lê este registro quando o servidor liga Registry:Cnpj:FeedRegistrationStatus.

Fluxos de tela e o que a UI precisa tratar

Onboarding — tenant sem empresa

  • A tela decide o modo chamando GET /me/company-scope (sem permissão): reason Onboarding e companies: [] significam que o tenant ainda não registrou empresa. É o único estado em que o cadastro da primeira empresa é a única coisa a mostrar, e só a quem tem create:companies, update:company-scopes; os demais veem “aguardando o cadastro da primeira empresa”.
  • NoGrant (há empresas, mas a pessoa não tem linha) e Expired (só linhas encerradas ou vencidas) também devolvem companies: []: a tela diz que falta concessão, não que não há empresa. Granted traz as empresas alcançadas, inativas incluídas (isActive).
  • Cadastro do tenant que não tem empresa (item, fornecedor, unidade) segue só a permissão, sem depender do escopo.

Criação de empresa — matriz inline e escopo do criador

  • Um só POST /companies: o formulário tem dois blocos (empresa e matriz) e vai num corpo só, com o CNPJ de 14 da matriz em headquarters.cnpj e a raiz de 8 em cnpjRoot. Empresa, matriz e a linha de escopo de quem criou nascem na mesma transação.
  • Só quem criou alcança a empresa nova (origin = CompanyCreation, sem validTo); ninguém mais a vê até receber escopo. Depois do 201, a tela deve oferecer a concessão da empresa a outras pessoas (PUT /users/{userId}/company-scopes).
  • Sem update:company-scopes no token: 403 COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN e nada é gravado. A tela esconde ou desabilita a ação sem as duas permissões; o 403 é a guarda, não o aviso.
  • Erros que o formulário mapeia a campo: COMPANY_CNPJ_ROOT_INVALID e ESTABLISHMENT_CNPJ_INVALID (400), ESTABLISHMENT_CNPJ_ROOT_MISMATCH_UNPROCESSABLE (a raiz do CNPJ da matriz não é a da empresa), as inscrições estaduais (obrigatória com Taxpayer, proibida com Exempt) e as duplicidades 409 de code, cnpjRoot, externalCode e CNPJ. Valores de linha apagada logicamente também colidem (reservados para sempre). Município IBGE desconhecido volta como VALIDATION_ERROR em headquarters.cityIbgeCode, com mensagem em inglês.

Desativação em cascata e reativação

  • PATCH /companies/{id}/deactivate desativa também todos os estabelecimentos ativos. Antes de confirmar, a tela mostra o que vai cair (activeEstablishments[] do detalhe); depois, mostra a lista que a resposta traz em deactivatedEstablishments[]. Cada estabelecimento que caiu tem a versão incrementada: recarregar a lista.
  • Reativar a empresa (PATCH .../activate) não reativa filiais. A empresa pode ficar ativa sem estabelecimento ativo até alguém reativar um (PATCH /establishments/{id}/activate): a tela oferece a lista dos inativos (inactiveEstablishments[]) para escolher quais voltam.
  • Reativar exige só update:companies. 409 USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT (a mensagem nomeia a empresa) significa que a empresa ficaria sem administrador permanente: levar a pessoa à tela de escopo.
  • Desativar um estabelecimento que é o último ativo de empresa ativa é 409 ESTABLISHMENT_LAST_ACTIVE_CONFLICT; a saída é desativar a empresa. Empresa inativa não emite documento novo, mas continua legível.

Troca de matriz

  • PATCH /companies/{id}/headquarters com { establishmentId, expectedVersion }: um ato só; a matriz antiga vira Branch. A tela oferece só estabelecimentos ativos da empresa (inativo é 422 ESTABLISHMENT_INACTIVE_UNPROCESSABLE; de outra empresa ou fora do escopo é 404 ESTABLISHMENT_NOT_FOUND).
  • Sobem a versão da empresa e das duas filiais: recarregar o detalhe inteiro. Indicar a matriz atual devolve 200 sem mudança.

Escopo por empresa

  • Leitura: GET /users/{userId}/company-scopes devolve o scopeHash e as linhas — vigentes (isLive = true) primeiro, encerradas depois. Só aparecem as empresas que quem lê alcança; as demais linhas da pessoa existem e a tela não as vê.
  • Escrita: o PUT substitui o conjunto vigente da pessoa dentro das empresas que quem concede alcança. A tela monta a lista do estado desejado (todas as empresas que a pessoa deve ter, cada uma com validTo opcional) e devolve o scopeHash lido. Empresa que sai da lista é encerrada; empresa nova ganha linha; empresa com outro validTo tem a linha encerrada e reaberta. Lista vazia encerra todo o acesso da pessoa nas empresas de quem concede. Empresas que quem concede não alcança ficam intocadas e invisíveis.
  • 409 USER_COMPANY_SCOPE_CONCURRENT_CHANGE_CONFLICT: o hash mudou (ou as travas estouraram em 10 s). Nada foi gravado: a tela recarrega o conjunto e refaz.
  • 409 USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT: o ato encerra ou encurta uma linha de quem concedeu linhas Restrict a outras pessoas. A tela precisa coletar, para cada linha Restrict alcançada, uma decisão — Keep (mantém), End (encerra) ou Trim (corta para o novo fim) — e reenviar em dependentGrantDecisions, com o scopeId de cada linha. A lista de decisões tem de cobrir exatamente as alcançadas. As linhas Cascade são cortadas em cadeia sem pergunta. Atenção: a mensagem lista pessoa, código da empresa e validTo e não traz o scopeId (ver Lacunas).
  • 403 USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN: ninguém altera as próprias linhas (a tela esconde a edição quando userId é o do usuário logado). 422 USER_COMPANY_SCOPE_BEYOND_GRANTOR_UNPROCESSABLE: a linha passaria da vigência de quem concede (a mensagem nomeia a empresa). 409 USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT: a empresa ficaria sem administrador permanente (a mensagem lista as empresas). 422 USER_NOT_IN_TENANT_UNPROCESSABLE: empresa no corpo para quem saiu da organização — para essa pessoa só a lista vazia é aceita.
  • validTo é um instante (ISO 8601 com offset), exclusivo, e precisa ser posterior ao momento do ato (senão 422 USER_COMPANY_SCOPE_VALIDITY_UNPROCESSABLE). Revogar vale na requisição seguinte.

Avisos do detalhe da empresa — warnings[]

  • CompanyDetailDto.warnings é uma lista de textos de código. Hoje existe um: company-closed-but-active — closedOn preenchido e a empresa ainda ativa. É aviso, não erro: a tela mostra a faixa “empresa encerrada em ..., ainda ativa” e oferece a desativação. closedOn nunca desativa sozinho.
  • Depois da data de encerramento, documento novo é recusado pelas portas (COMPANY_CLOSED_UNPROCESSABLE); no próprio dia ainda vale.

Situação cadastral irregular

  • O estabelecimento com situação Suspended, Unfit, ClosedDown some do lookup (quem escolhe estabelecimento para documento não o vê); Null, Active aparecem. Na tela de cadastro ele continua listado, com registrationStatus, e a correção manual é PATCH .../registration-status (status, data, nota do comprovante).
  • Sem provedor de consulta ao CNPJ, a situação só muda por correção manual: Null significa “não verificado”, não “regular”. registrationSource diz se veio do Provider ou foi Manual. A nota do comprovante não é gravada em coluna e não volta em nenhuma resposta.

Numeração — configurações agendadas, vigentes e aposentadas

  • GET /number-sequences devolve tudo, ordenado por tipo e início, com state derivado na hora: Scheduled (agendada: effectiveFrom no futuro), Current (vigente) e Retired (aposentada: tem retiredAt). A tela agrupa por documentType e destaca a vigente.
  • Mudar a numeração é substituir: POST com replacesSequenceId e expectedVersion da configuração mais recente do tipo. A sucessora precisa de prefixo novo ou período novo (422 NUMBER_SEQUENCE_REPLACEMENT_UNPROCESSABLE), e começar depois do início da substituída. Só o padding se edita no lugar.
  • 409 NUMBER_SEQUENCE_ALREADY_EXISTS: já há vigente ou agendada e o corpo não trouxe replacesSequenceId. Nenhum número emitido é exibido por estas rotas: a emissão é porta interna (INumberIssuer).

Campos derivados e somente leitura

EntidadeCampos que a tela mostra e nunca envia como valor editável
Empresaid, headquartersId, establishmentCount, isActive, version, createdAt — isActive muda pelas rotas activate/deactivate; headquartersId pela troca de matriz. code, cnpjRoot, functionalCurrency só existem na criação (o PUT os ignora).
Estabelecimentoid, companyId, type, cnpjRoot, stateCode, isActive, registrationStatus, registrationStatusDate, registrationSource, registrationCheckedAt, version — a UF vem do cityIbgeCode; a situação cadastral só muda pela rota própria. code é imutável depois de criado.
Conta contábilid, path, depth, hasChildren, accountType, isActive, version, createdAt, updatedAt — accountType é herdado do pai e imutável; path e depth são do servidor; o path lido volta no expectedPath do move.
Numeraçãoid, state, retiredAt, replacesSequenceId, version — state é calculado na resposta.
EscoposcopeHash, id, validFrom, origin, grantedByUserId, onGrantorChange, endedByUserId, endedAt, isLive — validFrom é o instante do ato, nunca enviado.
Permissões que exibem ou ocultam cada ação
Ação da telaPermissões necessárias
Ver empresas e estabelecimentos (listas, detalhe, lookups, seletor)read:companies. O seletor de empresa em si lê GET /me/company-scope, que não exige permissão.
Criar empresacreate:companies e update:company-scopes (esta no token de quem cria).
Criar estabelecimentocreate:companies.
Editar empresa ou estabelecimento, trocar matriz, desativar, corrigir situação cadastralupdate:companies.
Reativar empresaupdate:companies.
Reativar estabelecimentoupdate:companies.
Ver plano de contas e lookup de contasread:gl-accounts.
Criar contacreate:gl-accounts.
Editar conta, mover, habilitar por empresaupdate:gl-accounts.
Ver numeraçãoread:number-sequences.
Criar e substituir numeraçãocreate:number-sequences e update:number-sequences.
Ver o escopo de uma pessoaread:company-scopes.
Definir o escopo de uma pessoaupdate:company-scopes.
Escolher o estabelecimento padrãoRota de preferências do GrydAuth, no próprio host (PUT /auth/me/preferences/{key}); o Nexio não tem rota de negócio para isso.

O recorte por empresa não é permissão: uma pessoa com read:companies e nenhuma linha de escopo vê listas vazias e 404 por id. A tela deve tratar “tem a permissão, mas não alcança nenhuma empresa” como o estado “falta concessão”, não como erro.

Lacunas

Lacunas que o frontend vai sentir
FaltaEfeito na tela e o que fazer
Não há GET /gl-accounts/{id}A tela de edição de conta parte do item da listagem ou do retorno do POST/PUT/PATCH; hasChildren, path e version vêm da listagem.
Não há ativar/desativar conta contábilisActive aparece em GLAccountDto e é filtro de listagem, mas nenhuma rota o muda (as portas de status existem no agregado). Encerrar uma conta hoje só por validTo no PUT.
Não há leitura das habilitações por empresaPUT /gl-accounts/{id}/companies/{companyId} grava e devolve a linha, mas nenhuma rota lista em quais empresas a conta está habilitada. A tela de “habilitar por empresa” não consegue mostrar o estado atual além do que acabou de gravar (ausência de linha = habilitada).
Não há GET /number-sequences/{id}O Location do POST aponta para a listagem. A tela relê a lista.
Não há rota para listar o que uma pessoa concedeuPara decidir as linhas Restrict (Keep/End/Trim), a tela precisa do scopeId de cada uma, e o 409 USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT só lista pessoa, empresa e validTo em texto. O caminho é ler GET /users/{userId}/company-scopes dos beneficiários (as linhas trazem grantedByUserId, onGrantorChange e id) antes de montar o formulário de decisão.
Não há rota que liste quem alcançava todas as empresasA spec pede, na tela de criação de empresa, a lista de quem tinha escopo completo para concessão explícita (RN-EMP-27). O cálculo existe em processo (FullScopeService) e não tem rota nem chamador.
Não há rota para escrever a preferência de estabelecimento padrãoA tela escreve na rota de preferências da plataforma, com a chave nexio.default-establishment-id.
Eventos não são entreguesNenhum outro contexto reage ainda a desativação, troca de matriz, situação cadastral ou revogação de escopo. A tela não deve depender de efeito derivado desses eventos.
Pontos de entrega ainda não existemMudar município, UF ou allowsReceiving nunca é recusado por pontos de entrega; o campo deliveryLocationInterstateNotes[] do PUT é aceito e hoje não tem efeito.
Situação cadastral não é consultada de verdadeSem provedor, registrationStatus nasce Null e só a correção manual a muda.
Conta inativa ou fora da vigência escolhida à mãoNão há código de erro no catálogo (DEC-CO-13-3); só o lookup a oculta. A tela deve usar sempre o lookup para escolher conta.

Eventos de integração e jobs

Eventos de integração (versão 1)
EventoQuandoCarga (campos)
establishment.deactivatedUm por estabelecimento desativado: na desativação individual e na cascata da empresa. Stream = a empresa.tenantId, companyId, establishmentId, byCompanyCascade, occurredAt
company.headquarters-changedTroca efetiva de matriz (PATCH .../headquarters); indicar a matriz atual não publica.tenantId, companyId, previousHeadquartersId, headquartersId, occurredAt
establishment.registration-status-changedSó quando a situação cadastral muda, por correção manual ou pela varredura.tenantId, companyId, establishmentId, previousStatus, previousStatusDate, status, statusDate, source, occurredAt. A nota do comprovante não vai na carga.
number-sequence.replacedSubstituição de configuração (publicada pelo ato de substituição, não pela rota). Stream = a configuração sucessora.replacedSequenceId, sequenceId, documentType, effectiveFrom
user-company-scope.grantedPor linha aberta, no PUT de escopo e no POST /companies. Stream = a linha.scopeId, userId, companyId, validFrom, validTo, origin, grantedByUserId
user-company-scope.revokedPor linha encerrada: no PUT, no evento de remoção e na varredura de remoção. Stream = a linha.scopeId, userId, companyId, validFrom, endedAt, origin, grantedByUserId, endedByUserId, cause, causingScopeId, removedAt. cause ∈ Act, GrantorChange, UserRemovedFromTenant; removedAt nulo = desconhecido.

Todos são gravados em nexio.outbox na mesma transação do fato, antes do último SaveChanges. Nada é entregue: não há consumidor nem AddOutboxWorker. O kind é o nome literal e a versão fica em coluna própria; stream = (tenant, aggregateId), sequência = próxima posição do stream.

Jobs (só o Worker executa; agendamentos em UTC)
JobPadrãoO que faz
organization.tenant-roster7 * * * *Seleciona os tenants que têm empresa e grava, por tenant, uma entrada recorrente de cada job abaixo ({jobId}:{tenant}). Tenant novo é vigiado em até uma hora.
organization.company-administrator-watch0 6 * * *Confere a invariante do administrador permanente em toda empresa, ativa ou inativa, e avisa pela cadeia de três degraus (linha e permissão → administradores do tenant → canal de suporte); repete a cada dia enquanto a invariante não se sustenta. Não grava no banco do Nexio.
organization.scope.removed-users-sweep30 3 * * *Encerra, como ato de sistema, as linhas alcançadas pela remoção da pessoa da organização (caminho 3 da RN-EMP-24); é a rede sob o evento de remoção em processo, uma vez ao dia.
organization.establishment-registration-sweep30 4 * * *Consulta o provedor de CNPJ por estabelecimento ativo de empresa ativa, do mais desatualizado ao teto da janela (Organization:RegistrySweep:CeilingPerWindow, padrão 500).

Invariantes

Regras de negócio

RN-EMP-01

Tenant sem empresa ativa está em onboarding

Estado explícito, não seed: a plataforma não conhece Company e não chama o Nexio ao criar tenant. Nesse estado nenhum documento é emitido e a única tela disponível é o cadastro da primeira empresa, com o CNPJ pedido ali — mesmo precedente da união vazia de escopos: o usuário não opera, e a interface diz por quê

RN-EMP-02

Toda empresa tem exatamente uma matriz

No máximo uma: unicidade de (companyId) entre os estabelecimentos com type = Headquarters — índice único filtrado é exemplo de implementação. Pelo menos uma: POST /companies exige a matriz inline e grava as duas na mesma transação; estabelecimento criado depois nasce Branch; o PUT ignora type; a única troca é o PATCH /companies/{id}/headquarters (RN-EMP-38). ESTABLISHMENT_HEADQUARTERS_REQUIRED_UNPROCESSABLE guarda o agregado

RN-EMP-03

Todo estabelecimento carrega a raiz da sua empresa

Validação no agregado, 422 ESTABLISHMENT_CNPJ_ROOT_MISMATCH_UNPROCESSABLE. No esquema, a garantia é chave estrangeira composta, não check entre tabelas: o estabelecimento guarda cnpjRoot derivado do cnpj e referencia (tenantId, companyId, cnpjRoot) em companies(tenantId, id, cnpjRoot), com unicidade desse trio do lado da empresa

RN-EMP-04

Empresa ativa não fica sem estabelecimento ativo

Na criação, pela matriz inline; depois, verificação na desativação do último estabelecimento ativo, 409. A desativação da empresa não passa por essa guarda: ela cascateia, e empresa inativa sem estabelecimento ativo é o estado correto

RN-EMP-05

Desativar não emite documento novo; documento em andamento conclui

A mesma regra para empresa, estabelecimento e ponto de entrega. Criar ou submeter documento novo — submeter rascunho inclui — em estabelecimento inativo ou de empresa inativa é recusado com 422 ESTABLISHMENT_INACTIVE_UNPROCESSABLE ou 422 COMPANY_INACTIVE_UNPROCESSABLE. Todo documento já submetido segue até o fim: aprovação, pedido, recebimento, nota, devolução e estorno. A desativação nunca é recusada por haver documento em aberto. A resolução do UserCompanyScope não lê isActive: nenhuma leitura de histórico depende do estado

RN-EMP-06

A fatia de rateio usa a empresa do documento

A fatia não tem coluna de empresa. A invariante do centro de custo e a resolução do Budget leem a empresa do documento. Não existe rateio entre empresas na v1

RN-EMP-07

cnpjRoot, code e companyId são imutáveis

Ignorados no PUT. Não é erro pedir para mudar — é campo que não existe na atualização

RN-EMP-08

companyId nunca vem do cliente nem do token

Derivado no servidor do establishmentId, depois de validado contra o conjunto resolvido do usuário — fora dele, 404. O token não traz empresa, e o cliente nunca a informa como autoridade: companyId em query de listagem só intersecta com o conjunto

RN-EMP-09

Documento aponta para estabelecimento e congela a empresa

Regra da linha do documento. Alteração de cadastro não reescreve documento emitido

RN-EMP-10

A política de aprovação segue o estabelecimento do documento

Critério Empresa em lista do Fluxo de Aprovação, avaliado sobre o companyId do documento. Quem opera a compra é usuário; não existe eixo de política pelo lado do comprador

RN-EMP-11

Linha da empresa A só usa centro de custo corporativo ou de A

Especificado no Centro de Custo, lido contra a empresa do documento e o companyId efetivo do nó

RN-EMP-12

Conta sem allowsPosting não recebe rateio

Validação na fatia, 422

RN-EMP-13

Fatia sem conta não submete

Validação na submissão do documento, 422 ALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE; nulo só em rascunho (RN-CC-29)

RN-EMP-14

UserCompanyScope só estreita, nunca concede

Desenho. A tabela não dá organização, papel nem permissão — o token do GrydAuth continua decidindo isso. Linha de quem saiu da organização é encerrada (RN-EMP-24)

RN-EMP-15

Ausência de linha = nenhuma empresa

Resolvedor. Falha fechada, qualquer que seja o número de empresas do tenant; não existe concessão "todas as empresas". Ver a tabela de resolução em Escopo e segurança

RN-EMP-16

Nenhuma tabela do gryd é alterada

Fronteira de camada. A plataforma não conhece Company, e não deve conhecer

RN-EMP-17

Mudar município, UF ou allowsReceiving do estabelecimento revalida todos os pontos de entrega dele

PUT /establishments/{id} que altera cityIbgeCode, UF ou allowsReceiving revalida, na mesma transação, todos os pontos de entrega do estabelecimento, os de endereço próprio incluídos. Ponto sem endereço próprio copia o stateCode daqui, e só nele a UF é recalculada; isInterstate é recalculado em todos. A nota de cada ponto que passa a ser interestadual vem no mesmo corpo, em deliveryLocationInterstateNotes[] (deliveryLocationId + interstateNote); nota para ponto que não passa a interestadual é ignorada. A edição é recusada com 422 ESTABLISHMENT_ADDRESS_CHANGE_BREAKS_DELIVERY_LOCATIONS_UNPROCESSABLE, listando os pontos, quando algum ponto fica interestadual sem explicação — sem nota, fora de canteiro e sem recebedor distinto, ou canteiro sem centro de custo — ou quando allowsReceiving passa a falso com ponto ativo pendurado, que ficaria apontando para estabelecimento que deixou de receber: desative os pontos antes, o que nunca é recusado por documento (RN-DL-15). Passando, cada ponto afetado sobe a version e emite um evento. Escrita no Local de Entrega (RN-DL-19), com o mesmo gatilho e o mesmo código, aplicada aqui

RN-EMP-18

CNPJ é varchar nos dois lados do produto, normalizado antes de validar

Esquema: cnpjRoot varchar(8) e cnpj varchar(14), iguais aos do Fornecedor. Entrada: trim, sem máscara, letras em maiúsculas. Validação: 12 caracteres em 0–9A–Z e 2 dígitos verificadores, DV com ASCII − 48. Formato ou DV inválido é 400 ESTABLISHMENT_CNPJ_INVALID; raiz inválida, 400 COMPANY_CNPJ_ROOT_INVALID

RN-EMP-19

Concessão só dentro do escopo de quem concede

PUT /users/{userId}/company-scopes só abre ou encerra linha de empresa que quem concede alcança no momento do ato. Empresa fora desse conjunto, no corpo, responde 404 COMPANY_NOT_FOUND e nada é gravado. Linhas da pessoa em empresas que quem concede não alcança não são lidas, não aparecem no GET e não mudam. Consequência: um administrador de uma empresa não entrega outra. E ninguém mexe nas próprias linhas (RN-EMP-41).

RN-EMP-20

Empresa recém-criada é concedida só a quem a criou

POST /companies exige, além de create:companies, update:company-scopes no token de quem cria, conferida antes de gravar — sem ela, 403 COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN e nada é gravado. Com ela, grava na mesma transação uma linha de escopo para quem criou, com origin = CompanyCreation e sem validTo: a empresa nasce cumprindo a RN-EMP-24. Ninguém mais recebe a empresa — nem quem alcançava todas as outras. O evento é o mesmo de qualquer concessão (RN-EMP-21).

RN-EMP-21

Mudança de escopo é ato de controle

Toda linha aberta publica user-company-scope.granted e toda linha encerrada por ato ou pela remoção da pessoa da organização publica user-company-scope.revoked, depois do commit, com pessoa, empresa, vigência, origin e autor. O evento notifica, mede e integra; o efeito — a linha — já está gravado. Vencer pelo validTo informado na concessão não publica nada. Registro da trilha: o requisito é registro isolado por tenant, gravado na transação do ato; desde o Gryd.IO 11.1.0 ele é preparado por IAuditTrail.Stage junto com o evento. A própria linha, que não se apaga nem se reescreve, também guarda autor e instantes.

RN-EMP-22

Vigência sem sobreposição, e revogação na requisição seguinte

A linha vale em [validFrom, validTo), em instantes UTC. Para a mesma pessoa e empresa não há duas linhas com intervalos que se cruzem — 409 USER_COMPANY_SCOPE_DUPLICATE_CONFLICT. Encerrar grava validTo com o instante do ato, e a resolução seguinte já não a lê. Linha encerrada não reabre; reconceder é linha nova. validTo informado precisa ser posterior ao instante da concessão — 422 USER_COMPANY_SCOPE_VALIDITY_UNPROCESSABLE.

RN-EMP-23

PUT com lista vazia remove o acesso, e escrita concorrente não se mistura

Lista vazia encerra todas as linhas vigentes da pessoa nas empresas que quem concede alcança — com escopo em todas, a pessoa fica sem empresa nenhuma. A escrita é serializada em duas camadas, adquiridas sempre nesta ordem: primeiro por (tenantId, userId) de cada pessoa cuja linha o ato grava, em ordem crescente de userId; depois por (tenantId, companyId) de cada empresa que o ato toca, em ordem crescente de companyId. A ordem é total nas duas camadas, e é por isso que o ato que grava linha de várias pessoas de uma vez — a carga da migração — não trava contra outro igual: duas cargas com as mesmas pessoas em ordens diferentes ficariam uma esperando a outra para sempre. No PUT, as empresas tocadas são as do corpo e as das linhas vigentes da pessoa que quem concede alcança; os cortes e as decisões da RN-EMP-42 ficam na empresa da linha de origem e por isso já estão cobertos. Só depois de adquirir todas as travas, e sob elas, o ato lê a linha de quem concede, as linhas dependentes em cadeia e as linhas que sustentam a invariante da RN-EMP-24, e confere expectedScopeHash; divergente, 409 USER_COMPANY_SCOPE_CONCURRENT_CHANGE_CONFLICT e nada é gravado. A trava da empresa é tomada, na mesma ordem, por todo ato que grava linha ou confere a invariante nela: PATCH /companies/{id}/activate, o encerramento por remoção da organização e a gravação do ato de suporte no fim da carência (RN-EMP-24), e a carga da migração. É requisito, não recurso de banco: trava de linha numa tabela de controle por empresa é exemplo de implementação; o equivalente é qualquer serialização com as mesmas chaves e a mesma ordem de aquisição. Caso de ataque. Com a trava só da pessoa, U1 e U2 têm as duas únicas linhas sem fim na empresa X; dois PUT simultâneos, um sobre U1 e outro sobre U2, encerram uma linha cada, cada um enxerga a do outro ainda vigente, os dois passam na guarda, e X fica sem administrador. Ou Z encurta a linha de B, travando só B, enquanto B concede a D até depois do novo fim, travando só D: a cascata de Z não enxerga D. Com a trava de X, o segundo ato espera o primeiro e lê o resultado dele. E, sem a trava da pessoa, "remover tudo" concorrente com "acrescentar B" terminava com a pessoa em B.

RN-EMP-24

Toda empresa tem um administrador permanente — e a recuperação é do suporte

Invariante: toda empresa, ativa ou inativa, tem ao menos uma linha vigente sem validTo de pessoa ativa que tem update:company-scopes no GrydAuth. Ativa é quem tem vínculo ativo com a organização e não está bloqueada: linha de pessoa bloqueada ou removida não sustenta a invariante. Linha temporária nunca conta, por mais longa que seja: sem isso, A (sem fim) concede a C até 30/09, C encerra a linha de A, e em 30/09 a empresa fica sem ninguém. A invariante é conferida sob as travas da RN-EMP-23 — a de cada empresa tocada incluída —, com a permissão e o vínculo lidos no GrydAuth no instante do ato, em dois atos: no PUT /users/{userId}/company-scopes, recusado quando a empresa — ativa ou inativa — a cumpria antes e deixa de cumprir depois — encerrar, ou pôr validTo em, a última linha sem fim de quem tem a permissão, diretamente ou pelo corte em cadeia da RN-EMP-42 —; e no PATCH /companies/{id}/activate, recusado quando a empresa a reativar não a cumpre — só linhas temporárias, ou nenhuma linha sem fim de quem tem a permissão. Em qualquer caso, o ato que deixaria empresa, ativa ou inativa, sem nenhuma linha vigente, de ninguém, também é recusado. Todos respondem 409 USER_COMPANY_SCOPE_LAST_GRANT_CONFLICT, listando as empresas: conceda antes linha sem fim a outra pessoa com a permissão. A empresa inativa entra na guarda do PUT porque, fora dela, quem tem update:companies e só linha temporária desativaria a empresa, encerraria a última linha sem fim e deixaria a reativação em 409 para sempre — quem sobra não concede linha sem fim (RN-EMP-42). A guarda não segura o GrydAuth: a pessoa da linha sem fim pode sair da organização ou perder a permissão depois. Uma rotina diária — job com o tenant no payload, que não age em nome de ninguém — confere a invariante em toda empresa, ativa ou inativa, e, quando ela não se sustenta, avisa quem tem linha vigente e update:company-scopes na empresa, com destaque quando todas essas linhas terminam em até 7 dias; sem destinatário com linha e permissão, o aviso desce uma cadeia declarada, e só se cai num degrau quando o anterior não tem ninguém ativo: (1) quem tem linha vigente e update:company-scopes na empresa; (2) os administradores do tenant no GrydAuth — os membros elegíveis (vínculo ativo e dentro do período, conta ativa) que têm o papel Admin; o papel GroupAdmin não conta; (3) o canal de suporte da plataforma, que só recebe o aviso — com as empresas nomeadas e a lista das Restrict pendentes — e não abre o ato de suporte: o ato continua sendo pedido por um administrador do tenant, com confirmação no e-mail já cadastrado dele (adiante). O último degrau não depende de ninguém do tenant, e é por isso que a cadeia nunca termina sem destinatário — mas avisar não é conceder. Risco aceito, declarado aqui: tenant sem administrador ativo no GrydAuth (ninguém elegível com o papel Admin) não tem caminho pelo produto. A empresa segue sem administrador e as Restrict pendentes continuam órfãs até que o cliente devolva o papel Admin a alguém no GrydAuth, e nenhuma regra desta página compensa isso: abrir a porta pelo suporte recriaria a engenharia social que a exigência de quem pede fecha — recuperar superadministrador no Google Workspace e no Microsoft 365 exige outro superadministrador, e o suporte não concede escopo. A mitigação é o que se exige: o aviso é obrigatório, a lista das Restrict pendentes vai em todos os degraus e a rotina diária o repete enquanto a invariante não se sustentar, de modo que o estado fica visível em vez de silencioso. Quem é o administrador do tenant é leitura em processo do GrydAuth embutido, e o endereço do canal de suporte é configuração (Platform:SupportChannel); esta página declara a ordem e o que vai em cada degrau. Enquanto a invariante não se sustenta, o PUT que não a piora continua aceito, com uma exceção: encerrar ou pôr validTo na última linha sem fim da empresa, de quem quer que seja que continue na organização — inclusive a de quem perdeu a permissão ou está bloqueado —, é recusado com o mesmo 409, porque é por essa linha que a invariante volta. Tirar o acesso dessa pessoa não espera por isso: basta retirar os papéis dela, ou bloqueá-la, no GrydAuth, já que escopo sem permissão não dá acesso a nada. A invariante volta por dentro do produto só se o GrydAuth devolver a permissão, ou desbloquear, quem tem linha sem fim: quem só tem linha temporária não concede linha sem fim (RN-EMP-42). Fora disso, só o suporte da plataforma concede, fora da API de negócio, como ato de controle, com o mesmo evento e registro, e nunca para o próprio operador. O ato de suporte só é pedido por um administrador do tenant (papel Admin no GrydAuth), com confirmação no e-mail já cadastrado dele; todos que têm update:company-scopes no tenant recebem aviso; e a concessão só vale depois de uma carência de 24 h sem contestação. O beneficiário tem update:company-scopes, tem identidade verificada e, havendo outra pessoa ativa na organização com essa permissão, não pode ser quem pede; só sem ninguém mais quem pede é o beneficiário. A conta é por pessoa, não por usuário: "outra pessoa ativa com update:company-scopes" conta por CPF verificado — dois usuários do mesmo CPF são uma pessoa só e não fazem número —, e o CPF do beneficiário tem de ser diferente do CPF de quem pede. Beneficiário sem identidade verificada, ou com o CPF de quem pede, é recusado com 422 COMPANY_SCOPE_SUPPORT_GRANT_BENEFICIARY_UNPROCESSABLE, no pedido e de novo no fim da carência. Sem isso, quem pede cadastrava um segundo usuário com o próprio CPF, ele fazia número na conta e era o beneficiário do próprio ato de suporte. Nessa conta, quem teve update:company-scopes retirada nos 30 dias anteriores ao pedido conta como se ainda a tivesse, e recebe o aviso do pedido: sem isso, quem pede retiraria a permissão dos outros e passaria a ser o único. Retirada é a perda da permissão efetiva, por qualquer via — atribuição direta removida da pessoa, papel que a carrega retirado da pessoa, permissão retirada do papel, papel excluído: o que conta é a pessoa ter deixado de resolver update:company-scopes no GrydAuth, não o caminho pelo qual deixou. Sem essa amplitude, bastaria a quem pede tirar a permissão do papel, ou excluir o papel, para ninguém aparecer como retirado nos 30 dias e ele ficar sozinho na conta. Bloquear ou remover os outros da organização não entra nessa conta — risco aceito; o aviso e a carência continuam valendo. A linha nasce sem validTo e é gravada no fim da carência — o validFrom é esse instante, não o do pedido —, com as condições conferidas de novo nesse instante: se deixaram de valer, nada é gravado. Essas condições são regra da demanda de plataforma do Gryd.IO (ato de suporte "conceder escopo em empresa sem administrador"), e esta página só as cita. Não existe autoconcessão no produto.

Remoção da organização encerra; readmissão não revive. Quando a pessoa sai da organização no GrydAuth, as linhas alcançadas pela remoção são encerradas pelo primeiro de três caminhos. Alcançada se decide pelo vínculo da pessoa no instante em que o caminho roda, não pelo instante da remoção. Pessoa sem vínculo ativo com a organização: toda linha vigente dela é alcançada, qualquer que seja o validFrom. Pessoa com vínculo ativo, porque foi readmitida: alcançada é a linha vigente cujo validFrom é anterior ao início do vínculo vigente. Linha concedida depois da readmissão nunca é alcançada; linha anterior à remoção continua alcançada mesmo que a pessoa já tenha voltado. O instante da remoção informado pela plataforma fica no registro do encerramento e não recorta mais o alcance: linha aberta depois dele, na janela em que o Nexio ainda não sabia da saída, é alcançada como qualquer outra. Os três caminhos: (1) quem tem update:company-scopes trata a remoção pelo PUT /users/{userId}/company-scopes com lista vazia — o único PUT aceito sobre quem não está na organização; com empresa no corpo é 422 USER_NOT_IN_TENANT_UNPROCESSABLE — e decide no próprio ato as dependentes Restrict (RN-EMP-42); (2) o consumo do evento de remoção publicado pela plataforma; (3) a rotina diária, que procura linha alcançada em toda pessoa com linha vigente — a pessoa sem vínculo ativo e a pessoa com vínculo ativo que começou depois do validFrom da linha —, e não só quem está sem vínculo ativo. Em (2) e (3) o encerramento é de sistema: endedAt com o instante, endedByUserId nulo, user-company-scope.revoked com cause = UserRemovedFromTenant; as dependentes Cascade caem em cadeia e as Restrict ficam, com aviso a quem tem linha vigente e update:company-scopes na empresa listando-as para decisão. Sem destinatário com linha e permissão — o caso em que quem sai era o único administrador da empresa —, o aviso desce a mesma cadeia declarada do aviso da invariante — administradores do tenant e canal de suporte da plataforma —, e a lista das Restrict pendentes vai em todos os degraus, o último incluído — sem ele, quando quem saiu era também o administrador do tenant, ninguém ficaria sabendo que elas existem. O canal de suporte não decide nenhuma delas nem abre o ato de suporte: enquanto não houver administrador do tenant ativo para pedi-lo, as Restrict seguem pendentes e a empresa, sem administrador — é o risco aceito da cadeia, escrito na invariante. Enquanto houver Restrict pendente de decisão, a lista delas é repetida no aviso da rotina diária e no aviso do pedido do ato de suporte daquela empresa. Nos três caminhos o ato roda sob as travas da RN-EMP-23 e fora da guarda desta regra — linha de quem saiu não sustenta nada —; a empresa que ficar sem administrador entra no aviso da rotina e no ato de suporte. Readmitir a pessoa não revive escopo: a resolução não lê linha com validFrom anterior ao início do vínculo vigente da pessoa com a organização — a linha vale como encerrada até que um dos três caminhos a encerre —, e voltar a operar exige concessão nova. E o PUT não a revive: todo PUT /users/{userId}/company-scopes sobre pessoa readmitida encerra primeiro, no mesmo ato e como ato de sistema — endedAt com o instante, endedByUserId nulo, cause = UserRemovedFromTenant, Cascade em cadeia, Restrict mantidas com aviso —, as linhas alcançadas que ainda estiverem abertas, e só então compara o corpo com as linhas vigentes. Sem essa ordem, a empresa no corpo casaria com a linha antiga, que seria mantida, com o grantedByUserId e o validFrom de antes, em vez de concedida de novo por quem está gravando. Caso de ataque. Sem isso, a linha de quem saiu ficava vigente e inerte, e a readmissão meses depois, por engano ou de propósito, devolvia a empresa sem que ninguém concedesse nada. E sem o recorte por validFrom: a pessoa sai, é readmitida no mesmo dia e recebe linha nova, sem fim, na empresa X; o evento de remoção, atrasado, chega depois e encerra a linha nova — X fica sem administrador —, enquanto a rotina, que procurava só quem está sem vínculo ativo, nunca chega à linha antiga. E sem o corte pelo vínculo: a pessoa é removida no GrydAuth às 10h, o evento chega ao Nexio às 14h e, às 11h, quem ainda lia o vínculo antigo lhe concede a empresa Y — linha com validFrom posterior ao instante da remoção. Com o corte por esse instante, essa linha não era alcançada por caminho nenhum: a rotina não a achava, o PUT da readmissão não a encerrava, e a resolução a ignorava para sempre, por ser anterior ao início de qualquer vínculo futuro. Readmitida a pessoa, a linha — vigente, sem validTo, de pessoa de novo ativa com a permissão — passava a sustentar a invariante desta regra sem dar acesso a nada: um administrador fantasma. E o PUT normal nunca mais concedia Y àquela pessoa, porque a empresa no corpo casava com a linha morta e ela era mantida em vez de concedida de novo. Sem vínculo ativo, toda linha vigente é alcançada, e essa é encerrada com as demais.

RN-EMP-25

A resolução falha fechada, e o filtro não esquece entidade

Sem contexto de tenant ou com a leitura do escopo indisponível, a requisição falha com 500; não existe caminho que siga com conjunto mais amplo, com o conjunto de outra requisição ou sem filtro. Toda entidade com companyId ou establishmentId é registrada no filtro de escopo, e um teste de arquitetura quebra o build quando uma entidade nova com uma dessas colunas não está registrada. Critério de aceite: requisição sem linha vigente devolve lista vazia em toda rota de listagem de agregado por empresa, e 404 em toda leitura por id desses agregados.

RN-EMP-26

Escrita resolve exatamente uma empresa

Todo comando que grava dado por empresa resolve uma empresa — a do establishmentId validado, ou a do registro já existente — e confere que ela está no conjunto; nenhum comando recebe conjunto de empresas nem "todas". Leitura pode abranger várias empresas do conjunto; escrita, nunca. É o espelho do que a plataforma faz ao recusar escrita sob leitura de grupo.

RN-EMP-27

"Escopo completo" é linha vigente em todas as empresas ativas, no momento da verificação

Onde outra regra exige que a pessoa alcance todas as empresas — responsável de nó corporativo no Centro de Custo, aprovador de reserva e alvo de política no Fluxo de Aprovação —, a condição é ter linha vigente em cada empresa ativa do tenant no instante em que a regra é verificada. Não existe marca "irrestrito". Criar empresa tira essa condição de todos, menos de quem criou; por isso a tela de criação lista quem a tinha, com os vínculos de nó corporativo e as reservas afetados, para concessão explícita. Na execução, a defesa em profundidade do Fluxo continua valendo para quem ficou sem a empresa do documento.

RN-EMP-28

Uma configuração de numeração vigente por tipo de documento

Para cada (tenant, documentType) as vigências [effectiveFrom, retiredAt) das configurações não se sobrepõem — requisito de exclusão de faixas, a mesma mecânica do CostCenterAssignment. Criar configuração havendo vigente ou agendada, sem replacesSequenceId, é 409 NUMBER_SEQUENCE_ALREADY_EXISTS. A emissão usa a configuração vigente no instante da emissão.

RN-EMP-29

Troca de numeração é substituição, com prefixo novo ou período novo

scope, prefix, resetPolicy e timeZone não se editam: mudar qualquer um é criar a sucessora com replacesSequenceId e o expectedVersion da mais recente, e a substituída recebe retiredAt = effectiveFrom da sucessora na mesma transação. A sucessora é aceita só com prefixo novo — nunca usado por configuração daquele documentType no tenant — ou período novo: mesma resetPolicy (diferente de Never) e mesmo timeZone da substituída, e effectiveFrom exatamente no início de um período posterior ao corrente, nesse fuso, e posterior ao maior período já emitido por qualquer configuração daquele documentType com o mesmo prefixo e a mesma resetPolicy — o maior currentPeriodKey dos contadores de todas elas, não só os da substituída. Sem isso, a troca de fuso recompõe número: PC- em UTC+14 emite PC-2027-000001 quando São Paulo ainda está em 2026; a troca para PD- em UTC−11 é prefixo novo; a volta a PC- em UTC−11, no início de 2027 nesse fuso, seria período novo contra PD- e emitiria PC-2027-000001 de novo, recusado pela unicidade da RN-EMP-32 com 409 a cada emissão, em série. Configuração com a mesma resetPolicy e prefixo diferente, ou com o mesmo prefixo e outra resetPolicy, compõe número de outro formato e não entra na conta. Fora disso, 422 NUMBER_SEQUENCE_REPLACEMENT_UNPROCESSABLE; versão ou alvo divergente, 409 NUMBER_SEQUENCE_CONCURRENT_CHANGE_CONFLICT. Nos dois caminhos, o prefixo da sucessora, quando difere do de uma configuração já criada para o documentType no tenant, não pode começar com ele nem ser começo dele — senão 422 NUMBER_SEQUENCE_PREFIX_OVERLAP_UNPROCESSABLE, nomeando a configuração anterior, na criação da configuração e não na emissão. É o que impede recompor número já emitido: S1 com PC- em Establishment emite PC-SP01-2026-000001, e S2 com PC-SP01- em Company emitiria o mesmo número. Dois prefixos em que nenhum é começo do outro nunca produzem o mesmo número. A regra é a mesma em qualquer troca, sem simular formato de número. Publica number-sequence.replaced depois do commit.

RN-EMP-30

O escopo particiona contadores, e o contador nasce na primeira emissão

Tenant tem um contador; Company, um por empresa; Establishment, um por estabelecimento. O contador é criado na primeira emissão da partição, sob a unicidade (sequenceId, partitionKey), e nunca é compartilhado entre configurações.

RN-EMP-31

O período só avança, pelo relógio do servidor no fuso da configuração

O período de cada emissão é calculado pelo relógio do servidor no timeZone da configuração — nunca em UTC, nunca pela data do documento. Maior que o guardado: grava e zera. Igual ou menor — relógio atrasado de outro servidor —: usa o guardado e incrementa. Na mesma operação atômica, sempre. Documento retroativo recebe o período da emissão.

RN-EMP-32

Número emitido é único por empresa

Unicidade de (tenantId, documentType, companyId, number) no documento, em qualquer escopo — é o que garante a composição com o código do estabelecimento e o que pega colisão residual depois de uma troca. Colisão recusa a emissão com 409 DOCUMENT_NUMBER_ALREADY_EXISTS, sem gravar; o número consumido não volta e repetir emite o próximo. Lacunas são permitidas: o número não é fiscal, e a emissão confirma em transação própria antes do documento, então número consumido por documento que falhou não é reaproveitado.

RN-EMP-33

Cada eixo do documento tem o seu filtro

establishmentId exige estabelecimento ativo, de empresa ativa, com allowsReceiving — senão 422 ESTABLISHMENT_INACTIVE_UNPROCESSABLE, 422 COMPANY_INACTIVE_UNPROCESSABLE ou 422 ESTABLISHMENT_RECEIVING_NOT_ALLOWED_UNPROCESSABLE. requestingEstablishmentId exige estabelecimento ativo com allowsPurchasing — senão 422 ESTABLISHMENT_PURCHASING_NOT_ALLOWED_UNPROCESSABLE. Verificado na criação e na submissão do documento novo; documento já submetido não é reavaliado (RN-EMP-05). O lookup aplica o mesmo filtro pelo purpose.

RN-EMP-34

Quem pede é obrigatório e da mesma empresa de quem recebe

requestingEstablishmentId nunca é nulo: quando quem pede é quem recebe, é igual ao establishmentId. Estabelecimento de outra empresa é recusado com 422 REQUESTING_ESTABLISHMENT_COMPANY_MISMATCH_UNPROCESSABLE. Não existe rateio nem pedido entre empresas na v1; como demanda de várias empresas se consolida é decisão da spec da Cotação, dentro da RN-EMP-26 e desta regra.

RN-EMP-35

Moeda funcional é BRL na v1

Company.functionalCurrency nasce BRL e não muda: outro valor na criação é 422 COMPANY_FUNCTIONAL_CURRENCY_UNPROCESSABLE, e o PUT ignora o campo. Documentos, preços, limites e cotações continuam multimoeda, com conversão para a moeda do limite na submissão.

RN-EMP-36

Estabelecimento em situação irregular não emite documento novo

Criar ou submeter documento novo com estabelecimento — como establishmentId ou requestingEstablishmentId — em Suspended, Unfit ou ClosedDown é 422 ESTABLISHMENT_REGISTRATION_STATUS_UNPROCESSABLE. Null não bloqueia: é o estado de estabelecimento recém-cadastrado que o espelho da Receita ainda não traz, e o risco aqui é fiscal, não de fraude — a empresa não ganha nada mentindo sobre o próprio CNPJ. A situação vem do provedor de consulta ao CNPJ ou de PATCH /establishments/{id}/registration-status, que exige data da situação e nota do comprovante; entre os dois vale a data de situação mais recente, e a consulta seguinte com data posterior substitui a correção manual. Toda mudança publica establishment.registration-status-changed. Documento já submetido não é reavaliado (RN-EMP-05).

RN-EMP-37

O documento congela os dados fiscais de quem recebe

Na submissão, o documento congela taxRegime da empresa e icmsTaxpayerStatus, stateRegistration e suframaCode do estabelecimento. Mudança de cadastro vale para documento novo e nunca reescreve documento submetido — o Simples é opção anual, e a nota de um pedido de dezembro não muda de tratamento porque o regime mudou em janeiro.

RN-EMP-38

Trocar a matriz é um ato só

PATCH /companies/{id}/headquarters com { establishmentId, expectedVersion }: na mesma transação, a matriz atual vira Branch, o estabelecimento indicado vira Headquarters e headquartersId é regravado — a empresa nunca passa por "sem matriz" nem por "duas matrizes". Estabelecimento de outra empresa ou fora do escopo é 404 ESTABLISHMENT_NOT_FOUND; inativo, 422 ESTABLISHMENT_INACTIVE_UNPROCESSABLE; versão divergente, 409 COMPANY_CONCURRENT_CHANGE_CONFLICT; indicar a matriz atual devolve 200 sem mudança. Publica company.headquarters-changed. Documento emitido não muda: ele aponta para estabelecimento, não para "a matriz".

RN-EMP-39

O código enviado ao ERP tem ordem de resolução

Para a conta de uma fatia, o código externo é GLAccountCompany.externalCode da empresa do documento; ausente a linha ou o código, GLAccount.externalCode. Sem nenhum dos dois, o movimento não sai para o ERP e fica visível como aguardando código externo, com a conta e a empresa nomeadas — nunca descartado e nunca enviado sem código. Preencher o código libera o envio.

RN-EMP-40

Escrita em empresa e estabelecimento exige versão

Company e Establishment têm version int. PUT, /activate, /deactivate, /headquarters e /registration-status exigem expectedVersion — divergente, 409 COMPANY_CONCURRENT_CHANGE_CONFLICT ou 409 ESTABLISHMENT_CONCURRENT_CHANGE_CONFLICT, sem gravar nada. É o mesmo token do plano de contas, do Centro de Custo, do Item e do Fornecedor.

RN-EMP-41

Ninguém mexe nas próprias linhas de escopo

Quem concede não abre, não encerra e não altera a vigência das próprias linhas de UserCompanyScope — nem para ampliar validTo, nem para repetir o que já tem. PUT /users/{userId}/company-scopes com o userId de quem concede é recusado com 403 USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN, com ou sem mudança no corpo, antes de qualquer outra guarda. As únicas linhas que nascem para a própria pessoa são as de sistema: a de quem cria a empresa (RN-EMP-20) e a do suporte da plataforma (RN-EMP-24). Sem esta regra, um administrador com acesso temporário a uma empresa estenderia o próprio prazo sem ninguém ver.

RN-EMP-42

A concessão não passa da vigência de quem concede

"Só se concede o que está no próprio escopo" vale também no tempo. Na empresa em que a linha vigente de quem concede tem validTo, toda linha que o PUT /users/{userId}/company-scopes abre ou reabre para outra pessoa precisa de validTo menor ou igual ao dela; sem validTo, ou com validTo posterior, é 422 USER_COMPANY_SCOPE_BEYOND_GRANTOR_UNPROCESSABLE, nomeando a empresa, e nada é gravado. Quem concede com linha sem fim concede qualquer vigência. A comparação usa a linha de quem concede no instante do ato, e a regra continua valendo depois dele pelo onGrantorChange, gravado na concessão: Cascade quando a linha de quem concede tem validTo, Restrict quando não tem. (1) Cascade: sempre que o fim da vigência de quem concedeu, naquela empresa, diminui — validTo encurtado ou linha encerrada —, a linha dependente cujo validTo passa do novo fim é cortada no mesmo ato e na mesma transação: encerrada, se o novo fim é o instante do ato; encerrada e reaberta com o novo fim, se ele é futuro, conservando grantedByUserId e onGrantorChange. O corte segue em cadeia pelas linhas que ela concedeu, alcança inclusive a linha de quem faz o ato, e cada linha cortada publica user-company-scope.revoked com cause = GrantorChange e a linha de origem. (2) Restrict: o PUT que encerra ou encurta a linha de quem concedeu exige, em dependentGrantDecisions, uma decisão — Keep, End ou Trim — para cada linha Restrict que o novo fim alcança, também em cadeia (as dependentes de uma linha encerrada por End, cortada por Trim ou cortada por Cascade entram na conta). Trim corta a linha até o novo fim de quem concedeu: encerrada e reaberta com esse fim, conservando grantedByUserId e onGrantorChange; quando o novo fim é o instante do ato, vale como End. Cada linha entra uma vez: linha que o ato já alcançou — a encerrada ou encurtada pelo corpo, uma dependente já listada, decidida ou cortada — não volta à lista nem é cortada de novo, e a cadeia para nela; é o que fecha o ciclo em que A concede a B e B reabre a linha de A. Faltando decisão, ou sobrando, 409 USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT com a lista, e nada é gravado. A própria linha de quem faz o ato só admite Keep; End ou Trim nela é 403 USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN (RN-EMP-41). Aumentar a vigência, ou encerrar e reabrir sem encurtar, não corta nada. A guarda da RN-EMP-24 é conferida sobre o resultado do ato inteiro, cortes incluídos. Exemplo. A tem linha sem fim na empresa X e concede a B até 30/09 (Restrict: a linha de A não tem fim). B concede a C até 20/09 (Cascade: a de B tem). Em 10/09, Z encurta B para 15/09: C é cortada para 15/09 no mesmo ato, sem pergunta — sem o corte, C, ainda com linha até 20/09, devolveria a B o acesso até 20/09. Em 12/09, Z encerra a linha de A: o PUT sem decisão volta 409 USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT listando B; com End, B é encerrada e C cai junto, por Cascade; com Keep, B e C seguem até 15/09. Se, em vez de encerrar, Z encurtasse A para 14/09, a lista seria a mesma, e Trim levaria B a 14/09 e C, por Cascade, também. Ciclo: A concede a B, e B reabre a linha de A, que passa a ter B como quem concedeu; Z encerra A e decide End em B — a linha de A, dependente de B, já foi alcançada pelo ato e não volta à lista. Sem esta regra, um acesso temporário viraria permanente por interposta pessoa, e revogar alguém não revogaria o que ele espalhou.

Implantação

Migração

Sem cliente em produção e com o único módulo desenvolvido — centro de custo — autorizado a ser recriado. Esta é a janela em que a mudança custa quase nada.

1 · Onboarding no lugar do seed

Não se semeia Company a partir do tenant: a plataforma não conhece Company, nada no Gryd.IO chama o Nexio ao criar um tenant, e TenantSettings não existe nela. O tenant novo nasce sem empresa e isso é um estado explícito de onboarding — não emite documento, e a única tela disponível é o cadastro da primeira empresa, que pede o CNPJ ali e cria Company e Establishment Headquarters numa chamada só (POST /companies com a matriz inline). Não há tenant em produção: para os que existirem antes deste release, o mesmo onboarding serve, sem migrar dado.

2 · O CNPJ solto do tenant sai no mesmo release

O que o Nexio tinha de CNPJ único por tenant — do lado do Nexio, nunca da plataforma — sai junto com a entrada de Company. Manter os dois por um ciclo cria a segunda fonte de verdade que a Decisão 2 existe para impedir. Sai junto ou não sai.

3 · Renomes sem dado

cost_centers.legal_entity_id → company_id. Nenhuma tabela do gryd é tocada. Nasce uma tabela no esquema do Nexio, user_company_scopes, vazia — e vazia significa "ninguém alcança empresa nenhuma". ApprovalPolicy também não ganha coluna: o critério resolve. Nenhuma coluna existente precisa de backfill — a renomeada continua anulável, porque nulo é nó corporativo, e a tabela nova nasce vazia —, e não há janela.

4 · Tenant existente recebe escopo explícito, nunca implícito

Não há cliente em produção. Para tenant que já exista antes deste release, a migração não concede nada sozinha: a carga é um ato do suporte da plataforma, fora da API de negócio — por isso fora da RN-EMP-19 e da RN-EMP-41, que regem o PUT do tenant —, e toma do ato de suporte da RN-EMP-24 o pedido de um administrador do tenant, o aviso e a carência, e nada além deles: a lista pessoa × empresa só é pedida por um administrador do tenant no GrydAuth, com confirmação no e-mail já cadastrado dele; todos que têm update:company-scopes no tenant recebem aviso com a lista; e ela só é gravada no fim da carência de 24 h sem contestação, com as condições conferidas de novo nesse instante. Quem pede só entra na lista se ninguém mais ativo na organização tiver update:company-scopes, contando quem a teve retirada nos 30 dias anteriores ao pedido (RN-EMP-24); havendo, ele recebe as próprias linhas pela tela, de quem recebeu linha na carga. Identidade verificada só onde há risco de empresa órfã: a carga não exige CPF verificado de cada pessoa da lista — exige identidade verificada de quem recebe linha em empresa que, sem essa linha, ficaria sem nenhuma linha sem fim de quem tem update:company-scopes. Para as demais pessoas da lista valem o pedido de quem pede e a reconferência do fim da carência, e nada mais. Pessoa nessa condição sem identidade verificada é recusada com 422 COMPANY_SCOPE_SUPPORT_GRANT_BENEFICIARY_UNPROCESSABLE, nomeando a pessoa e a empresa, no pedido e de novo no fim da carência. A exigência acompanha o risco que a guarda do beneficiário protege: remetida às condições inteiras do ato de suporte, ela alcançaria cada pessoa da carga e nenhum tenant migraria antes de a plataforma entregar a identidade verificada. A carga grava uma linha por par, com origin = Grant, grantedByUserId = o operador de suporte, sem validTo e sem onGrantorChange, publicando user-company-scope.granted para cada linha — é ato de controle como qualquer concessão (RN-EMP-21). O operador não recebe linha: a carga recusa lista que o inclua. E recusa a lista inteira se ela deixar empresa, ativa ou inativa, sem linha sem fim de quem tem update:company-scopes (RN-EMP-24). Quem cadastrou a empresa no onboarding já tem a própria linha (RN-EMP-20); quem não está na lista não opera até ser concedido pela tela. Com uma empresa só, o seletor não aparece e a numeração continua única.

Validação

Referência de mercado

NexioOracle FusionSAPCoupaProtheus
TenantEnterpriseClientInstanceAmbiente
CompanyLegal EntityCompany CodeLegal EntityEmpresa
EstablishmentNão como site — o site não tem identificação fiscal própria. O que existe é a Legal Reporting Unit, subdivisão da entidade legal que carrega registros fiscais; usá-la por filial resolve o CNPJ e cobra o preço de modelar cada filial na estrutura legalBusiness Place · Local de Negócio, construto criado para o Brasilnão existeFilial
GLAccountNatural AccountGL AccountAccountPlano de contas
NumberSequenceDocument SequenceNumber Range—Numeração por filial

A linha que importa é a do estabelecimento. Oracle e Coupa não têm o conceito: para eles o site é uma unidade operacional sem CNPJ, e o identificador fiscal mora na entidade legal. Quem implanta esses produtos no Brasil resolve com campo livre, código de site colado no CNPJ ou uma entidade legal por filial — as três saídas ruins. A SAP precisou inventar o Business Place especificamente para a legislação brasileira. O Nexio nasce com o construto certo porque nasce aqui — e é exatamente a mesma vantagem que a spec de Fornecedor identificou ao derivar grupo econômico da raiz de 8.

Limites

Fora de escopo

O quePor quê
Escrituração contábilO Nexio classifica o gasto; partida dobrada, razão e balancete continuam no ERP
Rateio, pedido e transferência entre empresasFora da v1. Quem pede e quem recebe são da mesma empresa (RN-EMP-34). Como demanda de várias empresas se consolida é decisão da spec da Cotação, que tem de respeitar a RN-EMP-26 e a RN-EMP-34. Acerto entre empresas é ERP
Consolidação cross-tenantSe um dia existir, é serviço de leitura com permissão própria — nunca um filtro de tenant relaxado
Herança de cadastro do tenant Group para os filhosContraria a Decisão 3. Quem quer cadastro compartilhado usa o modo consolidado
Hierarquia entre empresas do mesmo tenantHolding e controlada dentro do tenant: lista plana até que consolidação societária exista, o que não está no roteiro. parentCompanyId não é reservado
Estabelecimento fora do BrasilcountryCode reservado, sem regra. Fornecedor estrangeiro já é suportado; comprar como empresa estrangeira, não
Parametrização fiscal por operaçãoCFOP, CST, ST, benefícios e regra de crédito nascem com a nota fiscal
DeliveryLocation e BudgetSpecs próprias. As duas dependem desta e nenhuma das duas depende da outra

Registro

Histórico de revisões

VersãoDataO que mudou
v1.2330/10/2026Permissões padronizadas em cinco verbos (decisão do Roger, 30/10/2026): read, lookup, create, update e delete. De/para: manage:company-scopes → update:company-scopes; numeração, que só tinha manage:number-sequences (inclusive a leitura), passa a read:number-sequences (GET), create:number-sequences (POST) e update:number-sequences (PUT). Os seletores (GET /companies/lookup, GET /establishments/lookup, GET /gl-accounts/lookup) passam a exigir lookup:companies e lookup:gl-accounts; read e lookup não se implicam. Nenhuma regra de negócio muda.
v1.2226/10/2026Consulta de CNPJ, CEP e município para o cadastro de empresa e estabelecimento (pedido do mantenedor, contrato confirmado em 26/10). Nenhuma regra (RN-EMP-01 a RN-EMP-42) muda. Contrato HTTP: três rotas de leitura sob api/v1/nexio/registry — GET /registry/cnpj/{cnpj}, GET /registry/zip-codes/{zipCode} e GET /registry/municipalities —, todas com read:companies, sem recorte por empresa e sem gravar nada; catálogo de erros de 57 para 62 códigos (REGISTRY_CNPJ_INVALID, REGISTRY_ZIP_CODE_INVALID, REGISTRY_CNPJ_NOT_FOUND, REGISTRY_ZIP_CODE_NOT_FOUND, REGISTRY_LOOKUP_UNAVAILABLE). O backend consulta a BrasilAPI (CNPJ e CEP) e o ViaCEP (reserva do CEP); a origem é configuração (Registry:*). RN-EMP-36 / D-10: o provedor concreto de CNPJ passa a existir, mas a varredura diária só o usa com Registry:Cnpj:FeedRegistrationStatus = true (padrão false: a API pública tem limite de requisições). Contrato para o frontend: nova seção Preenchimento por consulta.
v1.2125/10/2026GrydAuth embutido (DEC-EMB-0, decisão do mantenedor de 25/10): na primeira versão o Nexio usa o Gryd.IO e o GrydAuth acoplados e não consome a autenticação de outro serviço. Regra alterada — RN-EMP-24: a cadeia de aviso passa de quatro para três degraus (quem tem linha e update:company-scopes na empresa → administradores do tenant → canal de suporte); sai o titular da organização, que o GrydAuth não tem (DEC-EMB-4), e o ato de suporte passa a ser pedido por um administrador do tenant (DEC-CO-36-1, a confirmar). As demais regras (RN-EMP-01 a RN-EMP-42) não mudam. Estado da implementação atualizado: autenticação embutida (Api emissor; Worker, Portal e Migrator leem em processo; banco nexio-identity); diretório, preferências e início do vínculo reais, de modo que a linha 5 da resolução passa a valer e o administrador fantasma da readmissão é provado contra o banco de identidade real; evento de remoção real (o ouvinte anota, o commit do Auth libera, e a varredura das 03:30 UTC, uma vez ao dia, é só a rede); cadeia de três degraus provada; somem os riscos de plataforma de início do vínculo, transporte do evento, administrador e titular, token de serviço por tenant, read:users e indisponibilidade do diretório. Contrato HTTP: PATCH /companies/{id}/activate e PUT /users/{userId}/company-scopes deixam de exigir read:users; USER_DIRECTORY_UNAVAILABLE (503) deixa de existir (catálogo de 57 códigos) e falha de leitura da identidade é 500. Contrato para o frontend: nova subseção Autenticação, com a nota de migração. Conferido em feature/organization-embedded-auth, sobre o Gryd.IO 10.0.0.
v1.2025/10/2026Implementação do backend entregue; contratos conferidos. Nenhuma regra (RN-EMP-01 a RN-EMP-42) nem decisão foi alterada: o texto da página fica, e o que o código acrescenta ou muda aparece em blocos Implementado:. Novas seções: Estado da implementação (código × spec, item a item, e as divergências da letra decididas pelo mantenedor) e Contrato para o frontend (convenções, listagens, as 29 rotas com corpo, retorno e erros, fluxos de tela, permissões que exibem ou ocultam, lacunas, eventos e jobs). Reconciliadas: Relação com os demais domínios (portas publicadas da CO-19), Endpoints, Contrato de erros (58 códigos; nove acrescentados) e Permissões. Conferido na branch feature/organization-company, HEAD 3872c20, sobre o Gryd.IO 10.0.0: listagens em PagedResult<T>, eventos enfileirados no outbox na mesma transação (worker desligado até haver consumidor).
v1.1920/09/2026Recuo da validação de 18/09 (achado 12). Sai desta página “a compra consolidada emite um pedido por empresa”, que colidia com a RN-EMP-26: como demanda de várias empresas se consolida, e o escopo de numeração da QuotationRequest, são decisões da spec da Cotação, que tem de respeitar a RN-EMP-26 e a RN-EMP-34. Nenhuma regra muda.
v1.1813/09/2026Correção da validação R7 — EV-35 (decisão do Roger, 12/09). O degrau 4 da cadeia de aviso só recebe o aviso: o canal de suporte da plataforma não abre mais o ato de suporte, que continua sendo pedido só pelo titular da organização no GrydAuth, com confirmação no e-mail dele (RN-EMP-24). Tenant sem titular ativo não tem caminho pelo produto, e isso fica escrito como risco aceito — a empresa segue sem administrador e as Restrict pendentes seguem órfãs até o cliente reativar um titular —, com a mitigação exigida: aviso obrigatório, lista das Restrict em todos os degraus e repetição pela rotina diária. Sem degrau novo e sem regra compensatória
v1.1712/09/2026Correção da validação R6. Alcance da remoção: "linha alcançada" passa a se decidir pelo vínculo — sem vínculo ativo, toda linha vigente é alcançada; com vínculo, a de validFrom anterior ao início dele —, e não mais pelo instante da remoção, que fechava a janela em que a linha concedida entre a saída no GrydAuth e a notícia dela virava administrador fantasma. Fallback do aviso: cadeia declarada em quatro degraus — quem tem update:company-scopes na empresa, administrador do tenant, titular da organização, canal de suporte da plataforma —, com a lista das Restrict pendentes em todos. Migração §4 (EV-34): identidade verificada exigida só de quem recebe escopo em empresa que ficaria sem administrador sem ele; sai a remissão às "condições inteiras" do ato de suporte
v1.1611/09/2026Correção da validação R5. Remoção × readmissão: a remoção encerra só a linha alcançada — validFrom anterior ao instante da remoção, ou ao início do vínculo vigente —, a rotina diária procura linha alcançada em vez de pessoa sem vínculo, e o PUT sobre pessoa readmitida encerra as alcançadas como ato de sistema antes de comparar o corpo (RN-EMP-24). Aviso das Restrict mantidas: sem destinatário com linha e permissão, vai ao administrador do tenant, e a lista pendente é repetida no aviso da rotina e no do ato de suporte. Retirada da permissão é a perda da permissão efetiva por qualquer via — atribuição, papel retirado da pessoa, permissão retirada do papel, papel excluído. Ato de suporte (EV-26): beneficiário com identidade verificada e CPF diferente do titular; "outra pessoa com update:company-scopes" conta por CPF; 422 COMPANY_SCOPE_SUPPORT_GRANT_BENEFICIARY_UNPROCESSABLE. Travas: ordem total também entre pessoas, por userId crescente (RN-EMP-23).
v1.1511/09/2026Correção da validação R4. Concorrência: a escrita de escopo é serializada também por (tenantId, companyId) de cada empresa tocada, em ordem de id, e a linha de quem concede, as dependentes e a invariante são lidas sob as travas (RN-EMP-23). Invariante: conta só pessoa ativa — vínculo ativo, não bloqueada — e vale para empresa ativa ou inativa, também na rotina diária (RN-EMP-24). Remoção da organização encerra as linhas — PUT com lista vazia sobre quem saiu, evento da plataforma ou rotina, cause = UserRemovedFromTenant, Restrict mantidas com aviso — e readmissão não revive escopo. Ato de suporte: retirada de update:company-scopes nos 30 dias anteriores ao pedido conta como se a pessoa ainda a tivesse; bloqueio e remoção não contam (risco aceito). Cascata: decisão Trim ao lado de Keep e End, e cada linha entra uma vez na lista (ciclo A ↔ B) (RN-EMP-42). Resíduos de data e de mudança saíram do corpo.
v1.1411/09/2026Correção da validação R3. A guarda do administrador permanente vale também para empresa inativa, e sem a invariante a última linha sem fim, de quem quer que seja, não se encerra (RN-EMP-24). Cascata de escopo: campo onGrantorChange (Cascade · Restrict); linha concedida por quem tinha linha temporária é cortada junto, em cadeia; concedida por quem tinha linha sem fim exige decisão no PUT (dependentGrantDecisions), senão 409 USER_COMPANY_SCOPE_DEPENDENT_GRANTS_CONFLICT (RN-EMP-42). Ato de suporte: beneficiário com a permissão, nunca o titular se houver outra pessoa com ela, linha gravada no fim da carência (RN-EMP-24, validFrom); carga da migração segue as mesmas condições. Numeração: período novo conferido contra toda configuração com o mesmo prefixo e a mesma resetPolicy (RN-EMP-29); padding de 1 a 21, 422 NUMBER_SEQUENCE_PADDING_UNPROCESSABLE. Datas saíram do corpo.
v1.1311/09/2026Correção da validação R2. Invariante do administrador permanente: empresa ativa tem sempre linha vigente sem validTo de quem tem update:company-scopes, conferida no PUT de escopo e no /activate; linha temporária não conta; aviso diário sem destinatário vai ao administrador do tenant (RN-EMP-24). Concessão não passa da vigência de quem concede (RN-EMP-42, USER_COMPANY_SCOPE_BEYOND_GRANTOR_UNPROCESSABLE). Prefixo de numeração passa a obrigatório e não pode sobrepor prefixo anterior (NUMBER_SEQUENCE_PREFIX_OVERLAP_UNPROCESSABLE, RN-EMP-29). Carga de escopo na migração é ato do suporte da plataforma, sem linha para o operador. Ato de suporte citado com quem pede, confirmação, aviso e carência. POST /companies exige também update:company-scopes (COMPANY_CREATION_SCOPE_PERMISSION_FORBIDDEN). allowsReceiving com o mesmo gatilho e o mesmo texto de erro do Local de Entrega
v1.1211/09/2026Correção pós-validação. Ninguém mexe nas próprias linhas de escopo (RN-EMP-41, USER_COMPANY_SCOPE_SELF_CHANGE_FORBIDDEN). Guarda da última linha vale também para validTo programado, com aviso diário de empresa que ficará sem escopo em 7 dias. Emissão do número em transação própria, antes do documento. Códigos para recusas que não tinham: COMPANY_CLOSED_UNPROCESSABLE, ESTABLISHMENT_LOOKUP_PURPOSE_REQUIRED e o código do Local de Entrega na edição do estabelecimento, com deliveryLocationInterstateNotes[] no corpo. Tabela de resolução alinhada à desativação. Migração de tenant existente passa a gravar escopo explícito por lista aprovada; saiu o resto "todos anuláveis" e a nota de que até a v1.3 a seção mandava semear Company a partir de TenantSettings.cnpj
v1.1110/09/2026Validação de 10/09. Regras numeradas RN-EMP-01 a RN-EMP-40. Escopo por empresa fecha por padrão: ausência de linha = nenhuma empresa, sem concessão "todas"; UserCompanyScope.companyId obrigatória, vigência em instantes, sai isActive e sai roleId, entram origin e autoria; concessão só dentro do escopo de quem concede, empresa nova só para quem a criou, guarda da última linha e recuperação pelo suporte da plataforma; tabela de decisão da resolução; reason em inglês; falha fechada e teste de arquitetura; sai a menção a RLS. Numeração: NumberSequence vira configuração com fuso e substituição por prefixo ou período novo; nasce NumberSequenceCounter; período só avança; número único por empresa, com o código do estabelecimento no escopo Establishment; sai a sentinela. Desativação: não emite documento novo e documento em andamento conclui, igual ao ponto de entrega. Eixos do documento: allowsReceiving filtra quem recebe e allowsPurchasing quem pede; requestingEstablishmentId obrigatório e da mesma empresa. Sem rateio entre empresas na v1: sai a empresa da fatia. Moeda funcional BRL na v1. Situação cadastral do estabelecimento com fonte e bloqueio; IE opcional em NonTaxpayer; documento congela os dados fiscais; troca de matriz atômica; CNPJ normalizado e chave composta no lugar do check entre tabelas; código externo da conta com ordem de resolução; token de concorrência em empresa e estabelecimento; dez sufixos de erro
v1.9 – v1.1009/09/2026Desativar empresa cascateia para os estabelecimentos; closedOn deixa de mexer em isActive; empresa inativa restringe escrita e não leitura; GLAccountCompany.isEnabled ganha ponto de aplicação e o lookup de conta exige empresa; NumberSequence ganha POST, período guardado e período no número; edição de endereço do estabelecimento revalida pontos de entrega
até v1.8até 08/09/2026Estabelecimento padrão passa para a preferência da plataforma; regra de IBS e CBS alinhada ao Cadastro de Item (ausência não é erro); calendário do SLA sai da lista de parâmetros com entidade
v1.4—Matriz inline no POST /companies, headquartersId não anulável e onboarding explícito; token de concorrência no plano de contas
v1.001/09/2026Primeira versão: empresa dentro do tenant, os dois modos de tenancy, escopo por empresa do Nexio, plano de contas por empresa, numeração e compra centralizada