Ponto de partida
Dez decisões
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.
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.
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).
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.
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.
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.
É 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.
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.
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.
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
Draft → InReview → Approved → Closed → Archived. A versão é do exercício, nunca da linha.
path, independentemente dos outros.
BUDGET_CHANGE da semente do motor. Ao aprovar, emite movimentos de Adjustment.
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.
| Evento | Movimentos | Commitment | Obligation | Actual | Consumo |
|---|---|---|---|---|---|
| Requisição: 100 un × R$ 10 | Commitment +1.000 | 1.000 | 0 | 0 | 1.000 |
| Pedido: 60 un × R$ 11 | Commitment −600; Obligation +660 | 400 | 660 | 0 | 1.060 |
| Recebimento: 55 un | Obligation −605; Actual +605 | 400 | 55 | 605 | 1.060 |
| Encerrar as 5 un não entregues e a demanda restante | Obligation −55; Commitment −400 | 0 | 0 | 605 | 605 |
| Nota: diferença de R$ 10 aceita para as 55 un | Actual +10 vinculado ao Actual +605 | 0 | 0 | 615 | 615 |
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
- Resolver a política vigente por tenant, empresa e centro de custo, campo a campo, incluindo os limites ancestrais aplicáveis.
- Somente quando
requiresBudget = falseem todo o escopo aplicável, retornarNotControlled. Ainda assim, registrar o fato com suas dimensões. - 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. - 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.
- 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
Uncoverede 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
| Campo | Tipo | Regra |
|---|---|---|
id | uuid | — |
tenantId | uuid | Do token. Id de outro tenant responde 404, nunca 403. |
companyId | uuid | Obrigatório. Orçamento é da pessoa jurídica — é ela que tem sócio, conselho e prestação de contas. FK composta (tenantId, companyId). |
name | varchar(120) | Rótulo humano: "Orçamento 2027", "Obra Guarulhos 2027–2029". |
periodStart · periodEnd | date | O 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). |
periodGranularity | enum | Monthly | 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. |
currency | char(3) | ISO 4217. Copiada de Company.functionalCurrency na criação e congelada. Um exercício tem uma moeda só. |
version | int | Sequencial por (companyId, periodStart). Versão nova nasce como cópia da anterior em Draft. |
supersedesBudgetId | uuid? | A versão que esta substitui. A substituída vai para Archived no instante em que a nova é aprovada. |
status | enum | Draft | InReview | Approved | Closed | Archived. Só Approved e Closed participam da resolução; Draft e InReview são invisíveis para o consumo. |
origin | enum | Imported | Planned. Informativo — o ciclo de vida é o mesmo; muda só por onde as linhas entraram. |
approvalRequestId | uuid? | 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). |
lateActualsUntil | date? | 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. |
externalCode | varchar(60)? | Conciliação com o ERP. Único por tenant quando preenchido. |
rowVersion | int | Concorrê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. |
| De → para | Quem dispara | O que acontece |
|---|---|---|
Draft → InReview | Ação do usuário | Abre instância do processo BUDGET. Linhas viram somente-leitura. |
InReview → Approved | Motor de aprovação | Verifica 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 → Draft | Devolução no motor | Devolver não é rejeitar: a instância segue viva e as linhas voltam a ser editáveis. |
Approved → Closed | Ação com permissão própria | Roda a virada de exercício (adiante). Recusa Commitment e Obligation novos; aceita Actual até lateActualsUntil. |
| Closed → Archived | Após janela tardia e conciliação | Impede 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
| Campo | Tipo | Regra |
|---|---|---|
id · tenantId · budgetId | uuid | — |
costCenterId | uuid? | 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. |
glAccountId | uuid? | Nulo = qualquer conta. Mesma regra de subárvore. |
periodKey | varchar(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. |
amount | numeric(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. |
specificity | smallint | Derivado 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 · externalCode | varchar | Justificativa 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
| Campo | Tipo | Regra |
|---|---|---|
id · tenantId · companyId | uuid | companyId é obrigatoriamente a empresa do documento e da fatia. Escolhe o Budget consumido; pedidos centralizados são separados por destinatário fiscal (RN-O40). |
stage | enum | Commitment | 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). |
amount | numeric(21,4) | Com sinal, na moeda do exercício. Positivo consome (ou, em Adjustment, aumenta a verba); negativo libera. Zero é recusado. |
costCenterId · glAccountId | uuid? | 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 · competencePeriodKey | date · 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 · budgetLineId | uuid? | 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. |
resolutionReason | enum | NotControlled | Covered | Uncovered. Uncovered impede novos compromissos; Actual ocorrido pode ser registrado com pendência de classificação. |
currencyDocument · amountDocument | char(3) · numeric(21,4) | O valor como está no documento. Igual a amount e à moeda do exercício no caso comum. |
exchangeRate · quoteUnit · rateType · rateDate | numeric(18,8) · int · enum · date | A 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 · allocationId | enum · uuid | Requisition | 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 · sourceEntryId | uuid? | 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 · reclassifiesEntryId | uuid? | 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. |
entryReason | enum | BudgetEntryReason, 12 valores: Post, Liquidate, Reverse, DocumentCancelled, Return, ReceiptReversal, QuantityChange, DateChange, ResidueRelease, YearEndRollover, BudgetBackfill, BudgetReclassification. Obrigatório; mapeamento canônico em Motivo. |
reasonCodeId · reasonCode · reasonLabel | uuid? · 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 · idempotencyKey | uuid · 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 · createdByUserId | timestamptz · uuid | Quando o fato aconteceu no domínio e quando entrou no razão. userId do gryd, sem FK — mesmo padrão de CostCenterAssignment. |
originalQuantity · settledQuantity · sourceAllocationId | numeric(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 · fiscalPostingDate | date? | 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.
| Campo | Tipo | Regra |
|---|---|---|
costCenterId | uuid? | Nulo = padrão do tenant. Preenchido, vale para o nó e seus descendentes até que um descendente sobrescreva o mesmo campo. |
companyId | uuid? | 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 · validTo | date · 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. |
requiresBudget | bool? | Liga o controle. Padrão de fábrica falso. |
carryForwardMode | enum? | PeriodOnly | Cumulative. Padrão de fábrica PeriodOnly. |
overBudgetPolicy | enum? | 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 · toleranceCurrency | numeric(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. |
yearEndCommitmentPolicy | enum? | 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).
| Campo | Tipo | Regra |
|---|---|---|
number | varchar(30) | Via NumberSequence, escopo Company. |
type | enum | Supplement (dinheiro novo), Transfer (remanejamento entre linhas), Reduction (devolver verba). |
fromLineId · toLineId | uuid? | Transfer exige os dois; Supplement só toLineId; Reduction só fromLineId. Ambos precisam pertencer ao mesmo budgetId. |
amount | numeric(21,4) | Positivo. O sinal vem do type, não do número — assim ninguém digita menos-menos. |
reasonCodeId · reasonCode · reasonLabel · reasonComment | uuid · varchar(40) · varchar(120) · text | Obrigató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. |
effectiveDate | date | Data do movimento de Adjustment. Não pode ser anterior ao periodStart do exercício. |
status · approvalRequestId | enum · 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
}]
}
| Sinal | Quando | O que o requisitante vê |
|---|---|---|
NotControlled | Sem exercício, ou requiresBudget falso | Cinza · "sem controle orçamentário". Nenhum número, nem se a pessoa tiver permissão — não há o que mostrar. |
Ok | Cabe no disponível | Verde. Segue o baile. |
Warning | Estoura, mas dentro da tolerância | Âmbar · "vai consumir toda a verba de março". Passa, e o gestor recebe notificação. |
| Exceeded | Novo 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. |
| Uncovered | Ausência de exercício/linha apta em escopo controlado | Impede 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:
| Sinal | O serviço de lançamento | O motor de aprovação |
|---|---|---|
| Ok | Grava. | Nada. Não é critério. |
| Warning | Grava 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. |
| Exceeded | Novo 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. |
| Uncovered | Ausência de exercício/linha apta em escopo controlado | Impede 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ão | O que libera |
|---|---|
| read:budget-states | O 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-amounts | O 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-amounts | Remove o recorte. Controladoria, financeiro, diretoria. |
| read:budgets | As 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.
| Sistema | Regra | Comentário |
|---|---|---|
| Oracle General Ledger | Trê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 365 | Duas 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 FSCM | Keep 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 brasileiro | Restos 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ítica | Novo compromisso | Fato ocorrido |
|---|---|---|
| Warn | Permite, registra excesso e avisa os responsáveis. | Registra e marca a divergência. |
| Block | Recusa quando excede a tolerância em qualquer limite. | Registra Actual; não impede recebimento ou aceite ocorrido. |
| RequireApproval | Exige 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 exigeBudgetChangeRequest. 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ínio | O que ele faz com o orçamento |
|---|---|
Requisition | Consulta 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. |
PurchaseOrder | Grava 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). |
Receipt | Grava 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á. |
Invoice | Nã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. |
Contract | Mí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ção | Lê 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. |
CostCenter | Fornece 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. |
GLAccount | Segunda dimensão, com o mesmo rollup por path. Verba em "Despesas Operacionais" cobre toda conta abaixo dela. |
| Company | Fornece 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 · ExchangeRate | Fornece 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. |
Attachment | O 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. |
Establishment | Nenhuma 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 · Supplier | Nenhuma 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
| Rota | Permissão | Devolve | Erros |
|---|---|---|---|
| GET /budgets | read:budgets | PagedResult<BudgetSummaryDto> | 400 |
| GET /budgets/{id} | read:budgets | BudgetDetailDto | 404 |
| POST /budgets | create:budgets | BudgetDetailDto | 400 · 409 · 422 |
| PUT /budgets/{id} | update:budgets | BudgetDetailDto | 400 · 404 · 409 · 422 |
| POST /budgets/{id}/submit | update:budgets | BudgetDetailDto | 404 · 409 · 422 |
| POST /budgets/{id}/close | close:budgets | BudgetCloseReportDto | 404 · 409 · 422 |
| POST /budgets/{id}/versions | create:budgets | BudgetDetailDto | 404 · 409 |
| GET /budgets/{id}/lines | read:budgets | PagedResult<BudgetLineDto> | 400 · 404 |
| PUT /budgets/{id}/lines | update:budgets | 204 · só em Draft | 404 · 409 · 422 |
| POST /budgets/{id}/lines:import | import: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ônica | 400 · 409 · 422 |
| POST /evaluate:budget-state | read:budget-states | array de BudgetStateDto | 400 · 422 |
| GET /budget-lines/{id}/entries | read:budgets | PagedResult<BudgetEntryDto> — o extrato | 403* · 404 |
| GET /budget-entries | read:budgets | Razão filtrável por documento, nó, conta e período | 400 |
| GET /budget-entries/residues | read:budgets | Novo. 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-requests | create:budget-change-requests | BudgetChangeRequestDto | 400 · 422 |
| POST /budget-change-requests/{id}/submit | create:budget-change-requests | BudgetChangeRequestDto | 404 · 409 · 422 |
| GET /budget-policies/effective | read:budgets | EffectivePolicyDto com inheritedFrom por campo | 400 · 404 |
| PUT /budget-policies | update:budget-policies | 204 | 400 · 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ódigo | HTTP | Quando |
|---|---|---|
| BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE | 422 | Escopo 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_UNPROCESSABLE | 422 | Polí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_UNPROCESSABLE | 422 | Compromisso novo com competência em exercício Closed ou Archived. |
| BUDGET_OVERLAPPING_PERIOD_CONFLICT | 409 | Aprovar exercício cujo período se sobrepõe a outro já aprovado da mesma empresa. |
| BUDGET_LINE_ALREADY_EXISTS | 409 | Mesma combinação de dimensões e período já existe no exercício. |
| BUDGET_LINE_IMMUTABLE_CONFLICT | 409 | Tentativa de alterar valor de linha em exercício aprovado sem BudgetChangeRequest. |
| BUDGET_TRANSFER_INSUFFICIENT_UNPROCESSABLE | 422 | Remanejamento deixaria a linha de origem negativa, revalidado no instante da aprovação. |
| BUDGET_GRANULARITY_IMMUTABLE_CONFLICT | 409 | Mudar periodGranularity ou currency depois de aprovado. |
| EXCHANGE_RATE_MISSING_UNPROCESSABLE | 422 | Sem cotação para o par, tipo e data. Nunca se estima taxa. |
| BUDGET_ENTRY_IMMUTABLE_CONFLICT | 409 | Qualquer tentativa de UPDATE ou DELETE no razão. Recusado no banco, não na aplicação. |
| BUDGET_CONCURRENCY_CONFLICT | 409 | expectedVersion divergente do rowVersion atual do exercício ou da linha. |
Segurança
Permissões
| Permissão | Semeada em | Observação |
|---|---|---|
| read:budget-states | Requisitante, Comprador, Aprovador | Semáforo e razão. Sem ela o produto perde o motivo de existir. |
| read:budget-amounts | Gestor de centro de custo | Valores, recortados pelos nós com vínculo vigente, com rollup. |
| read-all:budget-amounts | Controladoria, Diretoria | Remove o recorte. |
| read:budgets | Controladoria, Gestor de CC | Telas do módulo e extrato. |
| create:budgets | Controladoria | Criar exercício e versionar. |
| update:budgets | Controladoria | Editar linhas em Draft e submeter para aprovação. |
| close:budgets | Controladoria | Fechar exercício e rodar a virada. Separada de update de propósito. |
| import:budgets | Controladoria | Carga em lote. |
| create:budget-change-requests | Gestor de centro de custo | Pedir suplementação ou remanejamento. Aprovar é do motor. |
| update:budget-policies | Administrador | Ligar controle, definir estouro, tolerância e virada. |
| read:exchange-rates | Controladoria, Comprador | Consultar cotações. Definida em Moeda e Câmbio. |
| manage:exchange-rates | Controladoria | Cadastrar taxa manual ou contratada. Definida em Moeda e Câmbio. |
| override:exchange-rates | Ninguém, por padrão | Informar 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
BudgetEntry aceita apenas INSERT e SELECT. UPDATE e DELETE são recusados por grant e por trigger. Toda correção é movimento novo.
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.
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.
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.
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.
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.
Sobreposição de intervalos entre Budget aprovados da mesma empresa é recusada por EXCLUDE + btree_gist, no banco.
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.
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.
Inclusive onde não há controle, com a tupla de dimensões preenchida e budgetLineId nulo.
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.
A janela do modo Cumulative começa em periodStart e termina no balde consultado. Reinicia a cada exercício, sempre.
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.
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.
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.
Budget.currency vem de Company.functionalCurrency e é imutável após a criação.
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.
Depois de Approved, amount é imutável. O valor efetivo é amount + Σ Adjustment, e todo ajuste tem autor, data e justificativa.
O saldo da origem é conferido no instante da aprovação, sob trava, e não no momento do pedido.
Cada campo de BudgetPolicy sobe o path independentemente. A API sempre devolve de qual nó cada valor veio.
Mudar política é encerrar a vigente e abrir outra. Sobreposição recusada por EXCLUDE.
Sem read:budget-amounts, o bloco de valores não vai na resposta — nem na API, nem no CSV, nem na notificação.
Nenhuma permissão ignora Block. O caminho para gastar mais é pedir mais verba ou mudar a política do nó, com registro.
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.
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.
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.
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.
Adjustment só nasce de mudança de verbaUm 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.
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.
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.
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.
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.
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.
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.
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.
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.
BudgetPolicy pode ter empresacompanyId 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.
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ê.
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.
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.
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 que | Por 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 receita | O Nexio controla gasto. Receita orçada não tem consumidor nenhum aqui. |
| Contabilização | Nenhum lançamento contábil é gerado. O razão orçamentário é gerencial e não pretende ser diário contábil. |
| Fechamento mensal formal | Substituí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 estabelecimento | Orç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 categoria | Dimensão errada. Categoria sugere conta contábil, e é a conta que entra na chave. |
| Override de bloqueio | Decisão explícita. Toda porta de fuga vira o caminho normal em três meses. |
| Reavaliação cambial periódica | Mudaria o saldo sem ninguém ter comprado nada. Variação aparece na liquidação, que é quando ela é real. |
| Consolidação entre empresas | Somar 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.