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 — TenantSettings 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 v2.1, Fluxo de Aprovação v2.1 e Cadastro de Item v3.1 4 agregados · 2 agregados filhos · 2 tabelas de vínculo v1.1 · 01/09/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ó; TenantSettings.cnpj morre. 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 simplesmente não recorta nada. 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 = 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.
TenantSettings.cnpjUm CNPJ por organização.removido — substituído por Company.

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 vindo do token, nunca do cliente.

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 de TenantSettings. 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 N por tipo de documento Numeração com escopo declarado: tenant, empresa ou estabelecimento. Incremento atômico no banco.
UserCompanyScope Nexio · N por usuário Quais empresas cada pessoa alcança. Referencia o userId do gryd sem FK. Nulo — e a ausência da linha — = todas.
CostCenter eixo irmão legalEntityId vira companyId. A regra do nó corporativo já estava escrita e não muda.
DeliveryLocation futuro · 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. Nunca numérico — aceita A–Z e dígitos. Ú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 | NotApplicableo 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.
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)opcISO 4217, padrão BRL. A moeda em que a empresa fecha; compra em moeda estrangeira converte para ela.
foundedOndateopcData de abertura.
closedOndateopcData de baixa. Preenchida, força isActive = false e bloqueia documento novo com data posterior.

Estado e derivados

CampoTipoRegra
isActiveboolgeradoAlterado só por /activate e /deactivate, nunca por PUT. Desativar com estabelecimento ativo é 409.
headquartersIdGuid?geradoO estabelecimento matriz. Derivado, não digitado — é o único com type = Headquarters.
establishmentCountintgeradoExibido na lista. Ativos e inativos separados no detalhe.

O que não está aqui, de propósito. Certidões, quadro societário, situação cadastral consultada na Receita, 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. Se um dia o produto virar portal e a empresa compradora precisar se apresentar a alguém, esses campos entram — e 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, sem máscara. Ú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.
typeenumobrigHeadquarters · Branch. Exatamente um Headquarters por empresa, sempre. Não se deriva da ordem 0001 do CNPJ: a ordem é convenção da Receita, e a matriz do cadastro é decisão do cliente. Difere do fornecedor de propósito:establishmentType é derivado da ordem 0001, porque o dado vem da Receita e ninguém o digita; aqui é escolha de quem cadastra a própria empresa.
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 quando icmsTaxpayerStatus = Taxpayer; recusada nos outros dois. 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.
registrationStatusenumopcActive · Suspended · Inactive · Closed. Situação cadastral do estabelecimento: uma filial pode estar baixada com a matriz ativa, e o sistema precisa saber disso antes de emitir um pedido em nome dela.

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. Falso, o estabelecimento não aparece no seletor de documento — mas continua podendo ser destino de entrega. Escritório administrativo que não compra é caso real.
allowsReceivingboolopcPadrão true. Falso, não pode ser destinatário de nota.
isActiveboolgeradoSó por /activate e /deactivate. 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.

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 devolver a força perdida — não para fingir que ela não se perdeu.

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.

Se o tenant é RLS, a empresa entra na mesma política

Onde o isolamento de tenant já é row-level security no Postgres, o conjunto resolvido entra na mesma policySET LOCAL na transação e a política lendo current_setting — e a separação volta a ser estrutural, sem depender de claim. Onde o filtro de tenant é aplicação, a empresa não piora nada: é o mesmo nível de risco que já se aceita hoje.

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.

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: uma linha órfã — usuário removido da organização no gryd — é inerte, porque o token nunca mais traz aquele tenant. Não existe escalonamento de privilégio possível por este caminho, e por isso a ausência de chave estrangeira para o gryd não é uma fragilidade.

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.

Nulo = todas. Em toda parte.

UserCompanyScope.companyId nulo é acesso a todas as empresas. CostCenter.companyId nulo é nó corporativo. GLAccountCompany ausente é conta habilitada. Uma convenção só, em três lugares, em vez de três invenções — e nunca "nenhum" nem "não sei".

Nulo é escolha explícita, não o valor que sobra. No cadastro, "vale para todas as empresas" é uma opção marcada, com o efeito escrito ao lado — nunca o comportamento de um campo que ninguém preencheu.

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 na escrita (GET /users/directory), nunca na leitura: leitura com id órfão simplesmente não casa com token nenhum.
companyIdGuid?opcNulo = todas as empresas do tenant. FK composta (tenantId, companyId)companies(tenantId, id): empresa de outra organização é recusada pelo banco.
roleIdGuid?reservadoRecusado quando preenchido nesta versão — 422. Nasce na tabela para que ligar o escopo por papel um dia não exija migração de esquema. Ver o que ele custaria.
validFrom / validTodate?opcMesma mecânica de vigência do CostCenterAssignment. Fora dela, a linha não entra na resolução e continua legível no histórico.
isActiveboolgeradoVigência é fato de calendário; isActive é decisão administrativa. Eixos diferentes, os dois existem.

Unicidade em dois índices parciais, não um: (tenantId, userId) where companyId is null, e (tenantId, userId, companyId) where companyId is not null. Um índice único simples não serviria — no Postgres dois nulos não colidem, e "todas as empresas" duplicado passaria batido.

Como o conjunto é resolvido

SituaçãoConjunto resolvido
Nenhuma linha para o usuárioTodas as empresas ativas do tenant. É a mesma convenção de GLAccountCompany: ausência de linha é permissivo, não restritivo
Alguma linha vigente com companyId nuloTodas as empresas ativas do tenant. As demais linhas não subtraem
Linhas vigentes com empresaA união delas, menos as empresas inativas
União resulta vaziaO usuário não opera. A interface diz por quê — "as empresas do seu acesso estão inativas" — em vez de mostrar telas vazias

Por que ausência de linha é permissivo. O caminho contrário parece mais seguro e é pior na prática: o cliente cadastra a segunda empresa e, no mesmo instante, todo o time perde acesso a tudo — inclusive quem cadastrou. Como a tabela só estreita, o padrão permissivo não amplia nada além do que o token já concedeu naquele tenant. A tela de administração compensa no lugar certo: ao criar a segunda empresa, ela pergunta quem enxerga o quê, com "todos" pré-selecionado.

Estabelecimento padrão — sem tabela

A pré-seleção do cabeçalho do documento é preferência de interface, e fica em localStorage. É o precedente que o próprio gryd já usa para essa classe de coisa — a tela de Permissões persiste ali a escolha entre visão de categorias e grade.

Uma tabela UserPreference no Nexio seria pior por dois motivos. "Preferências do usuário" é conceito da plataforma: o menu Preferências da conta do shell já existe, com tema, idioma e densidade de tabela — criar uma tabela homônima do lado do produto é convidar duas fontes para a mesma pergunta. E tabela com nome genérico vira gaveta: em seis meses tem oito campos que ninguém sabe quem lê.

O custo aceito é que o padrão não acompanha quem troca de máquina. É pequeno: o campo só aparece para quem tem mais de uma opção, e é sempre visível e editável — o pior caso é escolher de novo. Se um dia precisar seguir a pessoa, nasce UserDefaultEstablishment, com esse nome e esse único propósito. Nunca UserPreference.

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 na v2.0.
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
UserCompanyScopeanulávelNulo = todas as empresas do tenant. Ausência de linha para o usuário também.
GLAccountCompany · BudgetobrigatóriaSão, por definição, o recorte por empresa.
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 token de concorrência. 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.
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.
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.

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. 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.

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

NumberSequence
CampoTipoRegra
documentTypeenumobrigRequisition · QuotationRequest · PurchaseOrder · Receipt. Cresce com a espinha transacional.
scopeenumobrigTenant · Company · Establishment. Padrão Company.
companyId · establishmentIdGuid?condExigidos conforme o scope; preenchidos fora dele é 422. Uma sequência por combinação — a unicidade é (tenant, documentType, scope, companyId, establishmentId).
prefixstring(10)opcTexto fixo antes do número. Cliente que quer PC- ou a sigla da filial resolve aqui.
paddingintopcZeros à esquerda. Padrão 6.
resetPolicyenumopcNever · Yearly · Monthly. Padrão Yearly, que é o hábito do mercado.
nextValuebigintgeradoIncrementado por UPDATE … RETURNING atômico no banco, nunca por leitura seguida de escrita na aplicação. Dois POST simultâneos com o mesmo número é o defeito clássico deste componente.

O número pode ter buracos, e isso não é defeito. Transação abortada consome o número e não o devolve. 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. Está escrito aqui para não virar chamado.

Operação

Compra centralizada

Matriz comprando para a filial é rotina em grupo brasileiro, e a diferença entre suportar isso e não suportar são dois campos criados agora — contra uma mudança de chave em agregado já povoado, depois.

Campo do documentoO que respondeRegra
establishmentIdQuem compra, emite e recebe a notaObrigatório. Define a empresa do documento, a numeração e a política de aprovação.
requestingEstablishmentIdQuem pediuNulo quando igual ao anterior. Preenchido, é o que faz o pedido aparecer para quem solicitou e o que alimenta o rateio.
companyIdEmpresa do documentogerado — derivada de establishmentId e congelada. Documento não muda de dono porque o cadastro mudou.

Quando os dois são de empresas diferentes

Permitido, e só permitido, quando o usuário tem escopo nas duas empresas. É a única regra necessária: quem pode ver as duas pode relacionar as duas.

A transferência contábil fica fora

Empresa A comprou para a empresa B gera, no mundo real, um acerto entre elas — nota de transferência, rateio intercompany, ou um lançamento manual. Isso é ERP e continua sendo ERP. O que o Nexio faz é registrar o fato com precisão suficiente para que o acerto seja possível, o que é exatamente o que o campo entrega.

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

Documento fiscal eletrônico não é autorizado sem os campos de IBS e CBS preenchidos, 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 toda nota que chegar já vem com esses campos. O módulo de nota e o three-way match nascem tendo que lê-los.

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 Custo v2.1feitolegalEntityIdcompanyId 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 — já estava escrita e não muda uma vírgula. Rename de coluna sem dado para migrar, aplicado em 01/09/2026. A pendência que a v2.0 deixou registrada — apontar para a empresa ou para o estabelecimento — foi resolvida no mesmo dia: aponta para a empresa; apontar para o estabelecimento multiplicaria a árvore por CNPJ, que é o que o nó corporativo existe para evitar.
Fluxo de Aprovação v2.1feitoO 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 Complementar 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 Item v3.1feitoItem.defaultGlAccountId e CategoryNode.defaultGlAccountId, ambos opcionais, aplicados em 01/09/2026. Encerra as duas pendências que a spec carregava: o eixo contábil ausente e o multi-empresa. Nenhuma mudança de dimensionalidade: item continua sem empresa.
Fornecedor v2.0nenhumaFornecedor é 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.

O que o roleId reservado custaria. Escopo por papel × empresa exigiria o Nexio consumir e cachear o catálogo role → permission do GrydAuth, porque o SessionDto entrega permissions já achatado no tenant e não dá para recompor por papel. É uma integração a mais, não um impedimento — e fica adiada porque o caso que a justificaria (aprovar numa empresa e requisitar em outra) já é resolvido por CostCenterAssignment, que é por nó e o nó tem empresa.
Unidade de Medida v1.0nenhumaEscopo global ou de tenant, como está.
Poda do webapp v1.0acrescentaEmpresa 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.

Camada API

Endpoints

Prefixo api/v{version}/nexio. Todas exigem autenticação e declaram exatamente uma permissão. 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:companiesCompanyDetailDto400 · 409 · 422
PUT /companies/{id}update:companiesCompanyDetailDto400 · 404 · 409
POST /companies/{id}/activateupdate:companies204404 · 409
POST /companies/{id}/deactivateupdate:companies204404 · 409
Estabelecimento
RotaPermissãoRetornoErros
GET /companies/{id}/establishmentsread:companiesarray puro404
GET /establishments/lookupread:companiesagrupado por empresa400
GET /establishments/{id}read:companiesEstablishmentDetailDto404
POST /companies/{id}/establishmentscreate:companiesEstablishmentDetailDto400 · 404 · 409 · 422
PUT /establishments/{id}update:companiesEstablishmentDetailDto400 · 404 · 409 · 422
POST /establishments/{id}/activateupdate:companies204404 · 409
POST /establishments/{id}/deactivateupdate:companies204404 · 409

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 allowsPurchasing, com defaultEstablishmentId marcado. 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-accountsallowsPosting e vigentes400
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-sequencesmanage:number-sequencesarray puro400
PUT /number-sequences/{id}manage:number-sequencesNumberSequenceDto404 · 422
Escopo por empresa e preferência
RotaPermissãoRetornoErros
GET /users/{userId}/company-scopesread:company-scopesarray puro400 · 404
PUT /users/{userId}/company-scopesmanage:company-scopessubstitui o conjunto400 · 404 · 409 · 422
GET /me/company-scopeautenticadoo conjunto resolvido do usuário atual

PUT substitui o conjunto inteiro, não faz merge. É a mesma semântica que a tela de Papéis do gryd já usa — "salvar substitui permissões" — e é o que torna a tela de escopo uma lista de checkboxes honesta em vez de um diff que ninguém consegue conferir. GET /me/company-scope devolve o conjunto já resolvido, com a razão (Explicito ou SemRestricao), porque a interface precisa saber a diferença entre "você tem uma empresa" e "você tem uma empresa por enquanto".

Contrato

Contrato de erros

O status HTTP é derivado do sufixo do código, nunca da mensagem. _NOT_FOUND → 404 · _CONFLICT e _ALREADY_EXISTS → 409 · _UNPROCESSABLE → 422 · sem sufixo → 400.

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
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_HAS_ACTIVE_ESTABLISHMENTS_CONFLICT409Desativação com estabelecimento ativo
COMPANY_HAS_DOCUMENTS_CONFLICT409Exclusão com documento emitido. Empresa com histórico se desativa, não se apaga
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_HEADQUARTERS_ALREADY_EXISTS409Segunda matriz na mesma empresa
ESTABLISHMENT_HEADQUARTERS_REQUIRED_UNPROCESSABLE422Tentativa de deixar a empresa sem matriz
ESTABLISHMENT_LAST_ACTIVE_CONFLICT409Desativação do último estabelecimento ativo de uma empresa ativa
ESTABLISHMENT_STATE_REGISTRATION_REQUIRED_UNPROCESSABLE422icmsTaxpayerStatus = Taxpayer sem 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_TYPE_MISMATCH_UNPROCESSABLE422accountType divergente do pai
GL_ACCOUNT_CONCURRENT_CHANGE_CONFLICT409A linha não carrega mais o path em que foi lida
USER_COMPANY_SCOPE_NOT_FOUND404Linha inexistente, ou de um tenant que não é o do token
USER_COMPANY_SCOPE_DUPLICATE_CONFLICT409Duas linhas para a mesma pessoa e empresa, ou dois "todas as empresas"
USER_COMPANY_SCOPE_ROLE_NOT_SUPPORTED_UNPROCESSABLE422roleId preenchido. A coluna existe e a versão não a resolve
USER_NOT_IN_TENANT_UNPROCESSABLE422O userId não tem vínculo ativo com esta organização no GrydAuth. Verificado só na escrita
NUMBER_SEQUENCE_SCOPE_UNPROCESSABLE422companyId ou establishmentId incoerente com o scope declarado

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
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
manage:company-scopesDefinir quais empresas cada pessoa alcançaAdministrativa e sensível: é o que separa as empresas do grupo umas das outras
manage:number-sequencesConfigurar numeraçãoAdministrativa. Mudar escopo de numeração com documento emitido é permitido e não renumera nada

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.

Invariantes

Regras de negócio

RegraOnde é garantida
Todo tenant tem pelo menos uma empresa ativaSeed na criação do tenant. Não há estado válido sem empresa
Toda empresa tem exatamente uma matrizÍndice único parcial em (companyId) filtrado por type = Headquarters
Todo estabelecimento carrega a raiz da sua empresaValidação no agregado, 422. Coluna gerada left(cnpj, 8) com check contra cnpjRoot
Empresa ativa não fica sem estabelecimento ativoVerificação na desativação, 409
cnpjRoot, code e companyId são imutáveisIgnorados no PUT. Não é erro pedir para mudar — é campo que não existe na atualização
companyId nunca vem do clienteInjetado do claim em toda escrita, como tenantId
Documento aponta para estabelecimento e congela a empresaRegra da linha do documento. Alteração de cadastro não reescreve documento emitido
Linha da empresa A só usa centro de custo corporativo ou de AJá especificado no Centro de Custo v2.0, validado na linha com o companyId efetivo do nó
Conta sem allowsPosting não recebe rateioValidação na fatia, 422
UserCompanyScope só estreita, nunca concedeDesenho. A tabela não dá organização, papel nem permissão — o token do GrydAuth continua decidindo isso. Linha órfã é inerte
Ausência de linha = todas as empresasResolvedor. Mesma convenção de GLAccountCompany
Nenhuma tabela do gryd é alteradaFronteira de camada. A plataforma não conhece Company, e não deve conhecer
CNPJ é varchar nos dois lados do produtoEsquema. Validação de DV com ASCII − 48

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 · Seed a partir do que já existe

Para cada tenant: uma Company com code = "0001", cnpjRoot extraída de TenantSettings.cnpj e legalName do nome do tenant; um Establishment Headquarters com o CNPJ completo e code = "0001". Campos fiscais entram como pendência de preenchimento, não como bloqueio.

2 · TenantSettings.cnpj sai no mesmo release

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_idcompany_id. Nenhuma tabela do gryd é tocada. Nasce uma tabela no esquema do Nexio, user_company_scopes, vazia — e vazia significa "todo mundo enxerga todas as empresas", que é exatamente o comportamento de hoje. ApprovalPolicy também não ganha coluna: o critério resolve. Todos anuláveis e todos vazios: nenhum backfill, nenhuma janela.

4 · Tenants existentes não mudam de comportamento

Com uma empresa por tenant, o seletor não aparece, a numeração continua única e o escopo não recorta nada. A migração é invisível para quem tem um CNPJ só — que é o teste de que o modelo está certo.

Validação

Referência de mercado

NexioOracle FusionSAPCoupaProtheus
TenantEnterpriseClientInstanceAmbiente
CompanyLegal EntityCompany CodeLegal EntityEmpresa
Establishmentnão existe — o site não tem identificação fiscal própriaBusiness Place · Local de Negócio, construto criado para o Brasilnão existeFilial
GLAccountNatural AccountGL AccountAccountPlano de contas
NumberSequenceDocument SequenceNumber RangeNumeraçã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
Transferência intercompanyO documento registra quem comprou e quem pediu; o acerto entre as empresas é lançamento no 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