Especificação · Controle Orçamentário

Orçamento

A pergunta de primeira reunião comercial no Brasil. Sem controle orçamentário, o aprovador decide no escuro: ele sabe que pode aprovar R$ 40 mil porque a alçada permite, e não sabe se há R$ 40 mil. Esta spec resolve isso sem transformar o Nexio num sistema de planejamento financeiro — a verba entra pronta ou é coletada por centro de custo, e o que o produto faz de verdade é não deixar o dinheiro ser contado duas vezes entre a requisição, o pedido e a nota.

depende de CostCenter · GLAccount · Company consumido por Requisition · PurchaseOrder · Receipt · Invoice · Contract 6 agregados · câmbio em Moeda e Câmbio v1.12 · 09/09/2026

Ponto de partida

Dez decisões

Decisão 1 · Modelo

Verba não é um número, é um livro-razão. Não existe coluna consumedAmount atualizada por UPDATE. Existe BudgetEntry append-only e o saldo é projeção. Estorno é linha nova de sinal contrário; nada é apagado, nada é reescrito.

Decisão 2 · Estágios

Três estágios: Commitment → Obligation → Actual. Requisição aprovada compromete, pedido emitido empenha, recebimento realiza. Cada estágio liquida o anterior na proporção do que avançou — nunca soma em cima.

Decisão 3 · Chave

Dimensões opcionais com rollup. Empresa · centro de custo · conta · período, com centro de custo e conta anuláveis (nulo = qualquer). Verba num nó intermediário cobre a subárvore por prefixo de path. O consumo grava uma entrada só, na linha mais específica que casa — e caixas dentro de caixas vale de verdade: o saldo do ancestral soma o consumo da subárvore por prefixo, e o bloqueio avalia a pilha inteira de ancestrais com linha vigente, vencendo a política mais restritiva (RN-O31).

Decisão 4 · Ausência

Regra assimétrica. Fora de escopo controlado, passa livre. Dentro de escopo controlado sem nenhuma linha que cubra, bloqueia — porque a linha coringa (conta nula) existe justamente para cobrir o resto. Fica descoberto por decisão, nunca por acidente.

Decisão 5 · Registro

O movimento é sempre gravado, com ou sem controle, pela tupla de dimensões e não só pelo id da linha. Custo zero, e quando o cliente ligar o controle no ano seguinte terá doze meses de consumo real para dimensionar a verba em vez de chutar.

Decisão 6 · Competência

A competência depende do estágio: necessidade planejada na intenção, programação contratada na obrigação e recebimento/aceite ou período prestado no realizado. Preservar todas as datas; não substituir fato por planejamento.

Decisão 7 · Carry-forward

É modo de leitura, não movimento de transporte. PeriodOnly lê só o balde; Cumulative lê de janeiro até o balde. Sem fechamento mensal obrigatório, sem tela de reabertura, e a nota atrasada se corrige sozinha. Dentro do exercício apenas — nunca entre exercícios.

Decisão 8 · Virada

O compromisso segue o documento, não o calendário. Na virada, o que está aberto é revertido no exercício que fecha e recriado no novo, sem trazer verba junto por padrão. Aperto vira estouro em janeiro — que é exatamente quando o sócio quer ser consultado.

Decisão 9 · Moeda

Um exercício, uma moeda; a taxa vem congelada da submissão do documento. Documento em moeda estrangeira converte uma única vez na vida, na submissão, e o razão herda esse número — nunca reavalia e nunca resolve de novo. A variação cambial entre estágios aparece sozinha como consumo real, e é atribuível. O cadastro, a captura e a resolução da taxa são de Moeda e Câmbio.

Decisão 10 · Visibilidade

Ver o semáforo e ver o número são permissões diferentes. read:budget-states devolve cor e razão; read:budget-amounts devolve valores, recortados pelos nós em que a pessoa tem vínculo. O corte é no servidor — o campo não vai na resposta, não é escondido na tela.

Fundamento

Por que razão, e não saldo

O desenho ingênuo quebra em silêncio

A implementação óbvia é BudgetLine(amount, consumedAmount) com UPDATE consumed = consumed + x em cada aprovação. Ela funciona no caminho feliz e quebra em quatro lugares previsíveis: a requisição devolvida (quanto estornar, se o valor mudou na rodada?), o pedido cancelado parcialmente, a nota que chega com preço diferente do pedido, e o reprocessamento de uma mensagem duplicada. Em nenhum desses casos o sistema acusa erro — ele passa a mostrar um saldo errado, e ninguém descobre até a conversa com a controladoria em que os números não batem.

O que o razão dá de graça: resposta linha a linha para "quem comeu minha verba", reversão correta por construção, idempotência por chave natural, e reconstrução completa do saldo a partir dos fatos quando alguém desconfiar do número.

Saldo é projeção, com uma tabela materializada por desempenho

A verdade é BudgetEntry. Para não somar milhões de linhas a cada digitação de requisição, existe BudgetLineBalance — uma linha por budgetLineId × estágio, atualizada por upsert na mesma transação do movimento. Ela é cache, não fonte: um comando administrativo a reconstrói inteira a partir do razão, e a divergência entre as duas é o teste de integridade que roda em ambiente de homologação.

A consulta informa; o servidor decide

budgetStateFor é leitura sem trava, serve para pintar a tela enquanto a pessoa digita e pode estar desatualizada no instante seguinte. O bloqueio real acontece dentro da transação que grava o movimento, sob trava da linha de saldo. Nenhuma decisão de bloqueio confia numa consulta anterior — dois pedidos simultâneos contra a última fatia de verba não podem passar os dois, e essa é a única forma de garantir isso.

O que esta spec deliberadamente não é. Não é planejamento financeiro. Não há driver de cálculo (headcount × valor médio), comparação de cenários, rateio de overhead, projeção ou forecast rolante. Isso é FP&A e é outro produto. Planejamento aqui significa exatamente uma coisa: coletar números por centro de custo e aprová-los, reusando o motor de aprovação que já existe.

Modelo

Mapa de entidades

Budget aggregate root · o exercício Empresa + período + versão. Guarda a moeda, a granularidade do balde e o ciclo de vida Draft → InReview → Approved → Closed → Archived. A versão é do exercício, nunca da linha.
BudgetLine N por exercício · a verba Dimensões (centro de custo? · conta? · período) e valor. As duas dimensões anuláveis significam qualquer, e é isso que permite a linha coringa.
BudgetEntry append-only · o movimento Estágio, sinal, valor, taxa congelada, documento de origem e chave de idempotência. Sem update, sem delete. É a única fonte de verdade do consumo.
BudgetLineBalance projeção · cache Totais por linha e estágio, mantidos na mesma transação do movimento. Reconstruível a partir do razão — existe por desempenho e por trava, não por verdade.
BudgetPolicy por nó de CC · com vigência As quatro políticas: exigir verba, modo de saldo, reação ao estouro e comportamento na virada. Cada campo é resolvido subindo o path, independentemente dos outros.
BudgetChangeRequest documento · passa pelo motor Suplementação, remanejamento ou redução. Aprovado pelo processo BUDGET_CHANGE da semente do motor. Ao aprovar, emite movimentos de Adjustment.
Currency · ExchangeRate transversal · outro documento Nasceram aqui porque o orçamento precisou primeiro, e saíram em 01/09/2026 para a spec de Moeda e Câmbio — Cotação, Pedido e Nota consomem os mesmos. O orçamento é consumidor, não dono.
CostCenter · GLAccount já especificados As duas árvores que dão as dimensões. O orçamento consome path, allowsPosting e o rollup; nenhuma das duas ganha campo novo.

Convenção de nomes. O documento é escrito em português; todo identificador — entidade, atributo, valor de enum, rota, permissão e código de erro — é em inglês. Dados de negócio brasileiros ficam como são no mundo real: PtaxVenda, cnpj, BRL.

Contrato vigente · revisão consolidada

Estágios, parcelas e conservação do consumo

Commitment reserva a intenção aprovada; Obligation registra a obrigação efetiva; Actual registra recebimento, aceite ou serviço prestado. Adjustment altera exclusivamente a verba autorizada. Avançar liquida a parcela do estágio anterior e lança a parcela do novo, na mesma transação.

Requisição aprovadaCommitment→Pedido / mínimo contratualObligation→Recebimento / aceiteActual
Cada avanço liquida somente a parcela vinculada da origem. A nota ajusta o realizado correspondente.
EventoMovimentosCommitmentObligationActualConsumo
Requisição: 100 un × R$ 10Commitment +1.0001.000001.000
Pedido: 60 un × R$ 11Commitment −600; Obligation +66040066001.060
Recebimento: 55 unObligation −605; Actual +605400556051.060
Encerrar as 5 un não entregues e a demanda restanteObligation −55; Commitment −40000605605
Nota: diferença de R$ 10 aceita para as 55 unActual +10 vinculado ao Actual +60500615615

Cancelar não apaga recebimentos. Cada operação identifica as parcelas ainda abertas e o que suas liquidações consumiram. Cancelar 5 unidades não entregues reverte somente essa obrigação; se a demanda continuar viva, recompõe o Commitment correspondente. Devolver 10 unidades recebidas reverte o Actual dessas 10 e recompõe obrigação apenas quando houver reposição exigível. A alternativa sem reposição encerra essa parcela. Nenhuma recursão cega estorna todos os descendentes.

Grupo e entradas têm identidades diferentes. postingGroupId identifica uma operação de negócio; entryOrdinal identifica cada lançamento desse grupo. Uma operação pode liquidar várias entradas do mesmo estágio e fatia. Repetição com a mesma chave e o mesmo conteúdo devolve o resultado original; conteúdo diferente é conflito, nunca descarte silencioso.

O resíduo aberto tem documento, linha, fatia, quantidade, valor, responsável e idade. GET /budget-entries/residues lista essas parcelas; encerramento explícito libera o restante, com motivo e aviso durável ao responsável quando envelhece. O total consumido é a soma algébrica de Commitment, Obligation e Actual, sem contar Adjustment.

Contrato sem mínimo obrigatório consome nas liberações. Contrato com mínimo obrigatório gera Obligation ao entrar em vigor; cada liberação transfere sua parcela para o pedido, no mesmo estágio, sem somar novamente o mínimo. Excedente sobre o mínimo é nova obrigação e passa pelos controles. Detalhes de quantidades e vigência em Contratos transacionais.

Contrato vigente · revisão consolidada

Resolução da verba e controle obrigatório

  1. Resolver a política vigente por tenant, empresa e centro de custo, campo a campo, incluindo os limites ancestrais aplicáveis.
  2. Somente quando requiresBudget = false em todo o escopo aplicável, retornar NotControlled. Ainda assim, registrar o fato com suas dimensões.
  3. Em escopo controlado, procurar o exercício e as linhas vigentes para a competência do estágio. Ausência de exercício aprovado, período apto a receber novo compromisso ou linha correspondente resulta em Uncovered. Não abre exceção por falta de cadastro.
  4. Escolher a linha mais específica, depois a maior profundidade do centro de custo e da conta. Chaves únicas impedem empate. Avaliar também todos os limites ancestrais, inclusive limites sobrepostos que cubram a fatia, e todas as janelas cumulativas afetadas; cada entrada econômica é contada uma única vez em cada limite.
  5. Gravar uma entrada na linha vencedora e atualizar as projeções dos limites. Novo Commitment ou Obligation descoberto é recusado. Actual ocorrido permanece registrável, com Uncovered e pendência de classificação.

A política efetiva declara a origem de cada campo e signalFrom identifica todos os limites impeditivos. Consulta prévia é estimativa; a decisão que grava repete a avaliação sob as travas de todos os limites afetados. Liquidações usam a imputação vigente da parcela de origem, preservando a cadeia de reclassificações.

Campos

Budget · o exercício

CampoTipoRegra
iduuid—
tenantIduuidDo token. Id de outro tenant responde 404, nunca 403.
companyIduuidObrigatório. Orçamento é da pessoa jurídica — é ela que tem sócio, conselho e prestação de contas. FK composta (tenantId, companyId).
namevarchar(120)Rótulo humano: "Orçamento 2027", "Obra Guarulhos 2027–2029".
periodStart · periodEnddateO intervalo do exercício. Não precisa ser o ano civil — exercício de abril a março é comum em subsidiária de multinacional. Pode ultrapassar doze meses (orçamento plurianual de obra).
periodGranularityenumMonthly | Quarterly | Yearly. Define os baldes gerados entre periodStart e periodEnd. Imutável após Approved — mudar granularidade reparticiona todas as linhas e invalidaria o razão.
currencychar(3)ISO 4217. Copiada de Company.functionalCurrency na criação e congelada. Um exercício tem uma moeda só.
versionintSequencial por (companyId, periodStart). Versão nova nasce como cópia da anterior em Draft.
supersedesBudgetIduuid?A versão que esta substitui. A substituída vai para Archived no instante em que a nova é aprovada.
statusenumDraft | InReview | Approved | Closed | Archived. Só Approved e Closed participam da resolução; Draft e InReview são invisíveis para o consumo.
originenumImported | Planned. Informativo — o ciclo de vida é o mesmo; muda só por onde as linhas entraram.
approvalRequestIduuid?A instância do motor quando origin = Planned. Processo BUDGET — valor avaliado: total orçado do exercício; critérios: valor e empresa. Entrou na semente em 04/09/2026 (até então esta spec citava um processo que o motor não tinha).
lateActualsUntildate?Janela configurada para lançamento tardio na competência original, padrão inicial 60 dias após periodEnd. Depois dela, registrar em período aberto com referência à competência/fato original e à obrigação transportada, quando houver.
externalCodevarchar(60)?Conciliação com o ERP. Único por tenant quando preenchido.
rowVersionintConcorrência otimista: nasce 1 e incrementa a cada escrita; o cliente envia expectedVersion e a divergência é 409, declarado no OpenAPI. É o mesmo mecanismo de Item, Fornecedor e Centro de Custo — o nome aqui é rowVersion porque version já é a revisão do exercício. Vale também para BudgetLine. Até a v1.3 era bytea, tipo do Postgres; coluna inteira comum não depende do banco.
Transições de status
De → paraQuem disparaO que acontece
Draft → InReviewAção do usuárioAbre instância do processo BUDGET. Linhas viram somente-leitura.
InReview → ApprovedMotor de aprovaçãoVerifica que não há outro Approved com período sobreposto. Arquiva a versão substituída. A partir daqui as linhas só mudam por BudgetChangeRequest. E, quando periodStart já passou, executa o backfill — os movimentos de janeiro e fevereiro que nasceram NotControlled ganham imputação retroativa, por movimento novo, na mesma transação (RN-O32).
InReview → DraftDevolução no motorDevolver não é rejeitar: a instância segue viva e as linhas voltam a ser editáveis.
Approved → ClosedAção com permissão própriaRoda a virada de exercício (adiante). Recusa Commitment e Obligation novos; aceita Actual até lateActualsUntil.
Closed → ArchivedApós janela tardia e conciliaçãoImpede novos lançamentos nesse exercício; fatos tardios seguem período aberto e referência original. Não apaga histórico nem extingue obrigação transportada.

Campos

BudgetLine · a verba

CampoTipoRegra
id · tenantId · budgetIduuid—
costCenterIduuid?Nulo = qualquer centro de custo. Quando preenchido, cobre o nó e toda a subárvore. Não exige allowsPosting — orçar num nó intermediário é o caso normal; quem não pode receber lançamento é o documento, não a verba.
glAccountIduuid?Nulo = qualquer conta. Mesma regra de subárvore.
periodKeyvarchar(7)2027-03, 2027-Q1 ou 2027, conforme a granularidade do exercício. Materializado junto com periodStart/periodEnd para consulta por intervalo sem interpretar string.
amountnumeric(21,4)Na moeda do exercício. Pode ser zero (declarar explicitamente "aqui não se gasta" é diferente de não ter linha). Negativo é recusado.
specificitysmallintDerivado e materializado: 2 com as duas dimensões, 1 com uma, 0 coringa. Existe para o índice do desempate não precisar de CASE.
notes · externalCodevarcharJustificativa da verba e código do ERP.

Unicidade com duas colunas anuláveis

A chave lógica é (budgetId, costCenterId, glAccountId, periodKey), mas no Postgres dois nulos não colidem — um índice único simples deixaria criar duas linhas coringa idênticas. Como já foi feito em UserCompanyScope, a unicidade sai em quatro índices parciais: ambos preenchidos, só centro de custo, só conta, e nenhum dos dois. É verboso e é a única forma correta.

Linha só muda por documento, depois de aprovada

Enquanto o exercício está em Draft, editar linha é edição comum. Depois de Approved, amount não é mais editável: a mudança vem de BudgetChangeRequest aprovado, que grava movimento de Adjustment. O valor efetivo de uma linha passa a ser amount + Σ Adjustment — e o histórico de por que a verba cresceu fica no razão, com justificativa e aprovador, em vez de sumir num UPDATE.

Campos

BudgetEntry · o movimento

CampoTipoRegra
id · tenantId · companyIduuidcompanyId é obrigatoriamente a empresa do documento e da fatia. Escolhe o Budget consumido; pedidos centralizados são separados por destinatário fiscal (RN-O40).
stageenumCommitment | Obligation | Actual | Adjustment — quatro valores desde a v1.8. Os três primeiros consomem; Adjustment altera a verba e só nasce de BudgetChangeRequest, de Import ou de Rollover (RN-O28) — os mesmos três de que fala a regra das dimensões, logo abaixo. CarryOut/CarryIn saíram do enum: não consumiam, não tinham campo em amounts e faziam a virada prometer um transporte que não acontecia. A virada passa a gravar nos próprios estágios, com entryReason = YearEndRollover (RN-O13).
amountnumeric(21,4)Com sinal, na moeda do exercício. Positivo consome (ou, em Adjustment, aumenta a verba); negativo libera. Zero é recusado.
costCenterId · glAccountIduuid?Obrigatórios em novos Commitment e Obligation. Actual sem classificação guarda dimensões conhecidas e pendência explícita; classificação posterior usa par balanceado. Adjustment copia as dimensões da linha de verba, inclusive nulas.
competenceDate · competencePeriodKeydate · varchar(7)Competência do estágio: necessidade planejada em Commitment; programação contratada em Obligation; recebimento, aceite ou período prestado em Actual. Preservar datas distintas e a competência original; contabilização fiscal é outro campo. Mudança gera pares de reclassificação, nunca UPDATE.
budgetId · budgetLineIduuid?Imputação imutável desta entrada; nula em NotControlled ou Actual Uncovered. Liquidação usa a imputação vigente da parcela de origem, seguindo reclassificações. Backfill, reorçamento e correção classificatória transferem por pares balanceados.
resolutionReasonenumNotControlled | Covered | Uncovered. Uncovered impede novos compromissos; Actual ocorrido pode ser registrado com pendência de classificação.
currencyDocument · amountDocumentchar(3) · numeric(21,4)O valor como está no documento. Igual a amount e à moeda do exercício no caso comum.
exchangeRate · quoteUnit · rateType · rateDatenumeric(18,8) · int · enum · dateA taxa usada e o fator de cotação com que ela foi publicada, ambos congelados. 1.0 / 1 / None quando não há conversão. quoteUnit virou coluna em 09/09/2026: a regra de herança já mandava copiá-lo do documento e a tabela de campos não o tinha, de modo que o razão prometia reconstituir uma conta sem guardar um dos fatores dela — e em moeda cotada por 100 o erro é de duas ordens de grandeza. Movimento originado por documento não resolve taxa: herda a que o documento congelou na submissão (RN-O41). Resolução própria existe só para sourceType = Manual e Import. Detalhe na seção de multimoeda.
sourceType · sourceId · sourceLineId · allocationIdenum · uuidRequisition | PurchaseOrder | Receipt | Invoice | Contract | BudgetChangeRequest | Rollover | Import | Manual. É o rastro que responde "quem comeu minha verba". allocationId é a identidade estável da fatia — gerada na criação da CostAllocation e preservada na edição do documento (RN-CC-33), não o order, que é reusado quando o rateio muda de 70/30 para 50/50.
liquidatesEntryId · sourceEntryIduuid?A entrada liquidada e a origem econômica, com quantidade/valor da parcela. Uma liquidação por entrada de origem; várias entradas podem ser liquidadas no mesmo grupo. Nota referencia o Actual ajustado; não liquida novamente o pedido já recebido.
reversalOfEntryId · reclassifiesEntryIduuid?Estorno integral ou parcial aponta a entrada e a parcela corrigida. Reclassificação identifica imputação anterior. Validação impede reverter ou liquidar além do saldo elegível.
entryReasonenumBudgetEntryReason, 12 valores: Post, Liquidate, Reverse, DocumentCancelled, Return, ReceiptReversal, QuantityChange, DateChange, ResidueRelease, YearEndRollover, BudgetBackfill, BudgetReclassification. Obrigatório; mapeamento canônico em Motivo.
reasonCodeId · reasonCode · reasonLabeluuid? · varchar(40)? · varchar(120)?Anuláveis, e os três juntos. FK para o catálogo de Motivo mais o snapshot do código e do rótulo, na quádrupla que todo ato grava. Preenchidos só quando uma pessoa deu o motivo: respondem "por que a pessoa fez isso". O caso canônico é o Adjustment — pela RN-O28 ele só nasce de BudgetChangeRequest ou Import, e mudança de verba tem justificativa humana. O reasonComment fica no documento de origem; o razão guarda o código e o rótulo, não o texto. Passou a anulável em 09/09/2026
postingGroupId · entryOrdinal · idempotencyKeyuuid · int · varchar(200)Operação de negócio estável e identidade de cada entrada. Chave única (tenantId, postingGroupId, entryOrdinal); hash e resultado no grupo. A mesma operação pode conter várias liquidações da mesma fatia e estágio.
occurredAt · postedAt · createdByUserIdtimestamptz · uuidQuando o fato aconteceu no domínio e quando entrou no razão. userId do gryd, sem FK — mesmo padrão de CostCenterAssignment.
originalQuantity · settledQuantity · sourceAllocationIdnumeric(19,6) · numeric(19,6) · uuid?Quantidade original e parcela liquidada ou revertida, com unidade e fatores congelados na referência de origem. Valor não é recalculado a partir da quantidade normalizada arredondada.
originalCompetenceDate · servicePeriodStart · servicePeriodEnd · fiscalPostingDatedate?Datas de fatos distintas; ajuste tardio preserva competência original e informa o período autorizado para lançamento.

entryReason identifica a causa sistêmica; reasonCode e seu snapshot registram a justificativa humana quando houver. Reversão vinculada usa Reverse sem pressupor estorno de todos os fatos descendentes. O motivo não substitui a identidade de operação e entrada.

Por que os valores de máquina não podem morar no catálogo. ReasonCode é sobreponível por tenant: o cliente renomeia, desativa e cria linhas. Um valor de que a máquina de estados depende, dentro de uma tabela que o cliente edita, é a porta que a regra "motivo é dado, nunca regra" existe para fechar — o tenant que desativasse YearEndRollover quebraria a virada de exercício. Por isso BudgetEntryReason é enum, e por isso a lista fechada dele está escrita na spec de Motivo, que é onde o acervo inventaria vocabulário de motivo, sem virar linha de tabela.

operation é uma projeção de entryReason, conforme Motivo. A identidade de idempotência é a operação de negócio e cada entrada do grupo (RN-O34); dois lançamentos legítimos da mesma categoria não colidem.

Supplement e Transfer saíram, e não foram para lugar nenhum. Os dois eram cópia do type do BudgetChangeRequest, que o movimento já alcança por sourceType e sourceId. Duas fontes da verdade para o mesmo fato divergem na primeira correção; a que fica é a do documento.

BudgetEntry não tem UPDATE nem DELETE. Não é convenção de equipe, é grant: o usuário de aplicação recebe apenas INSERT e SELECT nessa tabela, e uma trigger recusa as outras duas operações com mensagem explícita. Toda correção é movimento novo. Quem precisar apagar razão em produção precisa de um caminho administrativo separado e auditado — e nunca precisou até hoje em nenhum sistema que fez isso direito.

Campos

BudgetPolicy · as quatro políticas

Tabela própria, e não colunas no nó de centro de custo, por dois motivos: as políticas precisam de vigência (ligar controle para 2027 sem reescrever o passado) e o nó já carrega bagagem suficiente. Mesma forma de CostCenterAssignment.

CampoTipoRegra
costCenterIduuid?Nulo = padrão do tenant. Preenchido, vale para o nó e seus descendentes até que um descendente sobrescreva o mesmo campo.
companyIduuid?Novo em v1.8. Nulo = todas as empresas do tenant. Preenchido, a política só vale para documentos daquela empresa. Sem esta coluna, um grupo — que é o público-alvo declarado — não conseguia ligar requiresBudget na empresa madura e deixá-lo desligado na recém-adquirida sem duplicar nós de centro de custo por empresa, que é exatamente o que o nó corporativo existe para evitar. Mesmo espírito de GLAccountCompany: a árvore é compartilhada, a decisão é da pessoa jurídica. Na resolução, política com empresa vence a de empresa nula para o mesmo campo e o mesmo nó (RN-O37).
validFrom · validTodate · date?Vigência. Sobreposição de duas políticas do mesmo (costCenterId, companyId) é recusada — a exclusão de faixas com igualdade nas duas dimensões e sobreposição no período, EXCLUDE + btree_gist como exemplo de implementação, com verificação na aplicação onde o banco não oferecer equivalente. Política de empresa nula e política de empresa preenchida convivem no mesmo nó e no mesmo período, e é essa convivência que dá o padrão do grupo com exceção por empresa. Nunca se edita vigência para o passado — trocar política é encerrar e abrir.
requiresBudgetbool?Liga o controle. Padrão de fábrica falso.
carryForwardModeenum?PeriodOnly | Cumulative. Padrão de fábrica PeriodOnly.
overBudgetPolicyenum?Warn | Block | RequireApproval. Padrão de fábrica Warn. É a única fonte da decisão de bloqueio no produto: desde 03/09/2026 o motor de aprovação lê esta política já resolvida e obedece, e não tem mais parâmetro próprio.
tolerancePercent · toleranceAmount · toleranceCurrencynumeric(9,6)? · numeric(21,4)? · char(3)?Percentual e valor: aplicar o menor quando ambos existem. Moeda obrigatória junto do valor, inclusive em política corporativa; converter pela taxa documentada da avaliação.
yearEndCommitmentPolicyenum?CarryCommitmentOnly | CarryCommitmentAndBudget | CancelOpen. Padrão de fábrica CarryCommitmentOnly.

Herança campo a campo, e a API devolve de onde veio

Cada campo sobe o path por conta própria: requiresBudget pode vir do nó "Obra Guarulhos", overBudgetPolicy de "Operações" e carryForwardMode do padrão do tenant. É mais flexível e é mais difícil de explicar em tela — por isso GET /budget-policies/effective devolve, para cada campo, o valor e o nó de onde ele veio, exatamente como effectiveOwners devolve inheritedFrom. A tela mostra "herdado de Operações" ao lado de cada linha, e ninguém precisa adivinhar.

Campos

BudgetChangeRequest · mexer na verba

Depois de aprovado o exercício, verba só muda por documento — e documento passa pelo motor, no processo BUDGET_CHANGE da semente, que avalia o delta da verba com os critérios valor, centro de custo e conta contábil (o critério de conta contábil entrou no motor em 04/09/2026, junto com o nome corrigido aqui).

CampoTipoRegra
numbervarchar(30)Via NumberSequence, escopo Company.
typeenumSupplement (dinheiro novo), Transfer (remanejamento entre linhas), Reduction (devolver verba).
fromLineId · toLineIduuid?Transfer exige os dois; Supplement só toLineId; Reduction só fromLineId. Ambos precisam pertencer ao mesmo budgetId.
amountnumeric(21,4)Positivo. O sinal vem do type, não do número — assim ninguém digita menos-menos.
reasonCodeId · reasonCode · reasonLabel · reasonCommentuuid · varchar(40) · varchar(120) · textObrigatórios, os quatro. A quádrupla do Motivo, no uso BudgetChangeJustify: o código classifica para o relatório, o comentário explica o caso. É o que a controladoria vai ler em dezembro — e, agora, é também o que ela consegue contar. Corrigido em 09/09/2026: era justification, text livre — a sexta maneira de resolver motivo que a spec de Motivo existiu para apagar, e que sobreviveu por estar numa página que aquela leva não tocou. É esta quádrupla que o BudgetEntry do Adjustment copia.
effectiveDatedateData do movimento de Adjustment. Não pode ser anterior ao periodStart do exercício.
status · approvalRequestIdenum · uuid?Draft | PendingApproval | Approved | Rejected | Cancelled. A transição para Approved é do motor, e é ela que grava os movimentos.

Remanejamento não pode deixar a origem negativa

Na aprovação, o servidor revalida o saldo da linha de origem naquele instante, sob trava — não no momento em que o pedido foi feito. Verba pode ter sido consumida durante a aprovação. Se não couber, a aprovação falha com 422 e o documento volta ao solicitante com o número atual, em vez de deixar a origem estourada.

Movimento, não versão

Movimento ajusta o orçado; versão substitui o exercício. Suplementar R$ 80 mil em março é um Adjustment — a linha original continua visível, e a diferença tem autor, data e justificativa. Refazer o plano inteiro no meio do ano é Budget versão 2, com o razão do anterior preservado e reapontado. Confundir os dois é como confundir commit com force push.

Contrato

Consulta de saldo

Um contrato só, consultado por todo mundo: a tela do requisitante enquanto ele digita, o motor de aprovação como critério, e o próprio serviço de lançamento antes de gravar. O documento manda as fatias que quer avaliar; a resposta vem fatia a fatia.

POST /evaluate:budget-state
{
  "companyId": "…",
  "slices": [
    { "ref": "L1-A", "costCenterId": "…", "glAccountId": "…",
      "neededOn": "2027-03-14", "amount": 18400.00, "currency": "BRL" }
  ]
}

200 OK
{
  "results": [{
    "ref": "L1-A",
    "signal": "Warning",
    "reason": "Covered",
    "budgetId": "…", "budgetLineId": "…", "periodKey": "2027-03",
    "carryForwardMode": "Cumulative",
    "overBudgetPolicy": "RequireApproval",
    "policySource": { "requiresBudget": "CC 4.2 Operações",
                      "overBudgetPolicy": "tenant" },
    "amounts": {                       // ausente sem read:budget-amounts
      "currency": "BRL",
      "budgeted": 240000.00,           // amount + Σ Adjustment
      "committed": 51200.00,
      "obligated": 96300.00,
      "actual": 74100.00,
      "consumed": 221600.00,           // = consumedSubtree
      "consumedOwn": 180400.00,        // resolvido para esta linha
      "consumedSubtree": 221600.00,    // + linhas de descendentes, por prefixo de path
      "available": 18400.00,
      "toleranceRoom": 2400.00
    },
    "signalFrom": { "budgetLineId": "…", "costCenterPath": "/TI/",
                    "overBudgetPolicy": "Block" },   // qual linha da pilha produziu o sinal
    "wouldExceedBy": 0.00
  }]
}
BudgetSignal · o que a tela pinta
SinalQuandoO que o requisitante vê
NotControlledSem exercício, ou requiresBudget falsoCinza · "sem controle orçamentário". Nenhum número, nem se a pessoa tiver permissão — não há o que mostrar.
OkCabe no disponívelVerde. Segue o baile.
WarningEstoura, mas dentro da tolerânciaÂmbar · "vai consumir toda a verba de março". Passa, e o gestor recebe notificação.
ExceededNovo compromisso é recusado por Block; RequireApproval exige cobertura complementar concluída. Actual ocorrido é registrado com pendência.Critério e controle obrigatório conforme RN-O27; aviso não substitui aprovação.
UncoveredAusência de exercício/linha apta em escopo controladoImpede Commitment/Obligation novos; Actual ocorrido permanece registrável com pendência. Não converte ausência em Warn.

O contrato dos sinais, e o que cada um obriga — novo em v1.8

BudgetSignal tem cinco valores. NotControlled é a ausência de controle e não é critério de nada: nem o motor nem o serviço de lançamento fazem qualquer coisa com ele. Os outros quatro são o contrato que evaluate:budget-states publica, e cada um obriga a exatamente uma coisa:

SinalO serviço de lançamentoO motor de aprovação
OkGrava.Nada. Não é critério.
WarningGrava e marca o documento como fora do orçamento.Critério disponível para política Supplemental; sem política que case, segue a cadeia normal.
ExceededNovo compromisso é recusado por Block; RequireApproval exige cobertura complementar concluída. Actual ocorrido é registrado com pendência.Critério e controle obrigatório conforme RN-O27; aviso não substitui aprovação.
UncoveredAusência de exercício/linha apta em escopo controladoImpede Commitment/Obligation novos; Actual ocorrido permanece registrável com pendência. Não converte ausência em Warn.

Uncovered significa ausência de exercício apto ou linha em escopo controlado. Impede novo compromisso sem passar por tolerância. Actual ocorrido é registrado com pendência de classificação; a mensagem indica o que falta corrigir e a classificação posterior é balanceada.

available depende do modo de leitura

Com PeriodOnly, é budgeted(março) − consumed(março). Com Cumulative (acumulado), é Σ budgeted(janeiro..março) − Σ consumed(janeiro..março). Mesmo razão, mesma linha, janela diferente — e é por isso que o modo aparece na resposta: a tela precisa poder dizer "R$ 18.400 disponíveis no acumulado do ano", que é uma frase diferente de "R$ 18.400 disponíveis em março".

O rollup do centro de custo é do lado da verba, não do consumo

Uma verba em "Operações" cobre "Operações › Manutenção › Elétrica". O consumo daquela fatia é gravado com o nó folha e resolvido para a linha de "Operações" — então o extrato da verba mostra o gasto das três folhas somado, e cada movimento continua sabendo de qual folha veio. Não existe verba "consolidada" calculada por cima: existe uma linha, com consumo de vários nós.

E quando há linha no pai e no filho, o extrato do pai enxerga o filho — corrigido em 09/09/2026. A consulta devolve dois números por nó: consumedOwn, o que foi resolvido para aquela linha, e consumedSubtree, que soma também o consumo resolvido para linhas de descendentes, por prefixo de path. Até a v1.7 o extrato de /TI/ mostrava o gasto de /TI/INFRA/ como zero, e as duas verbas somavam poder de gasto em vez de se conterem — o contrário do que a metáfora "caixas dentro de caixas" promete e do que Oracle Budgetary Control e SAP FM fazem, que é aplicar o controle em vários níveis ao mesmo tempo. O disponível de cada nó continua sendo o dele: consumedSubtree é o que a política do ancestral avalia (RN-O31).

O motor de aprovação usa isto como critério, e nada mais

O motor lê signal e wouldExceedBy como critérios de política — "estourou a verba" acrescenta um nível com a controladoria, exatamente como "valor acima de X" acrescenta um diretor. O motor não conhece verba e não grava movimento; quem grava é o agregado do documento, na transição — chamado pelo contrato de desfecho do processo, na mesma transação da decisão (RN-AP-38). A dependência é num sentido só, como já é com centro de custo.

Segurança

Quem vê o semáforo e quem vê o número

Em muitas empresas o valor da verba é informação de diretoria, e o requisitante não deveria saber que sobraram R$ 3 mil no mês — mas precisa saber que está prestes a estourar. As duas coisas são permissões separadas.

PermissãoO que libera
read:budget-statesO bloco signal + reason + o texto explicativo. Sem valores. Semeada por padrão no papel de requisitante — sem ela o produto volta a aprovar no escuro, que é o problema que esta spec existe para resolver.
read:budget-amountsO bloco amounts, recortado: só para os nós de centro de custo em que a pessoa tem CostCenterAssignment vigente, com rollup para os descendentes. Dono de "Operações" vê os números de toda a subárvore; dono de "Elétrica" vê os da dele. A linha coringa segue o mesmo recorte pelo nó que ela tem: a coringa comum — costCenterId preenchido, glAccountId nulo, que é a linha que a Decisão 4 manda cadastrar — é vista por quem responde por aquele nó, como qualquer outra. A linha sem centro de custo nenhum (as duas dimensões nulas, verba da empresa inteira) não tem nó por onde recortar: ela só aparece com read-all:budget-amounts, e o sinal continua indo para todo mundo por read:budget-states. Não é falha do recorte — é o que "verba de ninguém em particular" significa.
read-all:budget-amountsRemove o recorte. Controladoria, financeiro, diretoria.
read:budgetsAs telas do módulo: lista de exercícios, linhas, extrato. Implica ver valores dos exercícios que a pessoa alcança pelas duas regras acima.

O corte é no servidor. Sem read:budget-amounts, o objeto amounts não vai na resposta — não vai zerado, não vai mascarado, não vai escondido por CSS. Vale para a API, para o CSV exportado e para o payload de notificação. Uma permissão que só apaga o campo na tela não é permissão, é decoração: o número continua indo pela rede e aparece no primeiro DevTools aberto.

Isto não toca a plataforma. As permissões são linhas no catálogo que o gryd já mantém por tenant, e chegam achatadas no token como qualquer outra — nenhuma entidade do gryd muda, e a geração do token continua igual. O recorte por nó não vem do token: é resolvido no servidor a cada requisição, a partir de CostCenterAssignment, que já existe e já é a fonte de "quem manda em qual pedaço da árvore".

Tempo

Baldes, competência e carry-forward

A competência é a data de necessidade

O estágio define a competência, conforme RN-O11. Requisição de dezembro para necessidade de janeiro reserva janeiro; recebimento em outra data registra seu fato e liquida a imputação vigente da obrigação. Contabilização e competência original permanecem identificadas.

Por quê: é a leitura econômica correta e mata a corrida de fim de ano, em que todo mundo empurra pedido em dezembro para não perder verba. E resolve de graça o problema mais chato da virada — o compromisso que atravessa o ano já nasce no ano certo.

O que custa: mudar a data de entrega remaneja consumo entre baldes. O tratamento é o mesmo de sempre: dois movimentos com entryReason = DateChange, negativo no balde velho e positivo no novo, e se o novo balde não couber, vale a política de estouro daquele nó. Nunca um UPDATE em competencePeriodKey.

PeriodOnly × Cumulative — e por que nenhum dos dois transporta nada

O desenho óbvio de carry-forward é transportar: no fechamento de março, movimento negativo em março e positivo em abril. Funciona e arrasta duas coisas pesadas — fechamento mensal vira operação obrigatória (alguém precisa declarar que março acabou) e a nota atrasada quebra o fechamento (reabrir período, recalcular transporte, reescrever abril). Todo ERP que faz assim tem uma tela de reabertura de período e um chamado por mês por causa dela.

Aqui é modo de leitura. O razão é idêntico nos dois casos; muda a janela do somatório. Cumulative é carry-forward, exato, sem linha de transporte, sem fechamento e sem reabertura: a nota de março que chega em 8 de abril entra na soma e o disponível de abril se corrige sozinho — que é o comportamento certo, e no modelo de transporte seria um incidente.

O que se perde: o registro formal "em 31/03 foram transportados R$ 12.400". Isso vira consulta, renderizada na tela de acompanhamento, e não um fato gravado. Se algum cliente de setor regulado exigir o registro imutável, o fechamento formal nasce depois como opt-in — o razão já suporta, bastam os movimentos de virada — negativo e positivo nos próprios estágios, com entryReason = YearEndRollover, que é a mesma mecânica da virada de exercício (RN-O13).

Cumulative nunca atravessa exercício

A janela do acumulado é periodStart do exercício até o balde consultado. Em 1º de janeiro ela reinicia, sempre, qualquer que seja a política. O que sobrou de 2026 morre em 2026. Atravessar a virada é assunto da seção seguinte, tem regra própria e exige decisão explícita — não pode ser efeito colateral de um modo de leitura.

A pergunta difícil

O que acontece com o que está aberto em 31 de dezembro

Em 31 de dezembro existem requisições aprovadas que ainda não viraram pedido, pedidos emitidos que ainda não chegaram, e entregas feitas cuja nota ainda não veio. Cada uma dessas três situações tem um pedaço de verba de 2026 preso nela. A pergunta é o que fazer com esse dinheiro — e é uma pergunta de dono, não de sistema.

Como o mercado resolve
SistemaRegraComentário
Oracle General LedgerTrês regras à escolha: Encumbrances Only, Encumbrances and Encumbered Budget, Funds Available. Sem processo de virada, todo compromisso vai a zero.O compromisso é levado como saldo inicial do novo ano, não como movimento do período. A terceira regra é o que aqui já é o modo Cumulative, e não precisa existir na virada.
Microsoft Dynamics 365Duas opções: processar sem carregar orçamento, ou processar carregando orçamento. Nas duas, o compromisso é revertido no ano que fecha e restabelecido no novo.Carregar o orçamento gera ajuste que reduz o ano velho e recria no novo. Data de fechamento é o último dia do exercício; a de abertura, o primeiro do seguinte.
PeopleSoft / Oracle FSCMKeep Budget Active (o orçamento atravessa) ou Close and Reestablish (fecha, recria com valores novos, mantém as transações abertas vivas).O objetivo declarado é não obrigar a redigitar documento. A transação sobrevive à virada em qualquer das opções.
Setor público brasileiroRestos a pagar: o empenho não pago continua onerando o exercício em que nasceu, inscrito e carregado adiante.Contraexemplo deliberado. O resultado conhecido é uma fila que se acumula entre exercícios e um orçamento vigente que nunca é o orçamento real. É regra de direito financeiro público, não boa prática de gestão privada.

A convergência

Os três produtos privados fazem a mesma coisa e discordam só num ponto. Iguais: o compromisso aberto atravessa a virada — é revertido no exercício que fecha e recriado no novo, sem que ninguém redigite documento. Diferentes: se a verba vai junto — e nos três isso é opção do cliente, não do produto.

A recomendação · CarryCommitmentOnly como padrão

Ao fechar o exercício, cada saldo aberto de Commitment e Obligation gera um movimento negativo no próprio estágio no exercício que fecha e um positivo no mesmo estágio no exercício novo, os dois com entryReason = YearEndRollover e sourceType = Rollover, mantendo a mesma cadeia de documento, com competenceDate remapeada para o primeiro balde aberto e a taxa de câmbio preservada. A verba não vai junto.

Corrigido em 09/09/2026 — e é o que faz a Decisão 8 ser verdade. Até a v1.7 a virada usava dois estágios próprios, CarryOut e CarryIn, que não entravam no somatório de consumed e não tinham campo em amounts: o compromisso "carregado" era invisível, janeiro abria com a verba integral e o aperto de dezembro simplesmente sumia — o oposto exato de "aperto vira estouro em janeiro". Gravando nos próprios estágios, o carregado consome desde o primeiro dia, sem enum novo e sem campo novo na consulta de saldo (RN-O13).

Na visão do sócio. O número de 2027 foi decidido para 2027. Se o compromisso trouxesse verba automaticamente, o ano começaria comprometido por um valor que ninguém aprovou — e no primeiro mês o controle vira ficção: quanto mais atraso houve em dezembro, mais orçamento aparece do nada em janeiro, o que premia exatamente o comportamento que ele quer evitar. Com o padrão, o que sobrou de 2026 morre em 2026 e qualquer aperto aparece como estouro em janeiro — que é o momento em que ele quer ser consultado, com nome, valor e documento na frente.

Na visão do requisitante. O documento não morre, não precisa ser redigitado e não volta para a fila de aprovação: a cadeia já cumprida é preservada, pela mesma regra do motor de que continuidade e redução não reavaliam. Ele só descobre que houve virada se houver aperto real — e nesse caso o que ele vê é a política de estouro do nó dele, um caminho que ele já conhece, e não uma mensagem de erro sobre exercício encerrado.

As duas outras opções, e quando fazem sentido

CarryCommitmentAndBudget emite, junto com o movimento positivo do estágio no exercício novo, um Adjustment de mesmo valor na linha desse exercício, e um Adjustment negativo no exercício que fecha — os dois com origem Rollover, que é a terceira e última porta de entrada de Adjustment junto com BudgetChangeRequest e Import (RN-O28). É o caso de obra e projeto plurianual, em que a verba foi aprovada para o projeto e não para o calendário. Ressalva registrada: para esses casos a resposta melhor é um Budget com periodStart/periodEnd cobrindo o projeto inteiro — a opção existe como saída para quem já tem o orçamento partido por ano e não quer remodelar.

CancelOpen encerra somente parcelas cujo cancelamento de negócio seja válido. Requisição sem obrigação efetiva pode voltar para redecisão; pedido emitido e mínimo contratual permanecem vigentes até alteração ou rescisão válida. Parcelas ainda devidas atravessam a virada por reclassificação balanceada, nunca por apagamento automático da obrigação.

Nota atrasada de dezembro

Fato tardio permanece registrável. Até lateActualsUntil, lançamento na competência original pode ser autorizado; depois, período aberto com originalCompetenceDate e referência ao recebimento/aceite e à imputação vigente da obrigação. Não exigir obrigação ainda aberta no exercício antigo nem cobrar novamente saldo já transportado.

Documento em aprovação na virada

Requisição em aprovação não tem Commitment. Se a competência planejada fechar antes da conclusão, devolver para corrigir a revisão e reavaliar período, política e verba. Não mover silenciosamente para o primeiro balde aberto preservando assinaturas sobre conteúdo diferente.

Moeda

Uma moeda por exercício, taxa congelada no movimento

O cadastro de moeda, os pares, a rotina de captura, a regra de resolução da taxa e o serviço de conversão saíram desta spec. Eles são transversais — o Catálogo, o Fornecedor, a Cotação, o Pedido e a Nota consomem os mesmos — e vivem agora na especificação de Moeda e Câmbio. O que fica aqui é só o que é do orçamento: qual moeda o exercício tem, o que ele congela e como a variação cambial aparece no razão.

O exercício tem uma moeda; os documentos têm a que tiverem

Company.functionalCurrency — que já existia na spec de Empresa como metadado informativo e passa a ter consequência a partir daqui — define em que moeda a pessoa jurídica orça e presta contas. Padrão BRL, e qualquer ISO 4217 já na v1 — a restrição "só BRL na v1" saiu em 09/09/2026: um grupo com uma empresa em USD e outra em BRL é o caso que o produto declara atender, e a restrição fazia quatro páginas discordarem entre si. O que continua valendo é a ressalva de fonte: o boletim do Banco Central publica moeda contra real e mais nada, então par sem cotação vigente recusa, e não se estima taxa cruzada. Budget.currency é copiada dali na criação e congelada.

Um exercício não tem duas moedas. Empresa que quer acompanhar o mesmo orçamento em real e em dólar está pedindo relatório com conversão de apresentação, não um segundo orçamento — e dois orçamentos concorrentes para o mesmo período colidiriam com a regra de não-sobreposição, que existe justamente para não haver duas verdades.

A conversão acontece uma vez, na submissão do documento, e o razão herda — corrigido em v1.8

Cada BudgetEntry guarda amountDocument e currencyDocument como estão no documento, mais exchangeRate, quoteUnit, rateType e rateDate, e o amount já convertido para a moeda do exercício. Como valores, não como chave estrangeira — se a cotação for substituída depois por retificação de boletim, o movimento gravado não muda.

Quem escolhe o número é a submissão, não o lançamento. Até a v1.7 esta spec dizia que a conversão acontecia "no movimento", com resolução própria que podia até recusar o lançamento — e o Fluxo dizia que a taxa é a da submissão, "o único momento de conversão na vida do documento". Como o Commitment nasce dias depois, na conclusão da cadeia, as duas regras davam números diferentes para o mesmo dinheiro. Vence a submissão: todo movimento originado por documento copia exchangeRate, quoteUnit, rateType e rateDate do documento; o negativo de liquidação copia da entrada que liquida (RN-O30). A resolução própria de taxa desta spec passa a valer só para movimento sem documento de origem — Manual e Import (RN-O41). Cada documento seguinte da cadeia converte na sua própria submissão, e é dessa diferença que nasce a variação cambial abaixo.

Reavaliação retroativa não existe. Se a taxa de ontem mudasse o consumo de hoje, o saldo mudaria sozinho durante a noite e ninguém conseguiria explicar para o gestor por que a verba encolheu sem ninguém ter comprado nada.

A variação cambial aparece sozinha, e é consumo de verdade

Pedido de US$ 10.000 a 5,20 grava Obligation de R$ 52.000. A nota chega com o dólar a 5,60: a liquidação estorna o empenho pela taxa dele (−52.000) e lança Actual pela taxa nova (+56.000). Os R$ 4.000 de diferença consomem verba, porque consumiram caixa. Nenhum tratamento especial, nenhuma conta de ajuste — e o relatório de variação cambial é a soma dos deltas das cadeias cuja currencyDocument difere da moeda do exercício.

Converte-se a fatia, e a taxa que faltou recusa o documento inteiro

O rateio acontece na moeda do documento e cada CostAllocation converte separadamente — converter o total e depois ratear faz a soma das linhas do extrato não bater com o total do documento. Sem cotação para o par, o tipo e a janela, o lançamento é recusado com EXCHANGE_RATE_MISSING_UNPROCESSABLE 422 e a atomicidade por documento vale igual: nada é gravado. O sistema nunca estima taxa.

A janela de tolerância para trás, o tipo padrão e o comportamento em fim de semana e feriado são de CurrencySettings — a entidade que rascunhos anteriores desta spec chamavam de BudgetSettings. A rateDate efetivamente usada e a idade em dias voltam na conversão, são gravadas no movimento e aparecem no extrato: "US$ 10.000 × 5,2013 · PTAX venda de 29/12 (2 dias)".

Taxa contratada

Compra com câmbio travado usa a taxa do contrato, não a do dia: o documento carrega exchangeRateOverride com rateType = Contracted, exige a permissão override:exchange-rates e registra quem informou e com que referência. É a única forma de a taxa não vir da resolução, e ela fica visível no extrato da verba.

Contrato vigente · revisão consolidada

Estouro, aprovação complementar e última decisão

PolíticaNovo compromissoFato ocorrido
WarnPermite, registra excesso e avisa os responsáveis.Registra e marca a divergência.
BlockRecusa quando excede a tolerância em qualquer limite.Registra Actual; não impede recebimento ou aceite ocorrido.
RequireApprovalExige Supplemental aplicável, concluída para cada escopo e valor excedido. Sem cobertura, não conclui financeiramente.Registra o fato, a pendência e o encaminhamento; aprovação posterior não altera a data do fato.

A tolerância é o menor valor entre o percentual e o montante configurados, quando ambos existem. O montante declara toleranceCurrency e converte para a moeda do exercício pela taxa registrada na avaliação. Dentro da tolerância: Warning; fora: Exceeded. Ausência de verba é Uncovered, sem tolerância.

Última aprovação sem verba: na mesma transação, gravar o voto válido, devolver documento e instância para Returned com motivo e valores faltantes, e registrar aviso durável. Não gravar Approved, Commitment ou nova Obligation. O retorno é um resultado de negócio ReturnedForBudget, distinto da exceção técnica que reverte a transação. Reenvio cria nova rodada; a assinatura só é reaproveitada se continuar válida pela revisão, conteúdo, valor, política e evidências.

Falha de configuração de RequireApproval é impeditiva na execução; avisos de publicação e vencimento são auxiliares. Não existe permissão para ignorar Block. Corrigir verba ou política exige o processo correspondente, com histórico. Materialidade da aprovação não exclui nenhuma fatia da avaliação orçamentária.

Contrato vigente · revisão consolidada

Importação, backfill e reorçamento

A primeira entrega admite importar um orçamento autorizado externamente, registrando origem, versão, aprovador externo informado, usuário importador habilitado e evidência. Planejamento colaborativo usa Draft → InReview → Approved e o processo BUDGET; é uma evolução de experiência, não condição para registrar compras.

Backfill balanceado. Ativar exercício iniciado reclassifica os saldos econômicos ainda imputados fora do plano: para cada parcela, negativo na imputação antiga e positivo na nova, no mesmo estágio e valor. Ambos referenciam a origem por reclassifiesEntryId e pertencem ao mesmo grupo. Nunca copiar positivos isolados nem relançar parcelas já liquidadas como abertas. Actual líquido também é reclassificado. Soma consolidada antes = soma depois.

Reorçamento. A nova versão passa por aprovação, substitui a anterior atomicamente e transfere a imputação do consumo por pares balanceados. Movimentos históricos continuam imutáveis; uma cadeia aponta para a imputação vigente que futuras liquidações devem usar. Ajustes pontuais seguem BudgetChangeRequest. Uma versão nova não renova automaticamente o poder de gasto já consumido.

A ativação bloqueia o perímetro do exercício e suas configurações para concorrer com novas compras sem intervalo descoberto. Prévia apresenta movimentos, pendências sem linha e saldos; inconsistência impede a ativação. O relatório registra contagens, somas e chave da operação. Fatos pendentes continuam visíveis enquanto o plano é corrigido.

Carga

Importação em lote

Mesma mecânica já fechada na importação de centro de custo, com as diferenças que o orçamento impõe:

  • Dry-run obrigatório. A primeira chamada valida e devolve o relatório; a segunda, com o token daquele relatório, aplica. Não existe importar sem ter visto o que vai acontecer.
  • Tudo ou nada. Uma linha inválida recusa o arquivo inteiro, com a lista completa de problemas por número de linha — nunca "importadas 812 de 1.000".
  • Idempotente por chave de negócio — (código do centro de custo, código da conta, período), e não por posição no arquivo. Reenviar o mesmo arquivo não duplica nada e permite corrigir uma célula e reenviar.
  • Referência por código, não por id. A planilha traz o código do centro de custo e o da conta, como o financeiro escreve; a resolução para id é do servidor, e código inexistente ou nó inativo é erro nomeado.
  • Nó sem allowsPosting é aceito — verba em nó intermediário é o caso normal.
  • Depois de Approved, importação não altera valor. Ela só cria linhas que não existiam; alterar exige BudgetChangeRequest. Sem isso, a planilha reenviada por engano vira alteração orçamentária sem aprovação — que é exatamente o buraco que o documento existe para fechar.

Fronteiras

Relação com os demais domínios

DomínioO que ele faz com o orçamento
RequisitionConsulta na digitação (semáforo por fatia). Grava Commitment na conclusão da cadeia de aprovação, não na submissão — rascunho e documento em aprovação não seguram verba, senão o saldo fica preso em intenção que talvez nunca exista. Encerrar a requisição libera o resíduo.
PurchaseOrderGrava Obligation na emissão e liquida o Commitment da requisição de origem na proporção. Pedido sem requisição gera Obligation direto, sem liquidação. Encerramento libera o resíduo. Cancelar o pedido estorna o Obligation e restitui o Commitment que ele havia liquidado, na mesma transação (RN-O29).
ReceiptGrava Actual pelo que efetivamente entrou e liquida o Obligation. É o recebimento, e não a nota, que marca o realizado — a nota pode não chegar nunca e a mercadoria está lá.
InvoiceNão grava consumo novo no caminho normal: o realizado já veio do recebimento. Diferença de preço aceita no three-way match grava Actual adicional — ou o par estorno + relançamento de Actual —, com entryReason = Post e o reasonCode que a pessoa escolheu ao aceitar, do uso PriceVarianceAccept do catálogo de Motivo. Corrigido em 09/09/2026, duas vezes: mandava gravar Adjustment, e Adjustment aumenta a verba (RN-O28); e o valor PriceChange estava do lado da máquina, quando quem aceita uma nota mais cara é uma pessoa, com uma razão que muda de cliente para cliente — variação cambial, frete não previsto, reajuste contratual.
ContractMínimo obrigatório gera Obligation na ativação, por competência/programação. Liberações transferem parcelas ao pedido sem duplicar. Sem mínimo, consumo nasce nas liberações. Compromissos futuros controlados exigem cobertura aprovada para seus períodos; a falta impede assumir o mínimo.
Motor de aprovaçãoLê signal e wouldExceedBy como critérios. Não grava movimento e não conhece verba. Quem grava é o agregado do documento, na transição de estado, na mesma transação da decisão que a provocou — o motor invoca o contrato de desfecho do processo e o módulo dono empenha ali (RN-AP-38). O evento de domínio do motor sai depois do commit e é aviso, nunca a via do empenho.
CostCenterFornece path para o rollup, CostCenterAssignment para o recorte de visibilidade e a regra de resíduo do rateio. Não ganha campo novo — as políticas moram em BudgetPolicy.
GLAccountSegunda dimensão, com o mesmo rollup por path. Verba em "Despesas Operacionais" cobre toda conta abaixo dela.
CompanyFornece functionalCurrency, a moeda funcional do exercício. BudgetEntry, rateio e documento têm a mesma empresa. Cotação centralizada pode agrupar demanda de empresas distintas, mas cada pedido tem um destinatário fiscal e rateio exclusivamente em sua empresa.
Currency · ExchangeRateFornece o catálogo de moedas, a resolução da taxa e o serviço de conversão. O orçamento é o primeiro consumidor e não o dono: o mesmo serviço atende Cotação, Pedido e Nota. Ver Moeda e Câmbio.
AttachmentO Budget é dono de anexo desde 09/09/2026 (ownerType = Budget): planilha de premissas, ata que aprovou a verba, ofício de suplementação — com APPROVAL_EVIDENCE entre os tipos aceitos, e não só GENERAL, porque a ata que sustentou uma suplementação é o primeiro arquivo que a auditoria pede e a retenção de GENERAL a mataria junto com o documento. StateOf devolve Closed no exercício arquivado, nunca Missing. As onze respostas do descritor estão declaradas na página do Anexo, em O dono polimórfico — este módulo implementa, não redecide.
EstablishmentNenhuma relação. Orçamento é da pessoa jurídica; estabelecimento é dimensão fiscal. Se um dia um cliente quiser orçar por filial, a saída já existe e é a árvore de centro de custo.
Item · CategoryNode · SupplierNenhuma relação, escrita de propósito. Categoria sugere conta contábil, e é a conta que entra na chave da verba. Nunca derivar verba de item, categoria ou fornecedor.

API

Endpoints

RotaPermissãoDevolveErros
GET /budgetsread:budgetsPagedResult<BudgetSummaryDto>400
GET /budgets/{id}read:budgetsBudgetDetailDto404
POST /budgetscreate:budgetsBudgetDetailDto400 · 409 · 422
PUT /budgets/{id}update:budgetsBudgetDetailDto400 · 404 · 409 · 422
POST /budgets/{id}/submitupdate:budgetsBudgetDetailDto404 · 409 · 422
POST /budgets/{id}/closeclose:budgetsBudgetCloseReportDto404 · 409 · 422
POST /budgets/{id}/versionscreate:budgetsBudgetDetailDto404 · 409
GET /budgets/{id}/linesread:budgetsPagedResult<BudgetLineDto>400 · 404
PUT /budgets/{id}/linesupdate:budgets204 · só em Draft404 · 409 · 422
POST /budgets/{id}/lines:importimport:budgets?dryRun=true devolve o ImportReportDto sem gravar; sem o parâmetro, aplica. Tudo ou nada, nunca parcial — a mecânica do centro de custo, que é a canônica400 · 409 · 422
POST /evaluate:budget-stateread:budget-statesarray de BudgetStateDto400 · 422
GET /budget-lines/{id}/entriesread:budgetsPagedResult<BudgetEntryDto> — o extrato403* · 404
GET /budget-entriesread:budgetsRazão filtrável por documento, nó, conta e período400
GET /budget-entries/residuesread:budgetsNovo. Resíduo aberto de Commitment e Obligation por documento de origem e fatia, com a idade em dias desde o último movimento da cadeia. Filtra por nó, período, faixa de idade e dono. É leitura sobre o razão e a projeção — sem tabela nova (RN-O35)400
POST /budget-change-requestscreate:budget-change-requestsBudgetChangeRequestDto400 · 422
POST /budget-change-requests/{id}/submitcreate:budget-change-requestsBudgetChangeRequestDto404 · 409 · 422
GET /budget-policies/effectiveread:budgetsEffectivePolicyDto com inheritedFrom por campo400 · 404
PUT /budget-policiesupdate:budget-policies204400 · 409 · 422

* 403 aqui é a única exceção à regra de responder 404: o extrato de uma linha que a pessoa pode ver como semáforo mas não pode ver em valores é recusa de detalhe, não de existência. Para exercício ou linha de outro tenant, continua 404.

Não há endpoint público de lançamento. BudgetEntry é gravado por contrato de domínio interno, chamado pelos agregados de documento dentro da própria transação. Expor "lançar movimento" como rota seria abrir uma porta lateral para consumir verba sem documento — e o razão perderia a propriedade que o torna útil, que é toda linha ter origem rastreável.

Contrato

Erros

CódigoHTTPQuando
BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE422Escopo controlado e nenhuma linha cobre a combinação. A mensagem nomeia centro de custo, conta e período, e sugere a linha coringa.
BUDGET_EXCEEDED_UNPROCESSABLE422Política Block e o valor ultrapassa disponível + tolerância. Traz available e wouldExceedBy por fatia. É o erro que o motor de aprovação repassa ao recusar a submissão — ele não reembrulha nem cria um código próprio.
BUDGET_PERIOD_CLOSED_UNPROCESSABLE422Compromisso novo com competência em exercício Closed ou Archived.
BUDGET_OVERLAPPING_PERIOD_CONFLICT409Aprovar exercício cujo período se sobrepõe a outro já aprovado da mesma empresa.
BUDGET_LINE_ALREADY_EXISTS409Mesma combinação de dimensões e período já existe no exercício.
BUDGET_LINE_IMMUTABLE_CONFLICT409Tentativa de alterar valor de linha em exercício aprovado sem BudgetChangeRequest.
BUDGET_TRANSFER_INSUFFICIENT_UNPROCESSABLE422Remanejamento deixaria a linha de origem negativa, revalidado no instante da aprovação.
BUDGET_GRANULARITY_IMMUTABLE_CONFLICT409Mudar periodGranularity ou currency depois de aprovado.
EXCHANGE_RATE_MISSING_UNPROCESSABLE422Sem cotação para o par, tipo e data. Nunca se estima taxa.
BUDGET_ENTRY_IMMUTABLE_CONFLICT409Qualquer tentativa de UPDATE ou DELETE no razão. Recusado no banco, não na aplicação.
BUDGET_CONCURRENCY_CONFLICT409expectedVersion divergente do rowVersion atual do exercício ou da linha.

Segurança

Permissões

PermissãoSemeada emObservação
read:budget-statesRequisitante, Comprador, AprovadorSemáforo e razão. Sem ela o produto perde o motivo de existir.
read:budget-amountsGestor de centro de custoValores, recortados pelos nós com vínculo vigente, com rollup.
read-all:budget-amountsControladoria, DiretoriaRemove o recorte.
read:budgetsControladoria, Gestor de CCTelas do módulo e extrato.
create:budgetsControladoriaCriar exercício e versionar.
update:budgetsControladoriaEditar linhas em Draft e submeter para aprovação.
close:budgetsControladoriaFechar exercício e rodar a virada. Separada de update de propósito.
import:budgetsControladoriaCarga em lote.
create:budget-change-requestsGestor de centro de custoPedir suplementação ou remanejamento. Aprovar é do motor.
update:budget-policiesAdministradorLigar controle, definir estouro, tolerância e virada.
read:exchange-ratesControladoria, CompradorConsultar cotações. Definida em Moeda e Câmbio.
manage:exchange-ratesControladoriaCadastrar taxa manual ou contratada. Definida em Moeda e Câmbio.
override:exchange-ratesNinguém, por padrãoInformar taxa contratada num documento. Registra autor e referência.

Todas são linhas no catálogo de permissões que o gryd já mantém por tenant, e chegam achatadas no token como qualquer outra. Nenhuma entidade da plataforma muda, e a geração do token continua igual.

Invariantes

Regras de negócio

RN-O01O razão é a verdade

BudgetEntry aceita apenas INSERT e SELECT. UPDATE e DELETE são recusados por grant e por trigger. Toda correção é movimento novo.

RN-O02Saldo é projeção reconstruível

BudgetLineBalance é atualizada na mesma transação do movimento e pode ser reconstruída integralmente a partir do razão. Divergência entre as duas é falha de integridade, verificada por rotina em homologação.

RN-O03Travar todo limite afetado

Checar e gravar exigem travas sobre folha, ancestrais, limites sobrepostos e janelas cumulativas, incluindo guardas de configuração, em ordem determinística. Consulta anterior não autoriza gravação.

RN-O04Repetição devolve o resultado

Operação repetida com o mesmo conteúdo devolve seu resultado persistido. Mesmo identificador com conteúdo diferente é conflito. Grupo, entradas e resultado são atômicos.

RN-O05Liquidação proporcional

Cada estágio liquida o anterior na proporção do que avançou, com liquidatesEntryId preenchido. O resíduo só é liberado no encerramento explícito do documento de origem.

RN-O06Atomicidade por documento

Todas as fatias de todas as linhas de um documento são avaliadas e gravadas juntas. Se uma bloqueia, nada é gravado, e o erro devolve a lista completa das que falharam.

RN-O07Um exercício aprovado por período e empresa

Sobreposição de intervalos entre Budget aprovados da mesma empresa é recusada por EXCLUDE + btree_gist, no banco.

RN-O08Resolução determinística

Especificidade, depois profundidade do centro de custo, depois profundidade da conta. O critério é total: não existe empate e não existe escolha arbitrária.

RN-O09Ausência de controle é explícita

NotControlled exige política sem controle. Ausência de exercício ou linha em escopo controlado é Uncovered e impede novos compromissos. Fatos ocorridos seguem RN-O33.

RN-O10Movimento sempre gravado

Inclusive onde não há controle, com a tupla de dimensões preenchida e budgetLineId nulo.

RN-O11Competência pertence ao fato

Necessidade, promessa de entrega, recebimento/aceite, período de serviço e contabilização são datas distintas. Cada estágio usa sua competência; reclassificar mantém a origem e grava pares balanceados.

RN-O12O modo cumulativo não atravessa exercício

A janela do modo Cumulative começa em periodStart e termina no balde consultado. Reinicia a cada exercício, sempre.

RN-O13Virada conserva obrigação e consumo

Transportar somente parcelas abertas de Commitment e Obligation, por negativo na imputação anterior e positivo na seguinte no mesmo estágio, com YearEndRollover. CancelOpen exige cancelamento de negócio válido; não extingue pedido efetivo ou mínimo contratual por simples fechamento. Carregar verba só em CarryCommitmentAndBudget. Operação idempotente e balanceada.

RN-O14Fato tardio não desaparece

Até lateActualsUntil, política de fechamento pode autorizar competência original. Depois, lançar ajuste no período aberto com originalCompetenceDate e referência ao fato/obrigação, inclusive sua imputação transportada. Nunca exigir obrigação ainda aberta no exercício antigo para registrar entrega ocorrida.

RN-O15Taxa congelada, jamais reavaliada

A conversão acontece uma vez, na submissão do documento, e o movimento herda o número (RN-O41). Sem cotação disponível para o par, o tipo e a janela, a submissão é recusada; nunca estimada. Revista em 09/09/2026: dizia "no movimento", o que fazia o empenho gravado dias depois converter de novo e divergir do valor que a alçada avaliou.

RN-O16Uma moeda por exercício

Budget.currency vem de Company.functionalCurrency e é imutável após a criação.

RN-O17Um contrato de arredondamento

numeric(21,4) armazena dinheiro, com arredondamento HALF_UP nas casas da moeda, de zero a quatro. Cálculo mantém precisão decimal até o ponto canônico. Resíduo de rateio: maior fatia, empate pelo menor order. Total convertido é soma das fatias convertidas; não há segundo resíduo cambial.

RN-O18Verba aprovada só muda por documento

Depois de Approved, amount é imutável. O valor efetivo é amount + Σ Adjustment, e todo ajuste tem autor, data e justificativa.

RN-O19Remanejamento revalidado na aprovação

O saldo da origem é conferido no instante da aprovação, sob trava, e não no momento do pedido.

RN-O20Política herdada campo a campo

Cada campo de BudgetPolicy sobe o path independentemente. A API sempre devolve de qual nó cada valor veio.

RN-O21Vigência não se edita para trás

Mudar política é encerrar a vigente e abrir outra. Sobreposição recusada por EXCLUDE.

RN-O22Valores são cortados no servidor

Sem read:budget-amounts, o bloco de valores não vai na resposta — nem na API, nem no CSV, nem na notificação.

RN-O23Não existe furar bloqueio

Nenhuma permissão ignora Block. O caminho para gastar mais é pedir mais verba ou mudar a política do nó, com registro.

RN-O24Mínimo contratual é obrigação

Contrato com mínimo obrigatório gera Obligation na ativação efetiva. Liberações transferem a parcela ao pedido, sem duplicar consumo. Contrato sem mínimo só gera obrigação nas liberações; ultrapassar o mínimo exige controle sobre a diferença.

RN-O25Escopo de tenant e empresa

Id de outro tenant ou de empresa fora do conjunto resolvido responde 404, nunca 403 — com a única exceção declarada do extrato de valores.

RN-O26Esta spec decide o bloqueio; o motor obedece

A pergunta "este documento passa?" é respondida por evaluate:budget-states, com a política resolvida no nó, a tolerância aplicada e o sinal por fatia. O Fluxo de Aprovação não tem parâmetro de bloqueio — tinha um budgetBlockBehavior por tenant, removido em 03/09/2026, porque dois donos da mesma decisão divergem no primeiro documento com fatias em centros de custo diferentes.

RN-O27RequireApproval exige cobertura concluída

Cada escopo excedido precisa de aprovação Supplemental correspondente, válida para revisão e valor. Ausência, expiração ou cadeia incompleta impede a conclusão financeira; marca e aviso são auxiliares. A configuração pode ser corrigida sem conceder aprovação automática.

RN-O28Adjustment só nasce de mudança de verba

Um movimento de estágio Adjustment exige sourceType ∈ {BudgetChangeRequest, Import, Rollover}. É invariante do estágio, verificada na gravação, e não há caminho de exceção. Nenhum evento de execução — recebimento, nota, match — grava Adjustment.

Por quê: Adjustment aumenta a verba, e a RN-O18 diz que verba aprovada só muda por documento. Deixar a nota mais cara gravar Adjustment criava orçamento pelo caminho de maior volume do sistema, sem justificativa e sem aprovação. Diferença de preço aceita é Actual adicional, ou o par estorno + relançamento de Actual.

RN-O29Estorno segue a parcela do fato

Cancelar ou devolver identifica a entrada e a parcela ainda elegível, e reverte somente seus efeitos vinculados. Cancelar saldo não entregue não estorna Actual ocorrido. Restituir demanda ou obrigação depende do desfecho de negócio; não há recursão indiscriminada pela cadeia.

RN-O30Liquidação segue a imputação vigente da origem

Negativo de liquidação copia balde, moeda e taxa da parcela de origem ou de sua reclassificação vigente. Positivo resolve a competência e a taxa do novo fato. Os vínculos preservam todo o caminho até a entrada original.

RN-O31Um lançamento, todos os limites

A linha vencedora recebe a entrada; cada limite aplicável inclui esse consumo uma vez. Avaliar toda a pilha de limites e aplicar todas as exigências, sem multiplicar consumo econômico. As mesmas guardas protegem a leitura e a gravação.

RN-O32Backfill e reorçamento são balanceados

Transferem imputação por pares negativos/positivos no mesmo estágio, ligados à origem e ao grupo da operação. Apenas o saldo líquido elegível é transferido. Ativação e reclassificação são atômicas; soma consolidada antes e depois é igual.

RN-O33Registrar o que ocorreu

Block e Uncovered impedem novos compromissos. Actual ocorrido permanece registrável mesmo sem verba ou classificação, com pendência, origem e aviso durável. Correção posterior não apaga o fato nem cria verba.

RN-O34Identidade da operação e de cada entrada

postingGroupId estável identifica o fato; entryOrdinal distingue todas as entradas que ele produz. Unicidade (tenantId, postingGroupId, entryOrdinal), com hash do conteúdo e resultado no grupo. Linha e fatia têm IDs estáveis; ordem visual não identifica uma operação.

RN-O35Resíduo aberto tem consulta, dono, idade e aviso

O resíduo continua saindo só pelo encerramento explícito do documento (RN-O05), e ganha sustentação: GET /budget-entries/residues lista o saldo aberto por documento e fatia com a idade em dias; o dono é o dono do nó de centro de custo da fatia, por effectiveOwners com rollup; resíduo acima da idade configurada notifica esse dono. O encerramento por completude fica declarado como gancho — a proposta automática nasce quando Requisition e PurchaseOrder existirem, e a decisão continua do dono.

Por quê: "ação de domínio com dono" sem consulta, sem envelhecimento e sem aviso é uma frase, não um controle — e resíduo de intenção morta come o disponível ao longo do ano.

RN-O36Classificação pendente é explícita

Commitment e Obligation exigem dimensões completas. Actual admite classificação pendente para preservar fatos, sem transformar ausência em NotControlled. Adjustment copia a dimensão da verba. Resolver pendência produz reclassificação balanceada.

RN-O37BudgetPolicy pode ter empresa

companyId anulável; nulo vale para todas as empresas do tenant. Política com empresa vence a de empresa nula para o mesmo campo e o mesmo nó, e as duas convivem no mesmo período. A não-sobreposição é apurada por (costCenterId, companyId).

Por quê: Budget é por empresa e a árvore de centro de custo é compartilhada. Sem a coluna, ligar o controle na empresa madura e deixá-lo desligado na recém-adquirida exigia duplicar nós por empresa — o que o nó corporativo existe para evitar.

RN-O38Materialidade não muda o que o orçamento vê

allocationMaterialityPercent afeta apenas a montagem da cadeia de aprovação. O orçamento sempre avalia e grava a decomposição integral do rateio, e Block numa fatia absorvida recusa a submissão do mesmo jeito.

Por quê: controle de verba não é roteamento de alçada. A alternativa cria uma marca que dispara no funcionamento normal, e marca que dispara todo dia é marca que ninguém lê.

RN-O39Sinais e impeditivos declarados

NotControlled é controle explicitamente desligado; Ok, Warning, Exceeded e Uncovered descrevem a avaliação. Uncovered retorna BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE para novos compromissos. Actual usa o mesmo sinal com pendência e não sofre essa recusa.

RN-O40Empresa do pedido é a empresa do rateio

BudgetEntry.companyId = CostAllocation.companyId = empresa do documento. Compra centralizada pode compartilhar cotação, mas gera pedidos separados por destinatário fiscal. Nenhuma fatia transfere custo entre pessoas jurídicas.

RN-O41A taxa vem da submissão; o razão herda

Movimento com documento de origem copia exchangeRate, quoteUnit, rateType e rateDate do documento, congelados na submissão dele; o negativo de liquidação copia da entrada que liquida (RN-O30). Resolução própria de taxa vale só para sourceType ∈ {Manual, Import}.

Por quê: o Commitment nasce dias depois da submissão. Com resolução própria, a alçada comparava um número e o razão gravava outro para o mesmo dinheiro.

Contrato vigente · revisão consolidada

Concorrência e idempotência

Antes de decidir, determinar o conjunto completo de guardas: empresa/exercício, linhas vencedoras, todos os ancestrais e limites sobrepostos, e cada janela cumulativa atingida pela gravação ou reclassificação. Obter guardas de configuração e de saldo em ordem global (tenantId, companyId, budgetId, guardType, limitId, periodKey). Linhas de guarda devem existir previamente ou ser criadas sob uma trava de perímetro; travar só saldos já existentes não protege uma janela vazia.

Revalidar configuração e versões após obter as travas, calcular o efeito líquido de todas as fatias, avaliar limites, inserir grupo e entradas, atualizar projeções e confirmar. Publicação de política, mudança de hierarquia, reorçamento e virada usam o mesmo protocolo. Dois filhos disputando a última verba do pai serializam no pai; lançamento retroativo em modo cumulativo trava também as janelas futuras afetadas. Deadlock ou conflito serializável permite retry integral com a mesma chave.

Unicidade de operação: (tenantId, postingGroupId), com hash de conteúdo e resultado persistido. Unicidade de entrada: (tenantId, postingGroupId, entryOrdinal). IDs de entradas, linhas, fatias e parcelas são estáveis. Dedupe e efeitos ocorrem na mesma transação; chave existente com hash diferente retorna conflito. O índice não substitui validar o conjunto completo do grupo.

Índices iniciais cobrem tenant, documento/linha/fatia de origem, entrada liquidada e limites/períodos. Consulta de digitação lê projeção sem trava e identifica sua atualidade. Particionamento só será adotado após medição de volume, planos de consulta e manutenção; a unicidade global e a reconstrução do razão precisam continuar demonstráveis.

Contrato vigente · revisão consolidada

Referências e hipóteses de produto

Razão append-only, controle por dimensões e verificação concorrente são decisões do Nexio. Comparações de superioridade ou equivalência funcional com produtos comerciais não foram validadas em configurações comparáveis. Avaliar esforço de implantação, uso de rateio e necessidade de bloqueio em piloto.

As garantias técnicas dependem do banco: restrições relacionais do PostgreSQL e controle explícito de concorrência são referências de implementação; não substituem os testes de conservação e disputa de verba.

Limites

Fora de escopo

O quePor quê
Planejamento financeiro (FP&A)Drivers de cálculo, cenários comparados, rateio de overhead, forecast rolante. É outro produto e concorre com o ERP e com a controladoria.
Orçamento de receitaO Nexio controla gasto. Receita orçada não tem consumidor nenhum aqui.
ContabilizaçãoNenhum lançamento contábil é gerado. O razão orçamentário é gerencial e não pretende ser diário contábil.
Fechamento mensal formalSubstituído pelo modo de leitura. Nasce depois, como opt-in, se um cliente de setor regulado exigir o registro imutável de transporte.
Orçamento por estabelecimentoOrçamento é da pessoa jurídica. Quem quiser recorte por filial usa a árvore de centro de custo, que já resolve.
Verba por fornecedor, item ou categoriaDimensão errada. Categoria sugere conta contábil, e é a conta que entra na chave.
Override de bloqueioDecisão explícita. Toda porta de fuga vira o caminho normal em três meses.
Reavaliação cambial periódicaMudaria o saldo sem ninguém ter comprado nada. Variação aparece na liquidação, que é quando ela é real.
Consolidação entre empresasSomar orçamento de várias empresas é relatório de grupo, e depende de moeda e plano de contas comuns. Fica para quando existir demanda declarada.

Contrato vigente · revisão consolidada

Grupo de lançamentos e pendência de valorização

BudgetPostingGroup guarda tenantId, postingGroupId, operationId, payloadHash, resultado, autor e data. Suas entradas têm entryOrdinal único e referências de origem/parcela. O grupo é gravado integralmente; uma chave com conteúdo diferente não é tratada como retry.

Recebimento ou aceite sem preço/taxa confiáveis registra o fato no módulo de origem com pendência de valorização, sem inventar Actual zero ou taxa 1. Quando os valores forem conhecidos, lançar Actual uma vez, preservando occurredAt e competência original. Pendências não valorizadas aparecem nos relatórios de cobertura, para que saldo conhecido não pareça gasto completo.