Ponto de partida
Oito decisões
Duas camadas de regra: base e complementar. A base é a alçada financeira — uma só vence, escolhida por prioridade. As complementares empilham: todas que casam entram na cadeia. Controles de compra são ortogonais — alçada, dono do centro de custo, aprovador de categoria, Jurídico, LGPD, Qualidade — e um motor de regra única força o produto cartesiano dessas dimensões em fluxos configurados à mão.
O motor não sabe o que é um pedido. Ele conhece um assunto com valor, empresa, requisitante e um conjunto de linhas com nó de centro de custo e nó de categoria. ApprovalProcess é entidade de configuração — mesmo padrão do ItemType — e é o que faz requisição, cotação, contrato e alteração bancária percorrerem o mesmo código. Vale para toda aprovação humana do Nexio: cadastro e homologação de fornecedor, alteração bancária, curadoria de item, exercício e remanejamento de verba são instâncias deste motor. O motor decide; o módulo dono do documento executa o efeito.
Cadeia congelada na submissão; pessoas resolvidas na ativação do passo. O snapshot completo deixa a tarefa apontando para quem saiu da empresa; a resolução tardia total impede mostrar a cadeia ao requisitante no momento em que ele submete. O híbrido entrega as duas coisas — e é para onde Oracle e Coupa convergiram.
O passo tem escopo: documento, linha ou fatia de rateio. É o que permite avaliar a alçada sobre os R$ 40 mil que couberam à área, e não sobre os R$ 400 mil do pedido inteiro. O Protheus dispara pelo total; Coupa e Ariba tratam rateio como dado contábil sem efeito no fluxo.
Paralelo é dentro do nível, não entre níveis. Cada nível tem um modo: qualquer um, todos ou quórum de N — e em todos eles uma rejeição encerra a instância. Entre níveis é sempre sequencial — a semântica que o mercado inteiro usa.
Nada é aprovado por omissão. SLA vencido notifica ou escalona; nunca aprova. Aprovação automática existe como tipo de nível declarado — "abaixo de R$ 500 com fornecedor homologado" — e jamais como consequência de tempo, nem como chave que deixe o requisitante decidir o próprio pedido. Aprovar por decurso de prazo é a funcionalidade que destrói a validade jurídica de toda a trilha.
A alçada mora na pessoa, dentro do nó. CostCenterAssignment.approvalLimit. O nível do tipo "sobe até quem tenha alçada" caminha a árvore de centro de custo até encontrar quem cubra o valor. Trezentos centros de custo passam a ser trezentas linhas de vínculo, não trezentos fluxos — e é o modelo que o cliente vindo do Protheus já tem na cabeça.
Toda decisão é evento, e a trilha prova a instância. ApprovalEvent é append-only, com autor, papel pelo qual agiu, delegação em vigor, o passo alcançado e o que foi visto. O estado é gravado no agregado, na mesma transação do evento; a trilha é a prova de como ele chegou ali, não um log de onde se reconstrói — pessoas resolvidas e cadastros mudam depois, e é por isso que o evento grava o que leu. A decisão tem esquema: tabela de campos, enum fechado de ações, ordenação por sequence e a invariante de imutabilidade escrita. Sem isso, "por que este pedido foi aprovado" só tem resposta na cabeça de quem estava lá.
Fundamentos
Sete princípios
1. Configuração e execução são agregados diferentes
Editar uma política não muda documento que não foi reavaliado. A instância guarda o id e a versão de cada política que a formou, e a versão é imutável: alterar uma política publica uma versão nova e aposenta a anterior, que continua respondendo pelas instâncias vivas. É a regra RN-F08 da spec de junho, elevada de detalhe a invariante.
O que a invariante protege é o documento parado: nenhuma edição de política mexe numa cadeia sozinha. A reavaliação é o único caminho pelo qual configuração nova entra numa instância viva, ela é sempre disparada por uma mudança no documento — que pode acionar controle complementar que não existia —, e recasa contra as políticas vigentes na data da reavaliação (RN-AP-47). Congelar as versões antigas na reavaliação seria pior: a devolução reenviada é o caminho mais comum do produto, e o documento voltaria em regra que o cliente já corrigiu.
2. Quem aprova nunca é quem pede
O requisitante é excluído de toda tarefa da própria instância — inclusive quando é ele o responsável pelo centro de custo —, e com ele todo excluído que o módulo dono declara na submissão: quem preparou "em nome de", o beneficiário, o último editor e, na alteração bancária, o proponente (RN-AP-60). Nesse caso o motor sobe um nível na árvore; se a subida esgotar, cai no aprovador de reserva do processo. Recusar silenciosamente ou deixar o próprio aprovar são os dois erros que auditoria encontra primeiro. Não há parâmetro que desligue esta regra: allowSelfApproval não existe. E a garantia mora no agregado, não na tela: cada decisão conta por quem de fato agiu, e duas decisões do mesmo passo nunca são da mesma pessoa (RN-AP-56). Tenant em que o requisitante é o único aprovador possível recebe a recusa do princípio 4 — e resolve com um segundo aprovador ou com o aprovador de reserva, não com exceção.
3. Ambiguidade é erro de configuração, nunca desempate silencioso
Duas políticas base com a mesma prioridade e critérios que se sobrepõem são recusadas no momento de publicar, com 409 — e, se ainda assim duas casarem o mesmo documento em execução, a submissão é recusada com 422, nunca desempatada em silêncio. A garantia tem duas camadas: a verificação completa na aplicação, na mesma transação da publicação e serializada por uma trava por (tenant, processo), e uma guarda parcial no banco, quando o banco a oferecer. O que fica proibido é o que a implementação anterior fazia: varredura em memória, sem lock, que perde a corrida de dois POST simultâneos.
4. Um passo sem aprovador possível é recusa explícita
Não existe passo pulado por falta de gente. Se a resolução devolve vazio depois do rollup, da delegação e do grupo, o motor tenta o aprovador de reserva do processo; se também não houver, a submissão é recusada com 422 nomeando o passo. Pular é como um pedido de R$ 2 milhões passa sem que ninguém perceba.
5. A cadeia é mostrada antes de ser percorrida
O requisitante vê a cadeia inteira ao submeter, e qualquer pessoa com permissão pode simular sem criar documento. Nenhum ERP brasileiro pesquisado tem simulador; no Oracle ele existe atrás de privilégio especial. Aqui a lógica de seleção já é a mesma consulta — expor é quase de graça, e é o que transforma configuração de aprovação de adivinhação em engenharia.
6. O motor consome contratos, não caminha em árvores
Centro de custo publica path, responsáveis efetivos, alçada acumulada e empresa. Catálogo publica path de categoria. Orçamento publica saldo. O motor combina — e por isso pode ser testado sem nenhum dos três.
7. Toda aprovação humana é uma instância do motor
Onde uma segunda pessoa decide sobre o pedido de outra, existe um ApprovalProcess — sem exceção por módulo. O módulo dono do documento submete, guarda o resultado e aplica o efeito (carência bancária, movimento do razão, estado do item); o motor resolve quem, em que ordem, em quanto tempo e com que alçada. O motor decide; o módulo executa. Atos unilaterais com motivo — bloquear fornecedor, promover manualmente, revogar anexo, substituir cotação, forçar duplicata, expurgar arquivo — não são aprovação: quem age é quem decide, e o que os protege é permissão, motivo codificado e trilha.
Modelo
Mapa de entidades
Configuração — o que o cliente parametriza
| Entidade | Cardinalidade | O que é |
|---|---|---|
| ApprovalProcess | do produto · o tenant só lê | Qual documento é aprovável. Define o valor avaliado, os critérios disponíveis, as ações permitidas, o uso de motivo da rejeição e da devolução, as flags que o assunto expõe, se nasce com política padrão semeada e o núcleo de controle — se tem valor, se admite aprovação pela política, quantas pessoas distintas exige e quais tipos de aprovador e escopos admite (RN-AP-54). Dez tipos-semente: REQUISITION, QUOTATION, PURCHASE_ORDER, CONTRACT, SUPPLIER_REGISTRATION, SUPPLIER, BANK_ACCOUNT_CHANGE, ITEM, BUDGET, BUDGET_CHANGE. |
| ApprovalPolicy | N por processo · versionada | A regra. kind = Base ou Supplemental, prioridade, critérios, vigência e níveis. Versão imutável. |
| ApprovalCriteria | value object da política | Quinze critérios: faixa de valor, nó de centro de custo, nó de categoria, nó de conta contábil, tipo de item, destinação, empresa, estabelecimento, UF de entrega, fornecedor, flags do assunto, flags do item, estado do orçamento, categoria de homologação e linha não catalogada. Critério ausente casa tudo; critérios de linha casam na mesma linha (RN-AP-66). |
| ApprovalLevel | 1..N por política | Ordem, tipo de aprovador, alvo, modo dentro do nível, SLA próprio, valor de ativação e rótulo exibido — e o alvo do escalonamento (escalationTargetType · escalationTargetRef), exigido quando o processo escala e este nível não caminha árvore. |
| ApprovalGroup | N por tenant | Pool nomeado de usuários — "Comitê de Investimentos", "Jurídico". Referenciado por nível; o modo do nível decide se basta um. |
| ApprovalGroupMember | N por grupo · dentro do agregado | Quem está no grupo, com vigência — mesma disciplina do vínculo de centro de custo. A resolução do nível lê os membros vigentes na ativação do passo, e não na submissão: um comitê que muda de composição em outubro decide em outubro com quem está nele. Não é agregado próprio — nasce e morre com o grupo, e por isso a contagem de agregados não muda. |
| CostCenterAssignment | do Centro de Custo | Onde a alçada da pessoa mora (approvalLimit). O motor lê; não escreve. |
| ApprovalDelegation | N por usuário · uma vigente por processo | Ausência temporária. Uma linha por processo — scope nulo significa "todos" e conflita com qualquer outra —, cada uma com seu delegado, seu teto de valor e seu período, de fim obrigatório. |
| ApprovalProcessSettings | 1 por tenant × processo | Os parâmetros que o tenant ajusta por processo: aprovador de reserva, dedupe, SLA padrão, comportamento de escalonamento e de alçada estourada, delegação, reatribuição e lote. Um campo, um dono: o que é do produto está no ApprovalProcess. Não há chave de autoaprovação — ver RN-AP-08. |
| ApprovalSettings | 1 por tenant | Os parâmetros do motor que não variam por processo: o limiar de materialidade do rateio (RN-AP-15), o teto de duração da delegação (RN-AP-36) e o expediente com a lista de dias não úteis e o fuso (RN-AP-13). Uma linha por tenant, criada no provisionamento com os padrões do produto. Fecha a pendência "configuração por tenant do Nexio" do lado do fluxo, no padrão do CurrencySettings. |
| ApprovalSettingsChange | N por tenant · registro de ato de controle | A mudança proposta de um parâmetro crítico do motor — materialidade, reserva, alçada estourada, dedupe, delegação ou membros de grupo usado como reserva ou por nível de controle: alvo, diff campo a campo, quem propôs e quem efetivou ou descartou. Quem propõe não efetiva (RN-AP-69). |
Execução — o que o motor escreve
| Entidade | Cardinalidade | O que é |
|---|---|---|
| ApprovalRequest | aggregate root · no máximo 1 viva por assunto e processo | A instância. Assunto cuja instância terminou só ganha outra pelos caminhos declarados — documento rejeitado que volta (tabela de transições) e correção de identidade de fornecedor aprovado (identityCorrection), em que o cadastro continua Approved enquanto a instância nova corre. subjectType + subjectId, empresa, requisitante, valor avaliado, moeda, estado, e o snapshot das políticas que a formaram com suas versões. E três colunas: roundNumber (int, a rodada corrente — sobe na devolução reenviada e na reabertura, RN-AP-43), version (int, o token de concorrência da raiz por onde toda decisão serializa, RN-AP-44) e budgetRequirementUnmet (RN-AP-42). É dona de anexo (ownerType = ApprovalRequest): da evidência que o aprovador junta depois da submissão — parecer, cotação de terceiro, autorização por fora —, já que o documento em aprovação não aceita mais alteração; o documento que sustenta a proposta antes da decisão continua sendo do agregado que a propôs. CanWrite é o aprovador pendente da rodada corrente, e mais ninguém. 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. |
| ApprovalStep | 1..N por instância | Um nível resolvido. Ordem, origem (qual política e nível), escopo, modo, SLA, estado — e a marca authorityExceeded quando a alçada esgotou. |
| ApprovalAssignment | 1..N por passo | A tarefa de uma pessoa. Origem da atribuição, estado, decisão, comentário, momento. É a entidade que a spec de junho não tinha — e sem ela não existe fila endereçável, aprovação por papel, delegação registrada nem paralelo dentro do nível. |
| ApprovalEvent | append-only · dentro do agregado da instância | Toda transição, com autor, ação, alvo e metadados. A prova da trilha, com esquema: tabela de campos, enum fechado de ações, chave de ordenação (approvalRequestId, sequence) e a invariante de imutabilidade — ver Instância, passo e tarefa (RN-AP-48). É ele que a GET /approvals sustenta, e é dele que sai o relatório de aprovação que o Motivo delega a este módulo. |
| ApprovalToken | opcional · fase 2 | Link assinado para aprovar por e-mail sem sessão. De uso único, com expiração, vinculado a um ApprovalAssignment. |
Os três níveis da execução não são zelo arquitetural. Instância sem passo não sabe onde está; passo sem tarefa não sabe de quem está esperando. Colapsar tarefa em passo — que é o desenho da spec de junho — obriga a resolver "quem pode agir" em toda leitura de fila, transforma a consulta do aprovador numa varredura, e não deixa onde registrar que Ana agiu como substituta de Bruno na delegação nº 12.
Mecânica
Da submissão à decisão
Configuração
Processos aprováveis
ApprovalProcess é o que torna o motor genérico. Cada processo declara o que significa "valor" naquele documento, quais critérios fazem sentido e o que acontece quando a cadeia termina.
| Processo | Valor avaliado | Critérios que fazem sentido | Efeito da aprovação | Política padrão semeada |
|---|---|---|---|---|
| REQUISITION | Total estimado, por fatia de rateio | valor, centro de custo, categoria, conta contábil, tipo de item, destinação, orçamento | Libera para cotação ou para pedido | — · o tenant configura (RN-AP-03) |
| QUOTATION | Valor da proposta escolhida | valor, categoria, fornecedor | Libera a emissão do pedido | — |
| PURCHASE_ORDER | Total do pedido, por fatia | valor, centro de custo, categoria, conta contábil, fornecedor, empresa, orçamento | Autoriza o envio ao fornecedor | — |
| CONTRACT | Valor total comprometido | valor, categoria, fornecedor. Prazo e cláusula de exclusividade: a definir na spec do Contract | Coloca o contrato em vigência | — |
| SUPPLIER_REGISTRATION | — | fornecedor (flags do assunto: tipo de pessoa e correção de identidade identityCorrection) | Cadastro passa a Approved; o relacionamento continua Prospect. Na correção de identidade de fornecedor aprovado (identityCorrection), o cadastro continua Approved durante e depois da instância: o desfecho aplica os valores propostos ou devolve Refused, sem mudar o estado do cadastro (Fornecedor, RN-FOR-39) | PermissionHolder(approve:suppliers) · Any; complementar semeada e travada Role(Controladoria) · Any · Document pela flag identityCorrection |
| SUPPLIER | — | categoria de homologação, fornecedor (sancionado) | Grava o resultado da homologação e promove a SpendAuthorized | PermissionHolder(update:supplier-qualifications) · Any; recomendada: quatro níveis em sequência — jurídico, financeiro, técnico, qualidade |
| BANK_ACCOUNT_CHANGE | — · processo sem valor | fornecedor, flags do assunto (titular ≠ fornecedor — só Foreign —, chave PIX não verificada, sem telefone confiável noTrustedPhone, aviso anterior não entregue previousNoticeUndeliverable, canal de aviso contestado contestedChannel) | Aplica a proposta ao registro vivo e inicia a carência | A definição normativa da RN-AP-61: base PermissionHolder(approve:supplier-banks) · Quorum(2) · Document, com duas pessoas distintas e diferentes do proponente, e complementar Role(Controladoria) · Any · Document quando qualquer das quatro flags de controle estiver presente — titular ≠ fornecedor (só Foreign), noTrustedPhone, previousNoticeUndeliverable, contestedChannel. Sem reserva, sem devolução, sem dedupe, sem reatribuição, sem escalonamento e sem delegação — travados pelo produto, fora do alcance do tenant. Nenhuma tarefa ativa antes da janela de aviso (RN-AP-63), e a instância não conclui sem o callback confirmado que o Fornecedor registra na proposta — ou, com noTrustedPhone, sem a controladoria e o comprovante que o Fornecedor exige: titularidade na proposta de conta (RN-FOR-35); anexo do pedido de troca assinado, do tipo declarado e com veredito liberado, e carência de 7 dias na mudança de contato (RN-FOR-40, RN-FOR-34) |
| ITEM | — | categoria, tipo de item, flags do assunto (regulated, personalData e critical) | Item passa a Approved; a rejeição grava o motivo no item | PermissionHolder(approve:catalog-items) · Any |
| BUDGET | Total orçado do exercício | valor, empresa | Publica o exercício e arquiva a versão substituída | — |
| BUDGET_CHANGE | Delta da verba | valor, centro de custo, conta contábil | Aplica o remanejamento | — |
| Processo | Tem valor | allowPolicyApproval | minDistinctApprovers | Tipos de aprovador admitidos | Escopos admitidos |
|---|---|---|---|---|---|
| REQUISITION | sim | true | 1 | de árvore e sem árvore | Document · Line · Slice |
| QUOTATION | sim | true | 1 | sem árvore | Document · Line |
| PURCHASE_ORDER | sim | true | 1 | de árvore e sem árvore | Document · Line · Slice |
| CONTRACT | sim | true | 1 | sem árvore | Document |
| SUPPLIER_REGISTRATION | não | false | 1 | sem árvore | Document |
| SUPPLIER | não | false | 1 | sem árvore | Document |
| BANK_ACCOUNT_CHANGE | não | false | 2 | base: só PermissionHolder(approve:supplier-banks); complementares: sem árvore | Document, sem devolução e com as travas da RN-AP-61 |
| ITEM | não | false | 1 | sem árvore | Document |
| BUDGET | sim | true | 1 | sem árvore | Document |
| BUDGET_CHANGE | sim | true | 1 | sem árvore | Document |
Tipos de árvore são os CostCenterOwner…: partem do nó de uma fatia e só existem onde há fatia. Sem árvore são SpecificUser, Role, ApprovalGroup e PermissionHolder. A tabela é verificada na publicação da política — tipo ou escopo não admitido, faixa de valor ou activationMinValue em processo sem valor, cadeia que pode terminar sem pessoa onde allowPolicyApproval é falso, base sem nível que garanta minDistinctApprovers pessoas — e repetida como defesa na submissão e na decisão (RN-AP-54, RN-AP-56). PUT /approval-processes/{id} não mexe em nenhuma dessas colunas.
| Processo | Flag do assunto | Quem calcula a flag | Nível semeado | Quem não decide |
|---|---|---|---|---|
| REQUISITION · QUOTATION · PURCHASE_ORDER · CONTRACT · BUDGET · BUDGET_CHANGE | exchangeRateOverridden — taxa informada, ou de tipo diferente do que a configuração por processo fixa (processRateTypes, na Moeda), por exchangeRateOverride com a permissão de informar taxa no documento [PERGUNTA AO ROGER: o override de taxa por documento não cabe em nenhum dos cinco verbos sem se confundir com o cadastro de taxa; qual permissão o exige?]; ou taxa do tenant cujo autor (capturedBy) está entre os demais excluídos da RN-AP-60 ou é quem submete | o módulo dono do documento, na submissão e na reavaliação | Role(Controladoria) · Any · Document | requisitante e o autor de toda taxa do tenant usada e da taxa informada (RateOverrider) — RN-AP-70 |
| BANK_ACCOUNT_CHANGE | titular ≠ fornecedor — só em fornecedor Foreign | o Fornecedor; nos demais tipos, titular diferente é recusado lá, antes da submissão | Role(Controladoria) · Any · Document | proponente e quem decidiu o quórum — RN-AP-61; e, como todo decisor da instância — esta complementar inclusive —, quem não tem identidade verificada, quem tem o CPF do proponente, de outro decisor da instância ou de pessoa ligada ao número, e quem está ligado, sem prazo, a uma correção de identidade daquele fornecedor (Fornecedor, RN-FOR-36 e RN-FOR-41) |
| BANK_ACCOUNT_CHANGE | noTrustedPhone — sem telefone confiável, na proposta de conta e na mudança de contato de aviso ou da cadeia | o Fornecedor (RN-FOR-29): em fornecedor com CNPJ, depois de consulta nova ao provedor; em fornecedor sem CNPJ, sem consulta, pela falta de número confiável | Role(Controladoria) · Any · Document | proponente e quem decidiu o quórum — RN-AP-61; e, como todo decisor da instância — esta complementar inclusive —, quem não tem identidade verificada, quem tem o CPF do proponente, de outro decisor da instância ou de pessoa ligada ao número, e quem está ligado, sem prazo, a uma correção de identidade daquele fornecedor (Fornecedor, RN-FOR-36 e RN-FOR-41) |
| BANK_ACCOUNT_CHANGE | previousNoticeUndeliverable — o aviso ao canal anterior não é entregável | o Fornecedor (RN-FOR-23) | Role(Controladoria) · Any · Document | proponente e quem decidiu o quórum — RN-AP-61; e, como todo decisor da instância — esta complementar inclusive —, quem não tem identidade verificada, quem tem o CPF do proponente, de outro decisor da instância ou de pessoa ligada ao número, e quem está ligado, sem prazo, a uma correção de identidade daquele fornecedor (Fornecedor, RN-FOR-36 e RN-FOR-41) |
| BANK_ACCOUNT_CHANGE | contestedChannel — canal de aviso contestado: a contestação de mudança de contato já aplicada suspende o efeito e trava as propostas de conta; a flag vai na instância que decide o caso contestado e na troca de canal que desbloqueia | o Fornecedor (RN-FOR-38) | Role(Controladoria) · Any · Document | proponente e quem decidiu o quórum — RN-AP-61; e, como todo decisor da instância — esta complementar inclusive —, quem não tem identidade verificada, quem tem o CPF do proponente, de outro decisor da instância ou de pessoa ligada ao número, e quem está ligado, sem prazo, a uma correção de identidade daquele fornecedor (Fornecedor, RN-FOR-36 e RN-FOR-41) |
| SUPPLIER_REGISTRATION | identityCorrection — correção dos dados de identidade que o Fornecedor trava depois da aprovação, pedida por POST /suppliers/{id}:correct-identity, com o cadastro Approved. A troca entre tipo de pessoa com CNPJ e tipo sem CNPJ, e a troca de e para Foreign, não abrem instância: o Fornecedor as recusa antes | o Fornecedor (RN-FOR-39) | Role(Controladoria) · Any · Document | quem pediu a correção (Requester, RN-AP-60) e quem decidiu a base — pessoa distinta (RN-AP-56, RN-AP-64); identidade verificada, CPFs distintos entre si e do de quem pediu, e a ligação sem prazo de quem pede e de quem decide àquele fornecedor — que também os tira de conta bancária e de canal de aviso dele — são regra do Fornecedor (RN-FOR-41) |
A flag é calculada pelo módulo dono, nunca declarada por quem propõe. Estas complementares não se aposentam nem se alteram (APPROVAL_POLICY_SEEDED_CONTROL_LOCKED_CONFLICT · 409), e toda complementar é passo de controle (RN-AP-64).
Quatro desses processos não têm valor, e é por isso que eles precisam existir na lista. Cadastro e homologação de fornecedor, alteração de dado bancário e curadoria de item são aprovações sem dinheiro — e a alteração bancária é o controle antifraude mais importante de um sistema de compras, porque é o vetor de fraude mais explorado no Brasil. Um motor amarrado a "faixa de valor" não consegue aprová-las, e o produto termina com dois motores. O princípio 7 impede isso: nenhum módulo tem rota própria de aprovar.
A alteração bancária não tem valor avaliado, e isso é controle, não omissão. O controle antifraude mais importante do produto não pode depender de um número: enquanto não houver pedido nem nota, todo fornecedor teria "valor" zero ou indisponível, uma política por faixa nunca casaria, e a troca de conta cairia na política mais fraca — o controle nasceria degradado funcionando, que é o pior modo de falha que existe. Por isso o processo é sem valor, o quórum de duas pessoas é incondicional e a publicação recusa qualquer faixa ou activationMinValue nele (RN-AP-54). Onde outro processo devolver valor indisponível, ele não casa faixa e não desliga nível (RN-AP-62).
Política padrão semeada: o dia 1 se comporta como antes. Todo processo sem valor nasce com a política de reserva já publicada — isDefault, critérios vazios, um nível PermissionHolder apontando para a permissão do ato no módulo dono (approve:suppliers, update:supplier-qualifications, approve:catalog-items), modo Any — a alteração bancária é a exceção, abaixo. Quem aprovava ontem aprova hoje, agora pelo motor, com fila, SLA, delegação e trilha — com uma diferença deliberada: quem cadastrou não aprova (RN-AP-08), que é a separação de funções que auditoria de cadastro de fornecedor exige. Tenant em que o solicitante é o único detentor da permissão cai na RN-AP-09; é configuração de onboarding — segundo detentor ou aprovador de reserva do processo —, não exceção, e o painel de administração avisa "processo com um único aprovador possível". O BANK_ACCOUNT_CHANGE é a exceção declarada: nasce com o quórum de duas pessoas da RN-AP-61, não tem reserva, e com menos de duas pessoas elegíveis a instância fica parada com alerta ao administrador (RN-AP-59). O tenant edita a política quando quiser alçada; nunca fica sem ela. Os processos com dinheiro continuam exigindo configuração para ativar (RN-AP-03), porque alçada financeira não tem padrão sensato.
O que o motor não decide. Bloquear e desbloquear fornecedor, ou a relação de fornecimento de um item, promoção manual a SpendAuthorized, revogar anexo, substituir cotação, forçar item sobre duplicata, expurgar arquivo e desligar legal hold são atos de uma pessoa só, com permissão, motivo codificado e trilha. Não há pedido de um e decisão de outro, logo não há instância. Um destes virar aprovação é decisão de spec, não de política. A carga inicial do catálogo com autoApprove é a única exceção declarada ao allowPolicyApproval falso do ITEM: janela única por tenant, dupla custódia — quem importa não confirma a carga —, evento próprio e registro de ato de controle; a regra é do Item. Fora dela, item importado entra em aprovação pelo motor como qualquer outro. Aceitar diferença de preço na nota (PriceVarianceAccept) é ato unilateral ou processo a decidir pela spec da Nota; enquanto ela não existir, não há processo de nota no motor.
Configuração
Política e critérios
| Campo | Tipo | Regra |
|---|---|---|
| processId | Guid | A qual processo esta regra se aplica. Obrigatório. |
| kind | enum | Base ou Supplemental. Decide se a política concorre ou empilha. Imutável depois de publicada. |
| priority | int | Só tem efeito em Base: menor número vence entre as bases que casam. Em Supplemental não tem efeito — a posição na cadeia é o stage de cada nível (RN-AP-65). |
| status | enum | Draft · Published · Retired. Só Published dentro da vigência é candidata; Draft só entra na simulação, por policyOverrides (RN-AP-26). |
| isDefault | bool | Exatamente uma política Base por processo é a de reserva, com critérios vazios. Fica fora da concorrência: só responde quando nenhuma outra base casa, qualquer que seja a prioridade dela. |
| criteria | value object | Ver abaixo. Critério ausente casa tudo. |
| levels | 1..N | Ordenados. Uma política sem nível é recusada na publicação. |
| version | int | Imutável. Editar publica versão nova e aposenta a anterior, que segue respondendo pelas instâncias vivas. |
| validFrom / validTo | date / date? | Vigência da política. Fora dela, a política não é candidata — e o histórico continua legível. |
| fallbackApproverType · fallbackApproverRef | enum? · Guid? | Usado quando um nível resolve vazio: pessoa ou grupo, com as mesmas regras do reserva do processo (RN-AP-40, RN-AP-58). Se nulo, herda o do processo. |
Critérios disponíveis
| Critério | Operador | Fonte |
|---|---|---|
| Faixa de valor | intervalo semiaberto [min, max) | Valor avaliado do processo, na moeda funcional da empresa do documento |
| Centro de custo | nó, valendo para a subárvore | Prefixo de path. Sub-áreas criadas depois passam a ser cobertas sem tocar na regra |
| Categoria | nó, valendo para a subárvore | Prefixo de path do CategoryNode, pela mesma mecânica |
| Linha não catalogada | presença | itemId nulo na linha (Item). Linha sem item não tem tipo, flags nem criticidade: os critérios que leem esses atributos não casam com ela |
| Tipo de item | em lista | ItemType das linhas |
| Flags do item | presença | itemFlags da linha (§Contrato de submissão): regulated, personalData e critical, calculadas pelo dono a partir do item na hora do ato, pela regra do Cadastro de Item (RN-ITM-43). Critério de linha: "software com dado pessoal" só casa quando a mesma linha tem a categoria e a flag (RN-AP-66). Linha não catalogada não traz nenhuma. Novo. |
| Destinação | igualdade | predominantUse — CAPEX ou OPEX, sem depender do centro de custo |
| Empresa | em lista | companyId do documento, derivado do estabelecimento |
| Estabelecimento | em lista | establishmentId do documento. Novo. É o recorte para obra, filial em regime especial ou CD com alçada própria |
| Fornecedor | em lista · ou estado | Fornecedor da linha, ou o fato de ele ser Prospect, sancionado ou com documento vencido |
| Flags do assunto | presença | Cada processo declara as flags que o seu assunto expõe (subjectFlags): no catálogo, regulated, personalData e critical — as mesmas que, nos processos com linhas, chegam por linha como flags do item; na alteração bancária, titular ≠ fornecedor (só Foreign), chave PIX não verificada, sem telefone confiável (noTrustedPhone), aviso anterior não entregue (previousNoticeUndeliverable) e canal de aviso contestado (contestedChannel); no cadastro de fornecedor, o tipo de pessoa e a correção de identidade (identityCorrection); nos seis processos com valor, a taxa sobrescrita (exchangeRateOverridden, RN-AP-70). Nunca nós inventados na taxonomia. |
| Conta contábil | nó, valendo para a subárvore | Prefixo de path do GLAccount da fatia — obrigatória na submissão, RN-CC-29 — e, no BUDGET_CHANGE, da linha de destino. Terceira árvore, mesma mecânica. |
| Categoria de homologação | nó, valendo para a subárvore | scopeCategoryIds da homologação, por prefixo de path do CategoryNode. Só no processo SUPPLIER — o critério "categoria" lê as linhas do documento, e uma homologação não tem linhas. |
| Orçamento | estado | Ok, Warning, Exceeded, Uncovered. Os quatro que POST /evaluate:budget-state devolve (RN-AP-53) |
| UF de entrega | em lista · ou interestadual | deliveryStateCode das linhas, ou deliveryIsInterstate congelado no documento pelo Local de Entrega — nunca a comparação refeita na hora. Novo. Entrega interestadual muda alíquota, DIFAL e substituição tributária — e muitas empresas querem que ela passe por quem responde pelo fiscal antes de virar pedido |
Critério de linha casa na mesma linha, e critério de fatia na mesma fatia. Os critérios têm três alcances. De linha: categoria, tipo de item, destinação, UF de entrega, fornecedor, linha não catalogada e as flags do item. De fatia: centro de custo, conta contábil e estado do orçamento. De documento: faixa de valor, empresa, estabelecimento, flags do assunto e categoria de homologação. No processo ITEM o item é o próprio assunto, e as flags dele são do documento; nos processos com linhas, cada linha traz as do seu item (§Contrato de submissão). A política casa quando o documento satisfaz os critérios de documento e existe ao menos uma linha que satisfaz, sozinha, todos os critérios de linha — e, havendo critério de fatia, uma fatia dessa linha que satisfaz todos. Uma linha de software sem dado pessoal e outra de hardware com dado pessoal não acionam "software com dado pessoal"; na escolha da base, a de mais controle não perde para uma que só casou juntando linhas. O recorte do que o nível aprova continua sendo do scopeMode: Document manda o documento inteiro, Line gera passo para cada linha que satisfaz os critérios de linha da política de origem (RN-AP-66).
Política de empresa é critério, não coluna. Com multiempresa especificada em Empresa e Estabelecimento, a tentação é dar um companyId à própria política. Não é preciso e seria pior: o critério Empresa em lista já faz esse recorte, e faz melhor — uma política Base do grupo inteiro convive com uma Supplemental de uma empresa só, e a cadeia final é a união das duas. Uma coluna de escopo criaria um segundo mecanismo concorrente para a mesma pergunta.
Aprovador precisa de escopo na empresa do documento — e isso é garantido antes, não aqui. O vínculo de centro de custo só é criado para quem alcança a empresa do nó (RN-CC-30), de modo que quem o motor resolve já enxerga o documento. A empresa comparada é sempre a do documento, inclusive em passo Slice: a fatia de rateio não tem empresa própria, porque não há rateio entre empresas, e a compra centralizada roda no estabelecimento de cada empresa. Isso vale para os tipos que caminham a árvore; para os que não passam por vínculo nenhum — SpecificUser, Role, PermissionHolder, ApprovalGroup, a delegação e o próprio reserva — a guarda é dupla. Na publicação, cada empresa que a política alcança precisa ter, no alvo, gente elegível bastante para o modo (RN-AP-40). Na resolução, o filtro é por pessoa, nunca por nível: um Role com gente de empresas diferentes resolve só quem alcança a empresa do documento (RN-AP-59). Se o filtro esvaziar o nível, ele é tratado como resolvido vazio, cai no reserva e marca resolvedOutOfScope — nunca em silêncio (RN-AP-39).
Base concorre; complementar empilha
Uma requisição de R$ 80 mil de software com dado pessoal para o centro de custo /TI/INFRA/ resolve uma política base — a faixa de R$ 50 mil a R$ 100 mil — e duas complementares — "software com dado pessoal exige o DPO" e "categoria TI exige o arquiteto". A cadeia final é a união ordenada e deduplicada das três. Com motor de regra única, essa mesma combinação exigiria uma política escrita à mão para cada cruzamento de faixa × categoria × flag.
Especificidade não desempata — de propósito
Entre duas políticas base que casam, vence a de menor prioridade numérica, e ponto. Desempate por "regra mais específica" parece elegante e produz configurações que ninguém consegue prever: qual é mais específica, a que amarra centro de custo ou a que amarra faixa de valor? A prioridade explícita é feia e é auditável.
Sobreposição entre bases é recusada ao publicar — guarda parcial no banco, verificação completa na aplicação
Duas políticas base com a mesma prioridade e critérios que se cruzam nunca coexistem. Uma restrição de exclusão no banco sobre (tenant =, processo =, prioridade =, faixa de valor &&, nó de CC =, nó de categoria =) não basta, por três razões: igualdade não colide quando o nó é nulo — e nulo é o caso normal, o da política de reserva e o de toda regra "só por valor"; /TI/ e /TI/INFRA/ se sobrepõem por rollup, e igualdade não enxerga; e os outros critérios (empresa, estabelecimento, tipo de item, destinação, fornecedor, flags, UF, orçamento) e a vigência ficam de fora, o que recusa como sobrepostas políticas de empresas diferentes ou de janelas disjuntas.
A regra tem duas camadas. Na aplicação, sempre. Ao publicar uma versão de política base, o serviço carrega as bases ativas do mesmo tenant, processo e prioridade cuja vigência cruza a nova e recusa com 409 APPROVAL_POLICY_OVERLAP_CONFLICT se alguma tiver interseção não vazia em todos os critérios — nulo casa tudo, nó casa por prefixo nos dois sentidos, faixa e vigência por interseção. A verificação corre na mesma transação da publicação, serializada por uma trava por (tenant, processo) — por exemplo, bloqueio da linha de ApprovalProcessSettings do processo, ou trava consultiva equivalente do banco —, que cobre também as políticas sem centro de custo, o caso normal, para que dois POST simultâneos não passem os dois. Publicar versão nova aposenta a anterior na mesma transação, antes de verificar, para não colidir consigo mesma. No banco, quando ele oferecer. Uma restrição de exclusão sobre a mesma tupla, com sentinela no lugar do nulo (COALESCE(costCenterId, '0000…')) para que a reserva também seja protegida, continua valendo como segunda guarda — parcial, e a spec não depende dela.
Em execução, se duas bases de mesma prioridade casarem o mesmo documento — dado que escapou da verificação da publicação —, a submissão é recusada com 422 APPROVAL_POLICY_AMBIGUOUS_UNPROCESSABLE, nomeando as duas políticas. É o princípio 3 aplicado até o fim. E GET /approval-policies/{id}/validate lista a sobreposição como inconsistência, para que a tela avise antes de publicar.
Depende do banco. A restrição de exclusão com operador de faixa (EXCLUDE … btree_gist, no Postgres) e a sintaxe exata do bloqueio de linha são específicas do banco, e a arquitetura ainda não o fixou. O que esta spec garante não depende disso: a verificação na aplicação, o erro de execução e o /validate. A guarda no banco entra quando o banco for escolhido, na sintaxe dele — decisão registrada no mapa de domínio.
A cobertura por subárvore vale para os descendentes futuros
A política guarda o nó, nunca a lista expandida de folhas. Uma sub-área criada no mês seguinte já nasce coberta. O override no descendente existe: uma política amarrada em /TI/INFRA/ com prioridade menor vence a de /TI/ para tudo que estiver abaixo de infraestrutura.
Configuração
Níveis e aprovadores
| Campo | Tipo | Regra |
|---|---|---|
| order | int | Ordem local, dentro da política. |
| stage | int | A posição do nível na cadeia do processo, na mesma régua para base e complementares (ex.: 100, 200, 300). A cadeia ordena por stage; níveis de políticas diferentes com o mesmo stage formam um degrau, que ativa junto e fecha quando todos os seus passos fecham. Dentro de uma política, stage cresce estritamente com order (RN-AP-65). |
| approverType | enum | Ver a tabela seguinte. Determina a estratégia de resolução. |
| target | Guid? / string? | Usuário, papel, grupo, permissão ou e-mail externo, conforme o tipo. |
| mode | enum | Any · All · Quorum(n). É aqui que vive o paralelo. Em todos, uma rejeição encerra a instância. |
| label | string(80)? | "Aprovação Gerencial". O que a tela mostra; se ausente, o motor deriva do tipo. |
| maxApprovalHours | int? | SLA do nível. Nulo herda o da política, depois o do processo. |
| activationMinValue | decimal? | Abaixo deste valor o nível não entra na cadeia. É como se declara "diretor só entra acima de R$ 100 mil" sem criar outra política. Proibido em processo sem valor (RN-AP-54), e valor indisponível não desliga o nível (RN-AP-62). |
| scopeMode | enum | Document · Line · Slice. Um nível de alçada de centro de custo é normalmente por fatia; um de Jurídico é sempre do documento; um de fiscal é por linha — só as linhas que casam o critério geram passo (RN-AP-46). Cada processo admite só alguns: processo sem fatia não publica nível Slice nem tipo de árvore (RN-AP-54). |
Tipos de aprovador
| Tipo | Resolve para | Fase |
|---|---|---|
| SpecificUser | Um usuário fixo. Simples e frágil: é o tipo que quebra quando a pessoa sai. | MVP |
| Role | Todos os usuários ativos com o papel, no tenant. O modo do nível decide se basta um. | MVP |
| CostCenterOwner | Os Owner vigentes do nó da fatia; sem Owner vigente no nó, os Deputy vigentes do mesmo nó — o Centro de Custo define que o Deputy só entra quando não há Owner —; sem nenhum dos dois, rollup para o pai. Watcher nunca aprova. O tipo mais usado na prática. | MVP |
| CostCenterOwnerUpToAmount | Sobe a árvore de centro de custo até encontrar quem tenha approvalLimit que cubra o valor da fatia. É a alçada acumulada da decisão 7. | MVP |
| ApprovalGroup | Os membros ativos do grupo. Comitês. | MVP |
| PermissionHolder | Todos os usuários ativos com a permissão, no tenant. É o alvo das políticas padrão semeadas: no dia 1, quem aprovava pela rota do módulo continua aprovando, agora pelo motor. | MVP |
| CategoryOwner | O responsável declarado no nó de categoria, com rollup. Exige um vínculo equivalente no catálogo. | fase 2 |
| RequesterManager | O gestor direto do requisitante, na hierarquia de pessoas do GrydAuth. | fase 2 |
| ExternalEmail | Um e-mail sem conta, via ApprovalToken. Sócio, conselheiro, cliente final em obra. | fase 2 |
| Auto | Aprova sozinho quando a condição do nível é satisfeita. Sempre declarado, nunca por decurso de prazo. | fase 2 |
Modo dentro do nível, com precisão. Any: a primeira decisão fecha o nível — e as demais tarefas viram Obsolete, não desaparecem, porque a trilha precisa mostrar quem mais estava na fila. All: o nível só fecha quando todos decidem; uma rejeição encerra a instância imediatamente. Quorum(n): fecha em n aprovações de n pessoas distintas, contadas por quem agiu (RN-AP-56); qualquer rejeição encerra, como nos outros modos. Menos de n pessoas elegíveis é resolução vazia, nunca quórum menor (RN-AP-59). É o "quatro olhos" — duas pessoas do financeiro numa troca de conta bancária. A semântica de comitê de investimento, em que um voto contrário não derruba a proposta e a instância só encerra quando n fica inalcançável, é modo próprio de fase 2, Vote(n), restrito aos processos que o produto declarar (BUDGET, BUDGET_CHANGE, CONTRACT) — nunca uma releitura do Quorum, nunca em BANK_ACCOUNT_CHANGE.
Configuração
Alçada
A alçada financeira de uma pessoa mora em CostCenterAssignment.approvalLimit — no vínculo entre ela e o nó. Não há tabela de alçada separada, e isso é decisão: alçada é uma propriedade da responsabilidade, e responsabilidade já tem vigência, papel e histórico.
| # | Passo | Resultado |
|---|---|---|
| 1 | Owners vigentes do nó da fatia | Sem Owner vigente no nó, lê os Deputy vigentes do mesmo nó; havendo Owner, o Deputy não entra, cubra ou não o valor. Filtra quem tem approvalLimit ≥ valor da fatia na moeda do limite (RN-AP-52), ou quem tem hasUnlimitedAuthority. approvalLimit nulo é sem alçada, não alçada infinita (RN-AP-51). |
| 2 | Sobrou alguém? | Sim: é a resposta, e o nível fecha aqui. Não: sobe ao pai e repete. |
| 3 | Chegou à raiz sem cobrir | Usa os Owners da raiz, marca authorityExceeded no passo e grava approval-step.authority-exceeded na ativação. O que acontece depois é do processo, em authorityExceededBehavior (RN-AP-58): na semente, RequireControllerApproval — o nível ganha um passo paralelo Role(Controladoria) · Any, e o documento só avança com uma decisão da controladoria, de pessoa distinta de quem aprovou no topo; ou Refuse — a submissão cuja cadeia já estoura é recusada com APPROVAL_AUTHORITY_EXCEEDED_UNPROCESSABLE · 422. Aprovar no topo só com aviso não existe: é controle só detectivo. |
| 4 | Raiz também vazia | Aprovador de reserva do processo — pessoa ou grupo —, com a tarefa marcada origin = ByFallback e o evento approval-step.resolved-by-fallback. O reserva não tem alçada verificada, então em processo com valor o passo segue authorityExceededBehavior como no passo 3. Sem reserva, a submissão é recusada com 422 nomeando o passo. |
| 0 | Antes de tudo: há par de conversão? | Se a moeda do documento difere da moeda do limite e não há par vigente entre as duas, a submissão é recusada — nunca comparada em silêncio (RN-AP-52). |
Alçada não soma entre pessoas
Dois responsáveis de R$ 50 mil no mesmo nó cobrem R$ 50 mil cada, não R$ 100 mil juntos. O que acumula é a subida na árvore. Somar entre co-gestores criaria uma autoridade que ninguém concedeu por escrito — e o vínculo sem alçada preenchida não cobre nada (RN-AP-51): autoridade infinita exige um ato de cadastro, nunca um campo em branco.
Cada nível de subida é um passo, não um pulo
Se a fatia exige subir dois níveis, a cadeia ganha um passo — o do nó que cobre — e não um passo por nível atravessado. Exigir a assinatura dos níveis intermediários é configuração de v2 (ver Fora de escopo).
A régua é a moeda do limite, e a conversão é congelada na submissão
Não existe "moeda do tenant": o tenant pode ter mais de uma empresa, e é a functionalCurrency da empresa do documento que serve de régua para a faixa de valor do critério de política. A alçada tem régua própria: a comparação acontece na moeda do limite — CostCenterAssignment.approvalLimitCurrency, obrigatória quando o limite está preenchido, inclusive no nó corporativo, que não tem empresa. Com Alfa em BRL e Beta em USD, comparar o valor da fatia com o limite do nó corporativo sem dizer em que moeda é uma comparação silenciosa entre moedas exatamente no ponto em que a alçada decide (RN-AP-52).
Documento em outra moeda é convertido pela taxa vigente no momento da submissão — o único momento de conversão na vida do documento —, e a taxa fica no snapshot da instância. É essa taxa, e nenhuma outra, que a comparação da alçada usa; a rate e a rateDate empregadas entram no ApprovalEvent da decisão, para que a pergunta "por que este valor coube nesta alçada" tenha resposta meses depois. Reavaliar alçada com a cotação de hoje faria um pedido aprovado ontem precisar de outro aprovador amanhã, sem nada ter mudado no pedido; e o empenho gravado na aprovação usa esse mesmo número, para que a verba consuma exatamente o valor que foi aprovado — todo movimento do razão originado por documento herda a taxa do documento, e a resolução própria de taxa do orçamento vale só para movimento sem documento de origem. Ver a spec de Moeda e câmbio.
Alçada estourada exige a controladoria antes de aprovar
A marca, o evento e o aviso saem na ativação do passo, e o passo de controladoria nasce junto: a controladoria decide com a tarefa aberta, e pode corrigir o vínculo ou reatribuir antes. O simulador devolve a marca e o passo extra por passo, e /validate aponta a raiz sem Owner de autoridade ilimitada antes de a política ser publicada. Estouro que só aparece na ativação — o vínculo mudou depois da submissão — nunca recusa: a ativação não derruba a decisão de quem acabou de aprovar (RN-AP-45), e o passo segue como RequireControllerApproval mesmo no processo que escolheu Refuse. Caso de ataque: num tenant com a árvore incompleta, o gestor da raiz aprovava um pedido de R$ 2 milhões sem ninguém com alçada para isso, e a controladoria só descobria pelo aviso. Agora o pedido não conclui sem a decisão dela (RN-AP-58).
Configuração
Grupos e delegação
Pool nomeado de usuários, com membros que entram e saem com vigência — mesma disciplina do vínculo de centro de custo. Um grupo referenciado por política ativa não pode ficar vazio: a remoção do último membro é 409, com a lista de políticas que o usam. É o comitê que aprova sem que ninguém precise saber os nomes ao configurar.
Delegante, delegado, período, encerramento automático por rotina diária e registro em auditoria com delegante, substituto e referência à delegação. Tem escopo — scope, um processo por linha, nulo = todos — e teto de valor. A regra é uma vigente por delegante e por processo: com dez processos, uma só por pessoa impediria delegar o pedido ao Bruno até R$ 50 mil, o item à Carla e não delegar a alteração bancária. Três linhas, três delegados, três tetos.
Uma delegação vigente por delegante e por processo, e com fim
A chave é (delegante, scope, período) sem sobreposição — scope nulo lido como "todos": verificada na aplicação, na mesma transação, com guarda de exclusão por interseção no banco quando ele oferecer o recurso — qual banco é decisão de implementação, e ela vive na Arquitetura, não aqui — a mesma disciplina da sobreposição de políticas e do membro de grupo. Sobreposição é APPROVAL_DELEGATION_OVERLAP_CONFLICT · 409. validTo é obrigatório e não passa de maxDelegationDays (padrão 90, renovável): delegação sem fim é autoridade paralela permanente, que ninguém revisa e toda auditoria de compras encontra.
A tarefa delegada carrega a alçada do titular, nunca a do delegado
O passo já foi resolvido com o approvalLimit de quem responde pelo nó; o delegado apenas age. O teto da delegação só reduz — acima dele a tarefa fica com o titular. Um substituto com alçada maior aprovando o que o titular não poderia é o furo silencioso mais comum deste mecanismo, e aqui ele não existe por construção.
Processo pode não ser delegável
delegable por processo, false na semente do BANK_ACCOUNT_CHANGE: se a troca de conta do fornecedor exige quem responde pelo controle, ela não é coberta por quem está cobrindo férias. Criar delegação que alcance um processo não delegável é APPROVAL_DELEGATION_NOT_ALLOWED_UNPROCESSABLE · 422; a delegação de escopo nulo simplesmente não o alcança.
Delegação redireciona a tarefa; não substitui o aprovador
A ApprovalAssignment continua registrando o titular como aprovador de direito e o delegado como quem agiu, com o id da delegação. É por isso que a tarefa precisa ser entidade: não há onde guardar esse par num campo do passo.
Delegação vigente é lida na ativação do passo, não na submissão
Uma delegação que começa amanhã tem de valer para o passo que ativa amanhã, mesmo que a instância tenha sido criada hoje. É consequência direta da decisão 3.
Tarefa já aberta fica com o titular
A delegação alcança os passos que ativam enquanto ela está vigente. As tarefas que já estavam Pending quando ela começa ficam com o titular; quem precisa passá-las usa a reatribuição, onde o processo a permite, com permissão própria e evento. O fim da delegação não move tarefa: o delegado decide o que recebeu, ou a devolve ao titular por decline (RN-AP-49).
O delegado precisa ser elegível para o passo, e a delegação feita por outro avisa o titular
Criar delegação exige delegado com act:approvals e conta ativa (APPROVAL_DELEGATE_NOT_ELIGIBLE_UNPROCESSABLE · 422); nunca o próprio delegante. O resto é lido na ativação do passo, tarefa a tarefa, porque só ali se sabe o passo: o delegado precisa ser elegível para ele (permissão do nível, empresa do documento — RN-AP-59), não pode estar entre os excluídos da instância e não pode já ter tarefa ou decisão no mesmo passo. Em passo de controle (RN-AP-64), o delegado também precisa pertencer ao alvo do nível — membro do papel ou do grupo, detentor da permissão. Faltando qualquer um, a tarefa fica com o titular, como acima do teto. Delegação criada por quem tem create:approval-delegations em nome de outra pessoa é ato de controle: grava quem a criou e gera aviso obrigatório ao delegante. Não exige aceite — o aviso é o controle, e férias não esperam aceite. Caso de ataque: os dois membros de um Quorum(2) delegavam para a mesma pessoa, e um voto virava dois; ou um administrador criava a delegação do CFO para si mesmo sem o CFO saber. Agora a segunda tarefa não vai ao mesmo delegado, a segunda aprovação da mesma pessoa seria recusada no agregado de qualquer forma, e o CFO recebe o aviso com o nome de quem criou (RN-AP-56).
Cadeia de delegação não encadeia
Se A delega para B e B delega para C, a tarefa de A vai para B — e para. Encadear produz caminhos que ninguém consegue prever e é o vetor clássico de escapar da alçada em três saltos. B pode recusar a tarefa (POST …/decline): ela volta para A, com evento e aviso.
Execução
Instância, passo e tarefa
| Entidade | Estados | Transição terminal |
|---|---|---|
| ApprovalRequest | InApproval · Approved · Rejected · Returned · Cancelled | Rejected e Cancelled são finais: nenhuma rota sai deles, e o documento que volta a ser submetido depois de rejeitado abre instância nova. Approved é terminal condicional: sai só por revaluate (para InApproval, rodada nova) e por cancel (para Cancelled) — RN-AP-43. Returned volta ao requisitante com a instância viva, e sai por reenvio ou cancelamento. Não há Draft — o rascunho é do documento — nem Expired — nada vence por decurso (RN-AP-11). A tabela seguinte é a lista fechada de transições. |
| ApprovalStep | Pending · Active · Approved · Rejected · Skipped · Obsolete | Skipped só por activationMinValue, por dedupe ou pela dispensa da base no pedido que nasce de requisição aprovada (RN-AP-68) — nunca por falta de aprovador. Obsolete é o que a reavaliação produz. |
| ApprovalAssignment | Pending · Approved · Rejected · Returned · Obsolete · Reassigned | Obsolete quando outra tarefa do mesmo nível já fechou o passo em modo Any. A linha permanece: a trilha mostra quem mais estava na fila. |
| De | Para | Ato | Rota | Quem | Evento |
|---|---|---|---|---|---|
| — | InApproval | submeter | POST /approvals | módulo dono | approval-request.submitted · chain-resolved |
| — | Approved | cadeia inteira abaixo do activationMinValue, só com allowPolicyApproval (RN-AP-32); desfecho Applied na transação do envio (RN-AP-71) | POST /approvals | motor | approval-request.approved-by-policy |
| — | — | aprovação pela política com desfecho Refused(code): o envio é recusado, a instância não nasce e o documento continua em rascunho (RN-AP-71) | POST /approvals | motor | — |
InApproval | InApproval | aprovar tarefa que não conclui; o passo fecha pelo modo e o seguinte ativa | …/assignments/{aid}/approve | aprovador | approval-assignment.decided · approval-step.closed · approval-step.activated |
InApproval | Approved | aprovação que conclui, desfecho Applied (RN-AP-38) | …/assignments/{aid}/approve | aprovador | approval-request.decided (Approved) |
InApproval | InApproval | aprovação que conclui, desfecho Refused(code): nada gravado além da tentativa | …/assignments/{aid}/approve | aprovador | approval-request.decision-refused |
InApproval | Rejected | rejeitar, desfecho Applied | …/assignments/{aid}/reject | aprovador | approval-request.decided (Rejected) |
InApproval | Returned | devolver — não existe no BANK_ACCOUNT_CHANGE | …/assignments/{aid}/return | aprovador | approval-request.returned |
Returned | InApproval | reenviar: rodada nova, herança pela RN-AP-21 | POST /approvals/{id}/resubmit | módulo dono | approval-request.resubmitted |
InApproval · Returned | Cancelled | cancelar o documento, inclusive por contestação do fornecedor; desfecho sem efeito a desfazer | POST /approvals/{id}/cancel | módulo dono | approval-request.cancelled |
Approved | Approved | ajuste: alteração com (a), (b) e (c) da RN-AP-21 — sem rodada nova e sem tarefa —, desfecho Applied, que grava a diferença (RN-AP-71) | POST /approvals/{id}/revaluate | módulo dono | approval-request.revaluated |
Approved | Approved | ajuste com desfecho Refused(code): a alteração é recusada e o documento continua na versão aprovada (RN-AP-71) | POST /approvals/{id}/revaluate | módulo dono | approval-request.decision-refused |
Approved | InApproval | reabrir por alteração do documento que não é ajuste — valor que subiu, retrato ou cadeia que mudou, taxa alterada por override ou troca de moeda —, com rodada do zero, que reconverte o documento (Moeda e Câmbio, RN-CUR-22); o desfecho inverso é do módulo dono — a requisição reaberta não grava nada no razão (Orçamento, RN-O45). Recusada no BANK_ACCOUNT_CHANGE, cuja proposta é imutável e não tem rodada nova: APPROVAL_TRANSITION_NOT_ALLOWED_CONFLICT · 409 | POST /approvals/{id}/revaluate | módulo dono | approval-request.reopened · revaluated |
Approved | Cancelled | cancelar depois de aprovado; desfecho inverso | POST /approvals/{id}/cancel | módulo dono | approval-request.cancelled |
| Campo | Onde | O que guarda |
|---|---|---|
| policyId · policyVersion · levelOrder | passo | De onde este passo veio. Permite explicar a cadeia meses depois, com a política já editada três vezes. |
| scope · scopeRef · scopeAmount | passo | Document, Line ou Slice, com o id do alvo e o valor sobre o qual a alçada foi avaliada. |
| mode · quorum · slaDueAt | passo | Modo do nível, quórum exigido e o vencimento calculado na ativação. |
| authorityExceeded | passo | bool, gravado na ativação. Verdadeiro quando a subida esgotou sem ninguém com alçada para o valor e o passo foi aos Owners da raiz (decisão 7). O que acontece depois é da RN-AP-58. |
| resolvedOutOfScope | passo | bool, gravado na ativação. Verdadeiro quando o alvo do nível resolveu para quem não alcança a empresa do documento e o passo caiu no reserva. Escopo revogado depois do vínculo é o único caminho que deveria produzi-lo (RN-AP-39). |
| resolvedEmptyOnActivation | passo | bool, gravado na ativação. Verdadeiro quando o passo resolveu zero pessoas por passagem do tempo — vigência de membro de grupo vencida, papel sem detentor, Owner que deixou de ser vigente — e caiu no aprovador de reserva. É o terceiro irmão de authorityExceeded e resolvedOutOfScope (RN-AP-45). |
| roundNumber | instância e passo | int. A instância guarda a rodada corrente; cada passo guarda a rodada em que nasceu. É o que responde "quantas vezes este pedido rodou e o que mudou entre as rodadas" inclusive depois de uma reabertura (RN-AP-43). |
| version | instância | int. Token de concorrência da raiz: toda decisão passa expectedVersion e a divergência é 409. Não existe token no passo nem na tarefa — a serialização é uma só, na raiz (RN-AP-44). |
| inheritedFromStepId | passo | Guid?. O passo da rodada anterior cuja decisão aprovada este herdou (RN-AP-21). O passo nasce Approved, grava approval-step.inherited, e a decisão herdada conta com o actingUserId original na regra de pessoas distintas (RN-AP-56). |
| controlOfStepId | passo | Guid?. No passo Role(Controladoria) criado por alçada estourada ou por reserva, o passo marcado que ele controla (RN-AP-58). |
| awaitingEligibleApprovers | passo | bool. Verdadeiro quando o passo ativou sem pessoas elegíveis bastantes para o modo, nem com o reserva, e ficou parado com alerta (RN-AP-59). |
| criteriaSnapshot · criteriaSnapshotHash | instância | O retrato da rodada corrente — valores do documento, das fatias e das linhas, e todo atributo lido pelos critérios que o processo declara — e o seu hash, que cobre os atributos e não os valores nem a taxa (RN-AP-72). É o que a reavaliação compara (RN-AP-21); o retrato de cada rodada anterior fica no evento que a abriu. |
| activationNotBefore · activationHeld | instância | timestamptz? · bool. Nenhum passo ativa antes da data; com activationHeld, a instância espera o módulo dono informar a data (RN-AP-63). |
| valueUnavailable | instância | bool. O módulo dono devolveu o valor avaliado como indisponível; o valor fica nulo e não casa faixa (RN-AP-62). |
| ApprovalRequestExcludedUser | filha da instância | userId · reason (Requester · PreparedBy · Beneficiary · LastEditor · Proponent · RateOverrider · IdentityCorrectionLinked) · roundNumber. Quem não decide nesta instância, declarado pelo módulo dono na submissão e acrescido a cada reavaliação — nunca retirado (RN-AP-60). Dentro do agregado: não é raiz própria. |
| approverUserId | tarefa | Quem é o aprovador de direito. |
| actingUserId | tarefa | Quem de fato decidiu. Igual ao anterior, salvo delegação ou reatribuição. |
| origin · originRef | tarefa | Direct, ByRole, ByGroup, ByPermission, ByDelegation, ByEscalation, ByReassignment, ByFallback — com o id da delegação, do grupo, do papel ou da permissão. ByFallback marca toda tarefa que veio do aprovador de reserva, em qualquer processo (RN-AP-58). |
| decision · reasonCodeId · comment · decidedAt | tarefa | A decisão, o motivo codificado, o comentário e o momento. Em rejeição e devolução os dois são obrigatórios: o código classifica para o relatório, o comentário explica o caso para quem vai corrigir. A tarefa grava também reasonCode e reasonLabel congelados — ver Motivo. |
| Campo | Tipo | O que guarda |
|---|---|---|
| id · tenantId | Guid · Guid | Identidade e tenant, como em toda entidade do produto. |
| approvalRequestId | Guid | Sempre preenchido, inclusive nos eventos de passo e de tarefa. É a chave de agrupamento da trilha. |
| sequence | long | Monotônico dentro da instância, alocado na mesma transação que incrementa o version da raiz (RN-AP-44). A chave de ordenação é (approvalRequestId, sequence) — nunca occurredAt, que empata em decisão em lote e em fatias paralelas fechando juntas. |
| roundNumber | int | A rodada em que o fato aconteceu (RN-AP-43). É o que separa "o comitê aprovou" da primeira rodada do da terceira. |
| stepId · assignmentId | Guid? · Guid? | Nulos nos eventos de instância; o de tarefa carrega os dois. |
| action | enum | Enum fechado — a tabela seguinte. Acrescentar valor é migração, como em todo enum do produto. |
| outcome | string? | O desfecho, onde a ação o admite: Approved · Rejected · Returned · Cancelled · ApprovedWithProviso. É o que permite uma ação só para "decidiu" sem perder o quê. |
| actorUserId · actorType | Guid? · enum | Quem agiu e por qual tipo de aprovador (Role, CostCenterOwner, PermissionHolder…). actorUserId nulo é o motor — ativação de passo, aprovação pela política, vencimento de SLA —, e é o único caso de evento sem pessoa. |
| onBehalfOfUserId · delegationId | Guid? · Guid? | O aprovador de direito e a delegação em vigor, quando quem agiu não é quem responde (RN-AP-17). |
| policyId · policyVersion · levelOrder | Guid? · int? · int? | De onde veio o passo que produziu o evento. É o que explica a cadeia com a política já editada três vezes. |
| scope · scopeRef · scopeAmount · scopeCurrency | enum? · Guid? · decimal? · char(3)? | O escopo sobre o qual se decidiu e o valor avaliado, na moeda em que foi avaliado. |
| exchangeRate · quoteUnit · rateDate | decimal? · int? · date? | A prova da comparação de alçada (RN-AP-52): a taxa congelada na submissão, o fator de cotação da taxa efetivamente usada e a data dela, gravados no evento da decisão. Qual fator é esse em cada ramo é regra da Moeda (RN-CUR-11 e RN-CUR-27), que esta página cita e não redefine: com taxa informada por exchangeRateOverride não há linha ExchangeRate, e o fator é o do CurrencyPair, conferido e congelado com o override; sem rate — override só de tipo —, a taxa vem da resolução e o fator é o da própria linha, congelado nela na gravação. O fator está aqui pela mesma razão que está no movimento do razão: taxa sem fator não reconstitui a conta em moeda cotada por 100 — o iene comparado com o fator errado erra por duas ordens de grandeza, e a trilha guardaria a prova de uma conta que ninguém consegue refazer. Sem isso, "por que este valor coube nesta alçada" não tem resposta depois que a cotação mudou. |
| reasonCodeId · reasonCode · reasonLabel · comment | Guid? · string? · string? · text? | O motivo codificado com rótulo congelado e o comentário. Obrigatórios juntos em toda ação que não seja aprovar (RN-AP-19). |
| commentHash · commentErasedAt | string? · timestamptz? | Hash do comentário, gravado na criação, e o momento em que o texto foi apagado por LGPD — a única escrita que o evento admite (RN-AP-48). |
| subjectVersion · subjectHash | int? · string? | O que foi visto. A versão e o hash do conteúdo aprovável do documento — calculados pelo módulo dono e mandados na submissão, no reenvio e na reavaliação — gravados em toda decisão e na abertura de cada rodada (RN-AP-67). |
| occurredAt | timestamptz | Quando. Serve para medir, não para ordenar. |
| metadata | documento semiestruturado | O que é específico da ação — o alvo da reatribuição, o que mudou na reavaliação, o passo que a escalada acrescentou. Nunca o lugar de um campo pelo qual a consulta filtra: o que a GET /approvals filtra é coluna. |
| Alvo | Ações |
|---|---|
| Instância | approval-request.submitted · decision-refused · cancelled · chain-resolved · decided · returned · resubmitted · revaluated · reopened · approved-by-policy · budget-requirement-unmet |
| Passo | approval-step.activated · closed · sla-expired · skipped · obsoleted · escalated · authority-exceeded · resolved-out-of-scope · resolved-empty-on-activation · resolved-by-fallback · controller-required · inherited · stalled |
| Tarefa | approval-assignment.created · decided · reminded · info-requested · info-answered · reassigned · declined · delegated · obsoleted |
| Slug | Quando | Para quem |
|---|---|---|
nexio.approval-assignment.assigned | tarefa criada, delegada, reatribuída ou devolvida pelo delegado | quem recebe a tarefa |
nexio.approval-assignment.reminded | SLA vencido com Notify, ou vencimento em processo que não escala | aprovador e requisitante |
nexio.approval-assignment.info-requested · info-answered | pedido de informação e resposta | requisitante · quem perguntou |
nexio.approval-request.decided · returned · cancelled | desfecho da instância | requisitante |
nexio.approval-request.decision-refused | desfecho Refused(code) (RN-AP-38) | requisitante e dono do impedimento |
nexio.approval-step.authority-exceeded · controller-required | alçada estourada ou reserva em processo com valor (RN-AP-31, RN-AP-58) | controladoria |
nexio.approval-step.resolved-out-of-scope · resolved-empty-on-activation · resolved-by-fallback | a cadeia não saiu como o desenho previa (RN-AP-39, RN-AP-45, RN-AP-58) | controladoria |
nexio.approval-step.stalled | passo parado por falta de pessoas elegíveis (RN-AP-59) | administrador do tenant e controladoria |
nexio.approval-policy.published | versão de política publicada, com o diff (RN-AP-55) | controladoria |
nexio.approval-settings-change.applied | parâmetro crítico efetivado, com o diff (RN-AP-69) | controladoria |
nexio.control-role-member.granted · revoked | pessoa entra ou sai de papel referenciado por nível de controle — a Controladoria e qualquer outro; depende do evento de concessão da plataforma (RN-AP-69) | controladoria |
nexio.approval-request.exchange-rate-overridden | submissão de documento com taxa informada (RN-AP-70) | controladoria |
nexio.approval-delegation.created-by-other | delegação criada por quem não é o delegante (RN-AP-56) | delegante |
nexio.approval-delegation.expiring · nexio.approval-group-member.expiring | delegação ou membro de grupo referenciado por política ativa perto do validTo (RN-AP-45) | delegante · administrador do tenant |
Com base em quê. O metadata da decisão guarda os anexos vistos (fileId e sha256 dos anexos do documento e da instância no momento da decisão), e o de approval-step.activated, as pessoas resolvidas com a origem de cada uma — o GrydAuth não guarda histórico de papel, então é o evento que prova quem podia decidir naquele dia (RN-AP-67).
Imutável quer dizer sem rota de escrita. ApprovalEvent é append-only: não tem PUT, não tem DELETE, não tem soft delete — a exclusão lógica que o resto do produto herda da plataforma não se aplica aqui, e essa exceção é declarada, não esquecida. Corrigir um evento é gravar outro que o corrija, com metadata apontando o sequence corrigido. É a mesma disciplina do razão orçamentário, e pela mesma razão: uma trilha que pode ser editada não prova nada (RN-AP-48).
A única escrita permitida é apagar o texto livre de um comentário, por LGPD. Comentário é texto livre e pode carregar dado pessoal. Ele fica guardado enquanto durar a guarda do documento dono; pedido de eliminação sem base legal de guarda é atendido pelo ato POST /approvals/{id}/events/{sequence}/erase-comment, com erase:approval-comments e motivo: o motor substitui comment por nulo, grava commentHash (já calculado na criação) e commentErasedAt, e registra o ato como ato de controle, com evento pós-commit approval-comment.erased. Código de motivo, rótulo, autor, momento e decisão nunca são apagados.
Uma instância viva por documento. Reenviar um documento devolvido reaproveita a mesma ApprovalRequest, com uma nova rodada de passos e os anteriores marcados como Obsolete. Criar instância nova a cada reenvio parece mais limpo e quebra a pergunta "quantas vezes este pedido rodou e o que mudou entre as rodadas" — que é a métrica que expõe política mal configurada.
Mecânica
Congelar e resolver
| Momento | O que é fixado | Por quê |
|---|---|---|
| Submissão | Políticas aplicáveis e suas versões, valor avaliado, taxa de câmbio, rateio, limiar de materialidade efetivo, sequência de passos com tipo, escopo, modo e SLA, a lista de excluídos e o retrato dos critérios com o seu hash | Editar a política amanhã não pode mudar a cadeia de um documento que já está circulando. É o que torna a trilha defensável. |
| Ativação do passo | Os aprovadores concretos, a delegação vigente, os membros do grupo e o vencimento do SLA | Um responsável que mudou entre a submissão e a chegada do passo tem de ser o novo. Congelar pessoas na submissão produz tarefas para quem já saiu. |
| Nunca | Quem já decidiu | Decisão tomada é evento. Nenhuma reavaliação, edição de política ou troca de responsável apaga uma assinatura. |
Dedupe: quem já agiu sai do passo seguinte, e passo de mais de uma decisão nunca é pulado
Decidida na ativação, pelo actingUserId — quem de fato agiu, delegado incluído. Se Ana aprovou o passo anterior e está no seguinte: num passo que exige uma decisão (Any) e não é de controle, a decisão dela já cobre, e o passo vai a Skipped com o motivo — desde que ela tenha decidido um passo comum: decisão em passo de controle nunca cobre, ela sai do passo seguinte e ele continua com os demais; em passo de controle — toda complementar, o passo de controladoria e o quórum do BANK_ACCOUNT_CHANGE —, ela sai e nunca decide (RN-AP-64); num passo que exige mais de uma (All, Quorum(n)), ela sai do conjunto e o passo continua com os demais — nunca é pulado, e a decisão anterior dela não conta para o quórum. Configurável por processo — há empresas que exigem a dupla assinatura de propósito —, travado em falso no BANK_ACCOUNT_CHANGE (RN-AP-57). Caso de ataque: quem aprovou a fatia está também no papel da complementar de verba estourada; se a dedupe pulasse o passo, a base cobriria o controle. Agora a pessoa sai do passo de controle e outra decide, quórum nunca é pulado, e quem decidiu controle sai do passo comum seguinte em vez de levá-lo a Skipped.
O requisitante e os excluídos são removidos de toda tarefa da própria instância
Antes de tudo, e sem chave que desligue — o requisitante e todo excluído da lista que o módulo dono manda na submissão (RN-AP-60). Se a remoção esvaziar o passo, o motor sobe um nível nos tipos de árvore e cai no aprovador de reserva nos demais; se esgotar, a submissão é recusada com 422, e a mensagem diz qual passo ficou sem ninguém e que o excluído era o único aprovador possível — não "erro ao aprovar". No BANK_ACCOUNT_CHANGE, sem reserva, a instância fica parada com alerta (RN-AP-59).
Resolução vazia nunca vira passo pulado
É a diferença entre um sistema que controla e um que parece controlar. Ordem exata: tipo do nível (nos tipos de árvore, Owner vigente do nó; sem ele, Deputy vigente do mesmo nó) → rollup na árvore → delegação vigente → aprovador de reserva da política → aprovador de reserva do processo → recusa explícita. Em cada etapa só conta quem é elegível — conta ativa, permissão de agir, empresa do documento, fora dos excluídos —, e "vazio" é relativo ao modo: Quorum(n) com menos de n pessoas é vazio, e o reserva completa o conjunto, nunca reduz o quórum (RN-AP-59).
A recusa explícita é da submissão; a ativação tem outro desfecho. Percorrida a mesma ordem, um passo que ativa vazio por passagem do tempo não devolve 422: ele marca resolvedEmptyOnActivation, cai no reserva, emite evento e avisa a controladoria (RN-AP-45). O 422 continua sendo a resposta da submissão, quando quem chama é o módulo dono e ainda há para quem responder; devolvê-lo na ativação significaria recusar o POST de quem acabou de aprovar — e derrubar uma decisão legítima por um cadastro que venceu ontem.
Cadeia inteira pulada é aprovação pela política, com nome
Quando todos os níveis têm activationMinValue acima do valor avaliado, a instância termina Approved sem tarefa e grava approval-request.approved-by-policy, com a política e a versão — e o desfecho do processo é invocado na mesma transação: a requisição grava o Commitment, e o Refused recusa o próprio envio (RN-AP-71). É a única aprovação sem pessoa do MVP, e é declarada: aparece no simulador antes de submeter, e o /validate avisa que a política tem faixa sem decisor humano. Só existe em processo com allowPolicyApproval verdadeiro: nos de controle, a publicação recusa a política que permitiria isso, e a submissão recusa de novo como defesa (RN-AP-54).
Mecânica
Rateio e escopo do passo
Um documento tem N linhas; cada linha tem de 1 a N fatias de rateio, cada fatia com seu centro de custo e seu valor. O motor avalia as regras contra as linhas e fatias e decide sobre o documento inteiro — que é o padrão SAP, onde documentos são "aprovados na íntegra".
| # | Etapa | Detalhe |
|---|---|---|
| 1 | Agrega as fatias por centro de custo | Cinco linhas apontando para /TI/INFRA/ viram uma fatia agregada. Sem isso, o responsável recebe cinco tarefas do mesmo pedido. |
| 2 | Aplica a materialidade | Fatia abaixo do limiar — o menor entre allocationMaterialityPercent do valor do documento e allocationMaterialityAmount, que funciona como teto — é absorvida pela maior fatia para efeito de roteamento, e só. Continua contabilizada integralmente. O limiar é lido uma vez aqui e congela com a cadeia (RN-AP-06). Materialidade não é regra de orçamento: o orçamento sempre vê a decomposição integral, e Block numa fatia absorvida recusa a submissão do mesmo jeito (RN-AP-15). |
| 3 | Gera passos por escopo | Níveis com scopeMode = Slice geram um passo por fatia material, em paralelo entre si. Níveis com scopeMode = Line geram um passo por linha que satisfaz os critérios de linha da política de origem, com scopeRef = id da linha e scopeAmount = valor da linha — a materialidade não se aplica (RN-AP-46). Níveis de documento geram um passo só. |
| 4 | Deduplica aprovadores | Se a mesma pessoa responde por duas fatias, ela recebe uma tarefa que cobre as duas, com o valor somado no scopeAmount — e a alçada dela é reconferida contra a soma: não cobrindo, ela sai dessas fatias, cada uma continua a subida a partir do nó acima, e dedupe e reconferência se repetem até cobrir ou chegar à raiz (RN-AP-15). Em escopo Line a dedupe é por (pessoa, linha), não por (pessoa, fatia): quem responde pelo fiscal de duas linhas interestaduais recebe duas tarefas, porque o que ele examina é a linha. |
| 5 | Ordena | Passos de fatia do mesmo nível são paralelos entre si; os níveis seguem sequenciais. O documento avança quando todos os passos do nível fecham. |
Por que isso importa em números. Um pedido de R$ 400 mil rateado 90/10. Pelo valor total, as duas áreas precisam de aprovação de diretoria. Pela fatia, a área com R$ 40 mil resolve no gestor dela. Do lado oposto: sem limiar de materialidade, uma fatia de 0,5% — R$ 2 mil — geraria um aprovador próprio e transformaria toda compra rateada num carrossel de assinaturas. As duas metades da regra existem para evitar erros opostos, e é a combinação que nenhum concorrente pesquisado implementa.
Um dono só, e é o tenant. O limiar vive na configuração de tenant deste documento e não varia por processo. Duas razões, e a primeira é de fato: só dois dos dez processos-semente têm fatia — REQUISITION e PURCHASE_ORDER —, e eles carregam o mesmo rateio em dois momentos, de modo que limiares diferentes fariam o pedido reabrir cadeia para fatias que a requisição já tinha dispensado. Nos outros oito não há o que dispensar: a alteração bancária não tem valor, o exercício avalia o total orçado, e cadastro, homologação e curadoria não têm valor nenhum. A segunda razão é de controle: materialidade é dispensa — subir o número remove aprovadores —, e com herança por processo a tela do tenant mostraria 5% enquanto um processo rodasse a 30%. Com um valor só, a garantia da RN-AP-15 é uma consulta e uma linha de trilha. É a mesma escolha que tirou o allowSelfApproval em vez de lhe dar padrão: aperta-se a regra, não o controle. Se um dia aparecer o caso real de variar por processo, dois campos anuláveis no ApprovalSettings herdando deste valor são adição não-quebrante — o que não se desfaz é um número que já divergiu em produção.
Execução
Ações do aprovador
| Ação | Efeito | Comentário |
|---|---|---|
| Aprovar | Fecha a tarefa. O passo fecha conforme o modo; a instância avança para o próximo nível. | opcional |
| Rejeitar | Encerra a instância. O documento volta ao requisitante como rejeitado e não pode ser reenviado sem alteração. | obrigatório |
| Devolver para ajuste | Devolve ao requisitante sem encerrar. Os passos da rodada viram Obsolete; o reenvio abre uma rodada nova na mesma instância, que só herda decisão pela RN-AP-21. Não existe no BANK_ACCOUNT_CHANGE: a proposta é imutável, e corrigir é rejeitar e abrir outra (RN-AP-61). | obrigatório |
| Solicitar informação | Pergunta ao requisitante sem tirar o documento da fila. O SLA pausa enquanto aguarda — senão o aprovador é penalizado por uma resposta que não depende dele. | obrigatório |
| Reatribuir | Passa a tarefa a outra pessoa elegível, uma vez, sem alterar o passo — nunca a excluído nem a quem já tem tarefa ou decisão no passo (RN-AP-56, RN-AP-59); em passo de controle, só a quem pertence ao alvo do nível e não decidiu outro passo da rodada (422, RN-AP-64); não existe no BANK_ACCOUNT_CHANGE. Exige permissão própria e gera evento com origem e destino. | obrigatório |
| Aprovar com ressalva | Aprova e registra uma condição a cumprir — "mediante apresentação da ART". Não bloqueia o avanço; fica visível no documento e no recebimento. | obrigatório |
Devolver não é rejeitar, e essa distinção é o que faz o SLA significar algo
Sem "devolver", todo pedido com um erro de digitação é rejeitado e reenviado como novo — e a métrica de aprovação passa a medir qualidade de digitação em vez de decisão. É a ação mais usada em qualquer sistema que a oferece, e a primeira que falta em quem não a modelou.
Aprovação em lote existe; decisão em lote não
A tela pode aprovar dez tarefas numa ação, e o motor grava dez eventos, um por tarefa, cada um com seu timestamp. Um evento de lote pareceria uma decisão só e destruiria a rastreabilidade individual que a auditoria pede.
Mecânica
Mudança depois de aprovado
Um pedido aprovado por R$ 80 mil que vira R$ 180 mil com uma edição é o buraco mais explorado em sistema de aprovação. A resposta não pode ser "reprova tudo" nem "não reavalia".
| Mudança | Comportamento | Regra |
|---|---|---|
| Valor do documento, de uma fatia ou de uma linha sobe — qualquer quantia | Rodada nova do zero: todas as decisões anteriores vão a Obsolete. Não há tolerância de aumento. | RN-AP-21 (a) |
| Muda atributo lido por critério do processo | Rodada nova do zero — centro de custo, rateio, conta contábil, categoria, tipo de item, destinação, empresa, estabelecimento, UF de entrega, fornecedor, flags, estado do orçamento. O hash do retrato mudou. | RN-AP-21 (b) |
| A cadeia recalculada ganha passo | Rodada nova do zero, mesmo sem mudança no documento — a política vigente passou a exigir mais. | RN-AP-21 (c) |
| Valor igual ou menor, nenhum atributo de critério mudou, cadeia igual ou menor | Ajuste, sem reabrir: a instância continua Approved, sem rodada nova e sem tarefa, e o desfecho aplica a diferença — na requisição, a verba muda de mês ou cai; o Refused recusa a alteração. O evento registra o que mudou. No reenvio de documento devolvido, o mesmo teste preserva: cada passo que continua na cadeia herda a decisão aprovada (inheritedFromStepId). | RN-AP-71 · RN-AP-21 |
| Muda algo que nenhum critério lê | Ajuste pelo mesmo teste — quantidade sem mudar valor, descrição, prazo que não muda o estado do orçamento, anexo. | RN-AP-71 |
Preservar só quando nada do que o controle leu mudou. A assinatura vale para o que a pessoa viu. Valor maior, critério diferente ou cadeia maior é outro documento para o controle, e a rodada recomeça. Não há tolerância de aumento, por duas razões: tolerância em percentual é exatamente a faixa que um fracionamento explora; e "a pessoa ainda cobre o novo valor" exigiria reler alçada, escopo e vigência de cada assinatura antiga — um segundo motor dentro do primeiro. O custo é conhecido e aceito: edição que sobe o valor em um centavo pede as assinaturas de novo. Caso de ataque: pedido de R$ 80 mil com a fatia de TI de R$ 60 mil aprovada por Carla, limite R$ 150 mil; editado para R$ 250 mil, a fatia ia a R$ 187,5 mil e a assinatura dela era preservada acima da alçada — e editar a UF de entrega depois de aprovado escapava do fiscal. Agora o valor que sobe recomeça a rodada, e a UF está no retrato (RN-AP-21).
Sair de Approved é transição declarada, não improviso do módulo
O documento aprovado cuja edição não é ajuste (RN-AP-71) volta a InApproval por POST /approvals/{id}/revaluate, com rodada do zero; o que é cancelado depois de aprovado vai a Cancelled por POST /approvals/{id}/cancel. As duas são transições do contrato, com evento próprio (approval-request.reopened e approval-request.cancelled) e roundNumber incrementado. A instância continua sendo uma só — que é o que sustenta a pergunta "quantas rodadas este pedido teve". Sem a transição escrita, cada módulo transacional inventaria a sua: uns reabrindo, outros criando uma segunda instância, e a métrica de rodadas morreria no primeiro que escolhesse diferente.
A reabertura passa pelo desfecho sem gravar no razão
Reabrir passa pelo contrato de desfecho no sentido inverso, na mesma transação (RN-AP-38), e o que o efeito faz é do módulo dono. Na requisição, reabrir não grava nada no razão: o comprometido aberto continua preso, e a reaprovação grava só a diferença da parte ainda não pedida; a parte já pedida conserva data e rateio do ato que a pediu, e cancelar a requisição reaberta estorna o aberto (Orçamento, RN-O45). Liberar na reabertura soltaria a verba enquanto a edição corre, e outro documento a consumiria. A edição que é ajuste não reabre: grava a mesma diferença na hora, pelo desfecho de ajuste (RN-AP-71).
Execução
SLA e escalonamento
O vencimento é calculado na ativação do passo, em horas úteis: o relógio só corre nos dias da semana marcados como úteis, dentro do expediente do tenant, e pula as datas da lista de dias não úteis. Contar em horas corridas faz todo passo ativado na sexta vencer no domingo, e o escalonamento vira ruído semanal.
Não existe BusinessCalendar. O expediente e a lista nonWorkingDates são parâmetros do tenant, e o produto semeia a lista com os feriados nacionais — que são algoritmo, não cadastro: as datas fixas mais as derivadas da Páscoa, geráveis anos à frente sem ninguém digitar. O tenant acrescenta o feriado municipal e o recesso, ou tira o que não vale ali. Entidade com ciclo de vida próprio só se pagaria com um segundo consumidor, e não há: prazo de cotação e prazo de entrega contam em dias corridos, como o resto do produto. Três limites, registrados agora para não voltarem como defeito: a lista é do tenant, não do estabelecimento — filial em outro município acrescenta a data à lista de todos ou convive com ela —, não existe meio expediente, e errar uma data adianta um lembrete, nunca uma decisão (RN-AP-11).
Definido por processo, em escalationBehavior: Notify (lembrete ao aprovador e ao requisitante), Escalate (acrescenta uma tarefa, mantendo a original viva) e None. Não existe AutoApprove. O processo diz se escala; o nível diz para quem — só ele sabe a própria forma de resolução. NotifyManager não existe: avisar "o gestor" exige a árvore de subordinação do GrydAuth, que é fase 2 — a mesma dependência que mantém RequesterManager fora do MVP. Quem precisa do gestor hoje escala pela árvore de centro de custo, que é a hierarquia que o produto tem.
Escalate não tem destino implícito. Em nível que caminha árvore (CostCenterOwner, CostCenterOwnerUpToAmount) continua sendo o responsável do nó pai — a hierarquia existe e o simulador a desenha. Em Role, SpecificUser, ApprovalGroup e PermissionHolder não existe "nível acima", e o nível precisa declarar escalationTarget, no mesmo vocabulário do próprio nível. Sem alvo, a política não publica: APPROVAL_ESCALATION_TARGET_REQUIRED_UNPROCESSABLE. O motor não inventa aprovador em nenhum outro ponto desta spec, e não passa a inventar no vencimento — cair no aprovador de reserva transformaria um nome só no aprovador universal de tudo o que atrasa, e bastaria deixar o relógio correr para chegar nele.
allowEscalation por processo, false na semente do BANK_ACCOUNT_CHANGE: no vencimento, só lembrete. Escalonar é ganho de produtividade; num controle antifraude é superfície de ataque — quem quisesse outro decisor bastaria não decidir até o prazo virar. Mesmo raciocínio do gate 9, que tirou a dissidência do Quorum em vez de afrouxar a RN-AP-10. Publicar política com Escalate em processo que não escala é APPROVAL_ESCALATION_NOT_ALLOWED_UNPROCESSABLE.
Em Any, a tarefa original continua pendente e a escalada acrescenta um caminho: as duas fecham o passo, e o evento diz qual chegou primeiro. Em All e Quorum(n) a escalada substitui a tarefa do titular — que vai a Obsolete —, porque acrescentar um caminho ali é reduzir o número de pessoas distintas que o modo exige: num Quorum(2) de quatro olhos, o titular atrasa, a escalada entra, e o quórum fecha com uma assinatura de gente que decidiu e outra de quem entrou por decurso de prazo. O número de pessoas distintas exigido pelo modo não muda nunca — é o que o modo significa (RN-AP-50).
E o alvo da escalada passa pelas mesmas guardas do alvo original: RN-AP-08 (o requisitante nunca decide a própria instância), alçada — o escalado precisa cobrir o scopeAmount do passo, ou o passo herda a marca authorityExceeded e segue authorityExceededBehavior (RN-AP-58) — e escopo de empresa (RN-AP-39/40). Um alvo de escalonamento que escapasse dessas três seria a porta que a RN-AP-08 fecha na entrada e deixaria aberta no vencimento.
A pergunta avisa o requisitante (nexio.approval-assignment.info-requested) e pausa o SLA do passo; a resposta entra por POST /approvals/{id}/answer-info, rota do requisitante, avisa quem perguntou (nexio.approval-assignment.info-answered) e retoma o relógio. Os dois sentidos têm aviso: sem o da ida, o documento pararia esperando uma resposta que ninguém foi avisado de dar. O tempo pausado fica registrado, para que o indicador separe "aprovador lento" de "requisitante lento".
Gancho declarado
Orçamento e custo
O módulo de controle financeiro será especificado à parte. O que este documento fixa é onde ele toca o fluxo, para que a integração não exija reescrever o motor.
| Ponto de contato | Direção | Contrato |
|---|---|---|
| Critério de política | o motor lê | POST /evaluate:budget-state devolve, por fatia, o signal — Ok, Warning, Exceeded ou Uncovered —, a overBudgetPolicy já resolvida, o policySource de cada campo e o signalFrom: qual linha da pilha de ancestrais produziu o sinal, com o path do nó. A política é avaliada sobre toda a pilha e vence a mais restritiva, de modo que o nó que bloqueou pode não ser o nó da fatia — sem signalFrom a recusa diz "estourou" sem dizer onde, e a marca do passo e a trilha herdam a mesma cegueira. O critério estado do orçamento lê o sinal, e é o que permite a política "estouro de verba exige o controller" em vez de bloquear a compra. |
| Empenho na aprovação | o motor chama | A decisão que conclui a instância invoca o contrato de desfecho do processo — um por ApprovalProcessCode — na mesma transação, com as fatias, os valores congelados e o sinal de orçamento com que a rodada foi montada. O Orçamento devolve Applied — documento em Approved e Commitment gravado —, Refused(code) — estouro sob a trava: a decisão não é gravada e a tarefa continua ativa — ou Failed. Na aprovação pela política e no ajuste do documento aprovado, quem invoca o desfecho é o ato que os provoca, e o Refused recusa o ato (RN-AP-71). O motor continua sem conhecer verba: ele conhece o contrato (RN-AP-38). |
| Liberação no cancelamento | o motor chama | Cancelar depois de aprovado percorre o mesmo contrato, no sentido inverso e na mesma transação. Empenho que não é liberado é o defeito que faz o orçamento "acabar" em setembro — e é precisamente o defeito que a entrega por evento produz quando o consumidor falha em silêncio. Cancelar em InApproval invoca o contrato sem efeito a desfazer. |
| Bloqueio no estouro | o motor obedece | Quem decide é a overBudgetPolicy resolvida pelo orçamento — por nó de centro de custo, com vigência, herança campo a campo e tolerância —, nunca um parâmetro do motor. Block em qualquer fatia — material ou absorvida pela materialidade — recusa a submissão inteira, repassando BUDGET_EXCEEDED_UNPROCESSABLE sem reembrulho. Na decisão final, o mesmo código chega como Refused do contrato de desfecho: a aprovação não é gravada (RN-AP-38). Uncovered bloqueia sempre, independentemente de overBudgetPolicy, e com código próprio — BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE, também repassado sem reembrulho: fatia contra dimensão orçamentária inexistente não é estouro tolerável, é ausência de verba, e é o orçamento que o diz (RN-AP-53). RequireApproval não é comando novo para o motor: é o cliente escrevendo uma política Supplemental com o critério estado do orçamento — e aposentar essa política avisa quem dependia dela (RN-AP-41), e a exigência declarada que a cadeia não cumpriu é marcada e avisada (RN-AP-42). Ver Orçamento. |
| Alçada por fatia | compartilhado | A mesma fatia que a alçada avalia é a que o orçamento consome. Uma decomposição só, calculada uma vez, congelada na linha. A materialidade não abre uma segunda decomposição: ela decide se a fatia gera passo próprio, nunca se ela é consumida. O orçamento recebe todas as fatias, e Block em qualquer uma — absorvida ou não — recusa a submissão inteira. Sem isso a marca da RN-AP-42 dispararia no funcionamento normal, e controladoria que vê marca todo dia aprende a ignorá-la. |
Diferencial
Simulador
POST /approvals:simulate recebe o mesmo corpo de POST /approvals — o assunto com todo atributo que os critérios do processo leem, linhas, fatias e excluídos —, mais policyOverrides[] (versões Draft que substituem, na simulação, a publicada da mesma política) e asOf (casa contra as políticas vigentes naquela data), e devolve a cadeia que ele produziria, sem criar nada — passo a passo, com quem resolve em cada um, as marcas de exceção onde a cadeia não sai como o desenho previa — authorityExceeded, resolvedOutOfScope — e o limiar de materialidade efetivo com as fatias que ele dispensou, para que "por que esta área não aparece na cadeia?" tenha resposta antes de alguém perguntar. Três usos, e o terceiro é o que ninguém oferece:
Testar uma política antes de publicar, inclusive numa versão em rascunho. Configuração de aprovação é o lugar do produto onde o erro só aparece quando um pedido trava, semanas depois.
A prévia mostrada antes da submissão — "este pedido passará por Ana, depois pelo comitê" — é POST /approvals:preview: o módulo dono a chama sobre o rascunho, dentro da requisição de quem pode editá-lo, com a mesma seleção e sem policyOverrides nem asOf. Quem requisita não precisa de read:approval-policies. Expectativa correta é metade das reclamações de fluxo de aprovação.
Reexecutar uma instância antiga com asOf na data da submissão e comparar com o que aconteceu. A seleção de políticas e versões se compara exatamente; as pessoas resolvidas, não — o GrydAuth não guarda histórico de papel —, e por isso a prova de quem podia decidir vem do evento approval-step.activated, e a do que foi visto, de subjectHash e dos anexos da decisão (RN-AP-67).
Na prática
Exemplos de fluxo
Tudo o que um tenant consegue montar é combinação das peças definidas acima: um ApprovalProcess, a política Base que casa o documento, as Supplemental que empilham, e níveis com tipo, modo, escopo, valor de ativação e prazo. Esta seção existe para o time ver as combinações com números — um exemplo por processo-semente, uma requisição acompanhada do começo ao fim, e os padrões que se repetem entre processos. Nenhum exemplo usa recurso que não esteja nas seções anteriores; onde um parece pedir algo novo, a resposta está em "o que não dá para montar", no fim.
As peças e o que cada uma muda
| Peça | Opções | O que muda no fluxo |
|---|---|---|
| Política | Base — concorre por prioridade, uma vence · Supplemental — empilha, todas que casam entram | A base decide a espinha da cadeia; cada complementar acrescenta níveis na posição que o stage de cada nível dá. "OU" entre condições é duas complementares. |
| Critérios | Quinze, em conjunção, cada um no seu alcance (documento, linha, fatia); ausente casa tudo; nós de centro de custo, categoria e conta contábil valem por subárvore | Quem casa. Faixa de valor e nó são os mais usados; flags do assunto, estado do orçamento, fornecedor e UF de entrega são os que acionam controle extra. |
| Tipo do nível | SpecificUser · Role · PermissionHolder · ApprovalGroup · CostCenterOwner · CostCenterOwnerUpToAmount | Quem resolve — e se a resolução caminha a árvore de centro de custo ou lê uma lista. |
| Modo | Any · All · Quorum(n) | Quantas decisões fecham o nível. Em todos, uma rejeição encerra a instância. |
| Escopo | Document · Line · Slice | Sobre o que a alçada é avaliada e quantos passos o nível gera: um por fatia material, em paralelo, ou um só para o documento. Cada processo admite só alguns (RN-AP-54). |
| Ativação | activationMinValue | Abaixo do valor o nível não entra — "diretor só acima de R$ 100 mil" sem escrever outra política. |
| Alçada | CostCenterAssignment.approvalLimit, lida por CostCenterOwnerUpToAmount | Sobe a árvore até quem cubra a fatia; esgotada a subida, os Owners da raiz decidem com authorityExceeded marcado, e o processo exige a controladoria ou recusa (RN-AP-58). |
| Prazo | maxApprovalHours por nível, política ou processo · escalationBehavior só por processo · escalationTarget no nível | O prazo herda de baixo para cima; a resposta ao atraso, não. escalationBehavior tem um dono só — o processo, em ApprovalSettings —, para que o que acontece num vencimento não dependa de qual política casou; o para quem mora no nível, porque depende do tipo dele. Lembrete, tarefa declarada ou nada. Nunca aprovação. |
| Ausência | ApprovalDelegation, uma vigente por processo, com teto de valor e fim obrigatório | Para quem a tarefa vai enquanto o titular está fora — e o que continua com o titular: o que passa do teto, e todo processo marcado delegable = false. |
| Ações | aprovar · aprovar com ressalva · devolver · pedir informação · reatribuir · rejeitar | Devolver abre rodada nova na mesma instância; rejeitar encerra; pedir informação pausa o SLA. |
| Mudança | criteriaSnapshotHash — sem parâmetro | Toda edição reavalia; é ajuste, sem reabrir, só se o valor não subiu, nenhum critério mudou e a cadeia não cresceu (RN-AP-71); senão, rodada do zero. |
Um exemplo por processo
Nomes, valores e centros de custo são inventados; a mecânica é a da spec. "Cadeia" é o que o simulador mostra ao requisitante antes de submeter.
REQUISITION · alçada por fatia, diretor só acima de R$ 100 mil, DPO por complementar
Configuração. Base "até R$ 50 mil" (prioridade 10): nível 1 CostCenterOwner · Any · Slice. Base "de R$ 50 mil a R$ 500 mil" (prioridade 20): nível 1 CostCenterOwnerUpToAmount · Any · Slice; nível 2 Role(Diretor de Compras) · Any · Document com activationMinValue R$ 100 mil. Supplemental "categoria /TI/SOFTWARE/ com a flag dado pessoal" — dois critérios de linha, categoria /TI/SOFTWARE/ e flags do item com personalData: um nível SpecificUser(DPO) · Document. Materialidade do rateio, que é do tenant e vale para todos os processos: 10%.
Documento. Ana, de /TI/INFRA/, requisita R$ 80 mil em licenças de um CRM que o catálogo marca com handlesPersonalData, rateados 75% em /TI/INFRA/ (R$ 60 mil) e 25% em /MARKETING/ (R$ 20 mil). Bruno é Owner de /TI/INFRA/ com limite de R$ 50 mil; Carla é Owner de /TI/ com limite de R$ 150 mil; Diego é Owner de /MARKETING/ com limite de R$ 50 mil.
Cadeia. Casam a base de R$ 50–500 mil e a complementar. Nível 1, por fatia, em paralelo: a fatia de TI sobe de Bruno (R$ 50 mil não cobre R$ 60 mil) para Carla; a fatia de Marketing fica com Diego. Nível 2 não ativa — R$ 80 mil está abaixo de R$ 100 mil — e o passo nasce Skipped. Depois, o passo do documento: o DPO. Ana vê "Carla e Diego em paralelo, depois o DPO" antes de submeter. Com R$ 120 mil, o diretor entraria entre os gestores e o DPO; se Ana fosse ela própria a Owner de /TI/INFRA/, seria removida e a fatia subiria para Carla do mesmo jeito.
QUOTATION · fornecedor ainda não homologado passa por quem homologa
Configuração. Base de reserva (critérios vazios): nível 1 Role(Gerente de Compras) · Any · Document. Supplemental "fornecedor da proposta em estado Prospect": nível PermissionHolder(update:supplier-qualifications) · Any.
Documento. O comprador fecha a cotação escolhendo a proposta da Beta Serviços, que ainda é Prospect.
Cadeia. Gerente de compras, depois quem homologa. Com um fornecedor SpendAuthorized, só o gerente. Exigir justificativa aprovada por não ter escolhido a proposta mais barata é critério de v2, que nasce com a spec da Cotação.
PURCHASE_ORDER · alçada acumulada, verba estourada e comitê por quórum
Configuração. Base "acima de R$ 200 mil" (prioridade 30): nível 1 CostCenterOwnerUpToAmount · Any · Slice; nível 2 ApprovalGroup(Comitê de Compras) · Quorum(2) · Document, quatro membros. Supplemental "estado do orçamento = Exceeded": nível Role(Controladoria) · Any · Document, com stage entre o da alçada e o do comitê. Supplemental "fornecedor sancionado": nível Role(Compliance) · Any.
Documento. Pedido de R$ 420 mil, 90% em /OPERACOES/MANUTENCAO/ (R$ 378 mil) e 10% em /OPERACOES/LOGISTICA/ (R$ 42 mil). O Owner de manutenção tem limite de R$ 100 mil; o de /OPERACOES/, R$ 300 mil; Fernanda, CFO, é Owner da raiz com hasUnlimitedAuthority. O orçamento devolve Exceeded para a fatia de manutenção; a overBudgetPolicy do nó é RequireApproval, que é exatamente a complementar acima.
Cadeia. A fatia de manutenção sobe dois níveis e chega a Fernanda num passo só — cada subida não é um passo; a de logística fica com o Owner local. Depois a controladoria, em passo de controle por ser nível de complementar (RN-AP-64): se o Owner de /OPERACOES/ ou Fernanda também for do papel, sai das tarefas desse passo e outra pessoa decide — a dedupe nunca o pula. Depois o comitê: duas aprovações de quatro fecham, um "não" encerra; quem decidiu a controladoria sai das tarefas do comitê, que continua com os demais membros (RN-AP-64). Variante de alçada estourada: se Fernanda tivesse limite de R$ 250 mil e ninguém na raiz tivesse autoridade ilimitada, a fatia de R$ 378 mil iria a ela mesmo assim, com authorityExceeded marcado na ativação e, pela semente RequireControllerApproval, um passo paralelo Role(Controladoria) · Any no mesmo nível — passo de controle, decidido por pessoa distinta de Fernanda e de quem decidir a complementar —, sem o qual o nível não avança (RN-AP-58); a tarefa mostra a marca e nexio.approval-step.controller-required chega à controladoria. Com Refuse, a submissão teria sido recusada com APPROVAL_AUTHORITY_EXCEEDED_UNPROCESSABLE · 422. E o /validate teria apontado a raiz antes de a política ser publicada.
CONTRACT · jurídico sempre, comitê e CEO acima de R$ 1 milhão
Configuração. Base "até R$ 1 milhão" (prioridade 20): Role(Jurídico) · Any · Document, depois Role(Diretor de Suprimentos) · Any. Base "acima de R$ 1 milhão" (prioridade 10): Role(Jurídico) · Any, depois ApprovalGroup(Comitê de Contratos) · Quorum(2) · Document, depois SpecificUser(CEO). Supplemental "categoria /SERVICOS/TI/": SpecificUser(CISO).
Documento. Contrato de 36 meses com a Beta para monitoramento de rede, R$ 1,2 milhão comprometidos, categoria /SERVICOS/TI/MONITORAMENTO/.
Cadeia. Jurídico, CISO (a complementar entra no stage que declara, RN-AP-65, e é passo de controle, RN-AP-64), comitê com dois de quatro, CEO. Prazo e cláusula de exclusividade ainda não são critérios — a spec do Contract decide; até lá, contrato longo se recorta pelo valor total comprometido. É um dos três processos em que a votação com dissidência tolerada (Vote(n), fase 2) poderá ser ligada; hoje, no comitê, um voto contrário encerra.
SUPPLIER_REGISTRATION · quem cadastrou não aprova
Configuração. A semente: PermissionHolder(approve:suppliers) · Any · Document. Supplemental "flag tipo de pessoa = física": nível Role(Controladoria) · Any.
Documento. Ana, compradora com approve:suppliers, cadastra a Beta Serviços ME. Bruno e Carla também têm a permissão.
Cadeia. Ana é removida (RN-AP-08); Bruno e Carla recebem a tarefa, e o primeiro que decidir fecha. Faltou certidão? Bruno devolve, com motivo e comentário; Ana completa e reenvia na mesma instância. Fornecedor sancionado? Rejeita, e o motivo vai no uso SupplierReject. Tenant pequeno: se Ana fosse a única com a permissão, a submissão seria recusada com 422 nomeando o passo — é o onboarding que dá a permissão a mais alguém ou define o aprovador de reserva, nunca uma exceção à regra.
Correção de identidade. Meses depois, Bruno pede a correção de um dado de identidade da Beta que o Fornecedor trava depois da aprovação (POST /suppliers/{id}:correct-identity). O Fornecedor abre outra instância SUPPLIER_REGISTRATION com a flag identityCorrection e Bruno como Requester; o cadastro continua Approved. Casam a base e a complementar semeada da controladoria, que é passo de controle: Bruno não decide nenhum dos dois, Carla decide a base, e quem decide a controladoria é outra pessoa (RN-AP-64). Aprovada, o desfecho aplica o valor novo na mesma transação — ou devolve Refused(SUPPLIER_IDENTITY_CORRECTION_ACCOUNT_MISMATCH_CONFLICT) se alguma conta deixaria de conferir com o titular novo, e a tarefa continua ativa. Trocar a Beta para um tipo de pessoa sem CNPJ — ou de e para Foreign — não chega ao motor: o Fornecedor recusa antes de abrir instância (RN-FOR-39).
SUPPLIER · quatro áreas, uma depois da outra
Configuração. A semente é PermissionHolder(update:supplier-qualifications) · Any; o tenant a substitui por quatro níveis em sequência: Role(Jurídico) · Any, ApprovalGroup(Financeiro) · Any, Role(Engenharia) · Any e Role(Qualidade) · Any, todos Document.
Documento. Homologação da Beta para /SERVICOS/ENGENHARIA/.
Cadeia. Quatro passos em sequência, uma decisão de cada área; a primeira rejeição encerra. Aprovada, a homologação grava o resultado e promove a Beta a SpendAuthorized na categoria. As quatro áreas decidindo ao mesmo tempo, num nível só, é v2.
BANK_ACCOUNT_CHANGE · quatro olhos, um telefonema, e um "não" basta
Configuração. A definição normativa, que o tenant não desliga (RN-AP-61): base PermissionHolder(approve:supplier-banks) · Quorum(2) · Document; complementares semeadas pelas flags titular ≠ fornecedor (só em fornecedor Foreign), noTrustedPhone, previousNoticeUndeliverable e contestedChannel: Role(Controladoria) · Any · Document. O tenant acrescentou a complementar "flag chave PIX não verificada": Role(Tesouraria) · Any. Sem reserva, devolução, dedupe, reatribuição, escalonamento ou delegação, e sem faixa de valor — o processo não tem valor.
Documento. Eduardo, de compras, registra a proposta de troca de conta da Beta, com chave PIX aleatória ainda não resolvida. Seis pessoas têm approve:supplier-banks, ele entre elas, e uma está com a conta desativada. O Fornecedor manda o aviso por e-mail ao contato financeiro antigo e submete com activationNotBefore no fim da janela mínima e Eduardo como Proponent.
Cadeia. Até a data, nenhuma tarefa. Na ativação, Eduardo é excluído e a conta desativada não é elegível: quatro tarefas. Fernanda liga para o telefone confiável do fornecedor — qual número serve e quem pode registrar a ligação é regra do Fornecedor (RN-FOR-29) —, o fornecedor confirma, ela registra o callback Confirmed na proposta e aprova; Gustavo vê a evidência na tarefa, aprova, e o quórum fecha com duas pessoas distintas. Depois a Tesouraria, em passo de controle: decide quem não esteve no quórum (RN-AP-64). Aprovada, o Fornecedor aplica a proposta e inicia a carência. Se o contato antigo tivesse respondido "não fui eu" pelo link do aviso, o Fornecedor teria cancelado a instância; se Fernanda tivesse registrado Denied, a proposta estaria contestada e a instância, cancelada. É por este processo que Quorum não tolera dissidência: uma pessoa do financeiro desconfiar tem de bastar. Vote(n) nunca entra aqui. Variante de contato: se a proposta fosse a troca do e-mail do contato de aviso de um fornecedor sem telefone confiável, a instância nasceria com noTrustedPhone: sem callback, com a controladoria na cadeia, o pedido de troca assinado já anexado, do tipo declarado e com veredito Available — sem conferência do documento —, e o contato novo valendo só depois da carência de 7 dias — regras do Fornecedor (RN-FOR-40, RN-FOR-34).
ITEM · curadoria com controle extra por flag e por categoria
Configuração. A semente: PermissionHolder(approve:catalog-items) · Any. Supplemental "flag regulado": Role(Segurança do Trabalho) · Any. Supplemental "categoria /QUIMICOS/": Role(Engenharia de Processo) · Any.
Documento. Ana cria "Ácido sulfúrico 98% · tambor 200 L" em /QUIMICOS/ACIDOS/, com a flag regulado.
Cadeia. Aprovador do catálogo, Segurança, Engenharia — a ordem entre as complementares é o stage de cada uma (RN-AP-65), e as duas são passos de controle, decididos por quem não aprovou o catálogo (RN-AP-64). Rejeição grava o motivo ItemReject no item e avisa a criadora (nexio.item.rejected, além do decided). Importação com autoApprove só existe na carga inicial do catálogo — janela única por tenant, dupla custódia (quem importa não confirma a carga), evento próprio e ato de controle; a regra é do Item. Fora da janela, item importado entra em aprovação e passa por esta cadeia.
BUDGET · o exercício passa pela diretoria inteira
Configuração. Base de reserva: Role(Controladoria) · Any · Document, depois ApprovalGroup(Diretoria) · All · Document, depois SpecificUser(CEO). Supplemental "empresa = filial em regime especial": SpecificUser(Diretor da filial).
Documento. Exercício 2027 da Empresa Alfa, R$ 48 milhões orçados.
Cadeia. Controladoria — aqui nível da base, portanto passo comum: passo de controle se reconhece pela posição na cadeia, nunca pelo nome do papel (RN-AP-64); os cinco diretores, todos; CEO. Um "não" de qualquer diretor encerra — e o exercício volta como rejeitado, com motivo. Aprovado, o exercício é publicado e a versão anterior, arquivada. "Três de cinco bastam mesmo com dois contra" é o Vote(n) de fase 2, e este é um dos processos onde ele será permitido.
BUDGET_CHANGE · remanejamento por valor e por conta de destino
Configuração. Base "delta até R$ 50 mil" (prioridade 10): Role(Analista de Orçamento) · Any · Document. Base "acima de R$ 50 mil" (prioridade 20): Role(Analista de Orçamento) · Any no stage 10 e Role(Controladoria) · Any no stage 20. Supplemental "conta contábil de destino em /1/ (investimento)": SpecificUser(CFO) no stage 15. Ricardo é o CFO e também membro da controladoria.
Documento. Remanejar R$ 150 mil de /MARKETING/EVENTOS/ para /MARKETING/DIGITAL/, conta de destino 1.2.3 (investimento em software).
Cadeia. Analista no stage 10; Ricardo no stage 15, num passo de controle — é complementar (RN-AP-64); a controladoria no stage 20, passo comum da base. Ricardo decidiu o controle, então na ativação do stage 20 ele sai das tarefas: o passo não vai a Skipped, porque decisão de controle nunca cobre outro passo (RN-AP-57), e outro membro da controladoria decide; se Ricardo tentasse decidir, APPROVAL_CONTROL_STEP_SAME_PERSON_FORBIDDEN · 403; se fosse o único membro, o passo pararia com alerta (RN-AP-59). Com conta de destino de despesa, a complementar não casa: analista, depois a controladoria. O critério de conta contábil lê a linha de destino, que é a que recebe verba — é para isso que ele existe no remanejamento.
Uma requisição do começo ao fim
A requisição de Ana, do primeiro exemplo, acompanhada momento a momento — com o que o motor grava e o que a Notificação envia.
| # | Momento | O que o motor faz | Trilha e aviso |
|---|---|---|---|
| 1 | Ana submete | A Requisição chama POST /approvals no processo REQUISITION. O casador roda uma vez: base de R$ 50–500 mil (versão 3) e a complementar do DPO (versão 1). A cadeia é congelada com valor, taxa e rateio no snapshot; o nível do diretor nasce Skipped. | Evento de submissão com as políticas e versões. Nada é enviado ainda. |
| 2 | Nível 1 ativa | O resolvedor lê os Owners vigentes e as delegações de hoje: Carla para a fatia de TI, Diego para a de Marketing. Vencimento calculado em horas úteis. | nexio.approval-assignment.assigned para Carla e para Diego. |
| 3 | Carla aprova; Diego não decide em 48 h úteis | A fatia de TI fecha. O SLA de Diego vence; o processo tem escalationBehavior = Notify. | nexio.approval-assignment.reminded para Diego e para Ana. |
| 4 | Diego devolve para ajuste | Motivo ApprovalReturn e comentário ("falta a cotação anexa"). A instância vai a Returned, viva; os passos da rodada viram Obsolete, inclusive o que Carla aprovou. | nexio.approval-request.returned para Ana. |
| 5 | Ana anexa e reenvia | Rodada 2 na mesma ApprovalRequest. O casador roda de novo: o valor não subiu, anexo não é critério e a cadeia é a mesma — então o passo de Carla herda a aprovação da rodada 1, e só Diego recebe tarefa nova (RN-AP-21). | assigned para Diego. |
| 6 | Diego aprova | Os dois passos de fatia do nível 1 estão fechados — um herdado, um decidido; o passo do documento ativa. | assigned para o DPO. |
| 7 | O DPO aprova com ressalva | "Mediante DPA assinado". A instância termina Approved; a ressalva fica visível no documento e no recebimento. A decisão invoca o contrato de desfecho, e na mesma transação a Requisição transiciona e o Commitment é gravado com as fatias e os valores congelados (RN-AP-38); o evento sai depois do commit. | nexio.approval-request.decided (outcome Approved) para Ana. |
| 8 | Semana seguinte, Ana edita a descrição e o prazo | Reavalia (RN-AP-20). Valor igual, nenhum atributo de critério mudou — o hash do retrato é o mesmo, e o prazo novo cai em mês com o mesmo estado de orçamento — e a cadeia é igual: é ajuste (RN-AP-71). A instância continua Approved, sem rodada nova, e o desfecho grava na mesma transação só a diferença: o Commitment sai do mês do prazo antigo e vai ao do novo (DateChange, Orçamento RN-O45). | approval-request.revaluated com o que mudou. Nenhuma tarefa nova. |
| 9 | Ana edita para R$ 130 mil | Reavalia: o valor subiu, e a cadeia ganhou o nível do diretor. Rodada nova do zero — as decisões de Carla, Diego e do DPO vão a Obsolete, e a cadeia inteira roda de novo, agora com o diretor (RN-AP-21). | assigned para Carla e Diego; o diretor e o DPO vêm nos níveis seguintes. |
Padrões que se repetem
| Padrão | Como se monta | Onde aparece |
|---|---|---|
| Quatro olhos | Um nível Quorum(2). Duas pessoas fecham; um "não" encerra. Na alteração bancária, só PermissionHolder(approve:supplier-banks) (RN-AP-61); nos demais, PermissionHolder ou Role | Alteração bancária, cadastro de fornecedor pessoa física, item crítico |
| Comitê | ApprovalGroup com All ou Quorum(n); os membros mudam sem editar a política | Pedido acima da faixa, contrato, exercício orçamentário |
| Alçada progressiva | CostCenterOwnerUpToAmount · Slice; o limite mora no vínculo, não na política | Requisição e pedido — o padrão de quem vem do Protheus |
| Diretor só acima de X | activationMinValue no nível, dentro da mesma política | Qualquer processo com valor |
| Controle extra por condição | Uma Supplemental por condição — flag, categoria, fornecedor, UF, conta — cada uma com o nível que acrescenta | DPO, Segurança, fiscal, compliance |
| Exceção por subárvore | Base amarrada no nó filho com prioridade menor que a do pai | /TI/INFRA/ com alçada própria dentro de /TI/ |
| Verba estourada exige o Controller | Supplemental com critério estado do orçamento = Exceeded; é o que RequireApproval significa | Requisição, pedido, remanejamento |
| Interestadual passa pelo fiscal | Supplemental com critério UF de entrega e nível Role(Fiscal) · Line — só as linhas de fora da UF | Requisição e pedido com entrega em outra UF |
| Fornecedor ainda não homologado | Supplemental com critério fornecedor em estado Prospect | Cotação e pedido |
| Uma política para o grupo, uma por empresa | Base sem critério de empresa mais Supplemental com critério empresa ou estabelecimento em lista | Multiempresa, filial em regime especial, obra |
| Férias com teto, por processo | uma ApprovalDelegation por processo, cada uma com delegado, teto e fim; acima do teto a tarefa fica com o titular, e o vencimento faz o que o processo mandar | Qualquer aprovador |
| Compra pequena sem ninguém | Todos os níveis da política com activationMinValue acima do valor: a instância termina Approved com o evento approval-request.approved-by-policy e o desfecho grava o Commitment na transação do envio (RN-AP-71); o /validate avisa que a política tem faixa sem decisor humano | Requisição de baixo valor com fornecedor homologado — declarado na política, visível no simulador; nunca em processo com allowPolicyApproval falso (RN-AP-54) |
O que não dá para montar — e o que usar no lugar
- "Dois níveis ao mesmo tempo." Níveis são sequenciais. O paralelo é dentro do nível:
AllouQuorum(n)sobre um grupo,Slicepara fatias diferentes. Uma decisão de cada área no mesmo nível é v2. - "Deixar o requisitante aprovar quando não há mais ninguém." Não existe chave. O tenant dá a permissão a uma segunda pessoa ou define o aprovador de reserva; ausência de aprovador legítima é declarada na política, com
activationMinValuehoje e o nívelAutona fase 2. - "Aprovar sozinho depois de X dias." SLA vencido lembra, chama o gestor ou cria tarefa no nível acima. Nunca aprova.
- "Aprovar metade das linhas." A decisão é do documento inteiro. Quem quer separar, separa antes de submeter.
- "Comitê onde um voto contrário não derruba."
Vote(n)— fecha emn, encerra só quandonfica inalcançável —, restrito aBUDGET,BUDGET_CHANGEeCONTRACT. Nunca em processo de controle antifraude. - "Condição A ou B numa política só." Critérios são conjunção. Duas complementares, uma por condição, produzem o mesmo nível — e o motor deduplica.
- "Aprovar pelo e-mail, sem entrar."
ApprovalTokenestá modelado e desligado por parâmetro.
Configuração
Parametrizações
O que o cliente ajusta sem código, e em que nível cada coisa vive. Parâmetro ausente herda o nível acima.
| Parâmetro | Tipo | Padrão | Efeito |
|---|---|---|---|
| workWeek | DayOfWeek[] | seg · ter · qua · qui · sex | Os dias que contam como úteis. Tirar o fim de semana resolve cinco sétimos do erro de contagem. |
| workdayStart · workdayEnd | time · time | 09:00 · 18:00 | A janela diária em que o relógio corre. Não existe meio expediente: o dia é útil inteiro ou não é útil. |
| nonWorkingDates | date[] | feriados nacionais, semeados | As datas que o relógio pula. Semeada pelo produto e editável pelo tenant, que acrescenta feriado municipal e recesso. |
| timeZone | string (IANA) | America/Sao_Paulo | O fuso em que expediente, dias não úteis e vencimento de SLA são calculados. Um por tenant: o Brasil tem quatro fusos, e sem ele "18:00" não tem instante (RN-AP-13) |
| allocationMaterialityPercent | numeric(9,6) | 5,00 | Fatia abaixo disso não gera passo próprio. Um valor por tenant, e o dono é este documento — o Centro de Custo cita, não redeclara (RN-AP-15 · RN-CC-24). |
| allocationMaterialityAmount | numeric(19,2) | 1.000,00 | Vale o menor entre os dois: o valor absoluto é o teto do limiar, para que num documento grande uma fatia de centenas de milhares nunca seja dispensada (RN-AP-15). Não varia por processo: limiares diferentes fariam o pedido reabrir cadeia para fatias que a requisição já dispensou. |
| maxDelegationDays | int | 90 | Duração máxima de uma delegação. validTo é obrigatório e não passa disso; renovar é criar outra, com trilha. |
Estes sete parâmetros são a entidade ApprovalSettings — uma linha por tenant, criada no provisionamento com os padrões acima, com rotas e permissão próprias. TenantSettings não existe na plataforma, e Company e Establishment não eram o lugar: são parâmetros do tenant, não da empresa. Padrão do CurrencySettings — entidade nomeada por assunto, nunca um JSON genérico nem tabela-gaveta, que é o que a spec de Empresa recusou por escrito. costCenterMaxDepth saiu desta tabela: nunca foi parâmetro de aprovação — quem valida profundidade de árvore é o Centro de Custo, e é lá que ele mora, como gancho declarado.
| Campo | Tipo | Valor | Efeito |
|---|---|---|---|
| code · hasValue · subjectHasCompany | string · bool · bool | por processo | O processo, se tem valor avaliado e se o assunto tem empresa (RN-AP-54) |
| allowPolicyApproval · allowReturn · minDistinctApprovers · admittedApproverTypes · admittedScopeModes | bool · bool · int · enum[] · enum[] | tabela Núcleo de controle | O núcleo que configuração não desliga (RN-AP-54) |
| subjectFlags | string[] | por processo | As flags que o assunto expõe ao critério "flags do assunto"; as que acionam complementar de controle semeada estão na tabela de §Processos (RN-AP-61, RN-AP-70) |
| seededDefaultPolicy | bool | true nos processos sem valor | A política de reserva vem publicada pelo produto, com um nível PermissionHolder. O tenant edita; nunca fica sem |
| rejectReasonUsage · returnReasonUsage | ReasonCodeUsage · ReasonCodeUsage? | ver efeito | O uso de motivo que a rejeição e a devolução gravam, como o Motivo os declara: ITEM → ItemReject; SUPPLIER_REGISTRATION e SUPPLIER → SupplierReject; BANK_ACCOUNT_CHANGE → BankAccountChangeReject; BUDGET → BudgetReject; BUDGET_CHANGE → BudgetChangeReject; os demais → ApprovalReject. Devolução: ApprovalReturn em todos, nulo no BANK_ACCOUNT_CHANGE, que não devolve |
| Parâmetro | Tipo | Padrão | Efeito |
|---|---|---|---|
| fallbackApproverType · fallbackApproverRef | enum? · Guid? | — | Último recurso quando toda resolução esvazia: User ou ApprovalGroup, resolvido pessoa a pessoa (RN-AP-59) e sempre com marca ByFallback (RN-AP-58). Obrigatório para ativar processo com valor; opcional nos sem valor com política semeada; proibido no BANK_ACCOUNT_CHANGE (RN-AP-03) |
| authorityExceededBehavior | enum | RequireControllerApproval | RequireControllerApproval · Refuse. O que acontece quando a alçada esgota na raiz ou o passo cai no reserva em processo com valor (RN-AP-58) |
| deduplicateConsecutive | bool | true | Quem agiu no passo anterior sai do seguinte; passo de uma decisão vira Skipped, passo de mais de uma nunca é pulado (RN-AP-57). false e travado no BANK_ACCOUNT_CHANGE |
| defaultSlaHours | int | 48 | SLA herdado por política e nível |
| escalationBehavior | enum | Notify | None · Notify · Escalate. Vive aqui e em nenhum outro nível — política e nível herdam o do processo |
| allowEscalation | bool | true | false e travado no BANK_ACCOUNT_CHANGE. Desligado, o vencimento só lembra — e política com Escalate não publica. |
| delegable | bool | true | false e travado no BANK_ACCOUNT_CHANGE. Nenhuma delegação alcança um processo não delegável, nem a de escopo nulo. |
| requireCommentOnApprove | bool | false | Rejeição e devolução já exigem sempre |
| allowReassign | bool | true | Permite passar a tarefa a outro elegível, uma vez. false e travado no BANK_ACCOUNT_CHANGE |
| allowBatchApproval | bool | true | Aprovar várias tarefas numa ação — sempre um evento por tarefa |
| emailApproval | bool | false | Liga o ApprovalToken. Fase 2 |
Parâmetros críticos. allocationMaterialityPercent e allocationMaterialityAmount (do tenant); fallbackApproverType · fallbackApproverRef, authorityExceededBehavior, deduplicateConsecutive e delegable (por processo); os membros de grupo usado como reserva ou por nível de controle em política ativa ou na cadeia congelada de instância viva; e os membros do papel Controladoria e de papel usado por nível de controle. Não mudam pelo PUT: mudam por proposta que outra pessoa efetiva — nem quem propôs, nem quem a mudança inclui, prorroga ou põe como reserva —, com diff, evento e aviso à controladoria (RN-AP-69).
| Parâmetro | Nível | Efeito |
|---|---|---|
| priority · isDefault · kind | política | Como a política concorre ou empilha |
| criteria | política | Quinze critérios; ausência casa tudo |
| validFrom · validTo | política | Vigência da regra |
| fallbackApproverType · fallbackApproverRef | política | Sobrepõe o do processo, com as mesmas regras |
| approverType · target | nível | Estratégia de resolução |
| mode · quorum | nível | Any · All · Quorum(n) |
| scopeMode | nível | Document, Line ou Slice (RN-AP-46). |
| activationMinValue | nível | Abaixo disso o nível não entra |
| maxApprovalHours | nível | SLA próprio; nulo herda |
| escalationTargetType · escalationTargetRef | nível | Para quem a escalada vai. Obrigatório quando o processo escala e o nível não caminha árvore; proibido quando caminha — lá o alvo é o nó pai. |
| label | nível | O nome que o aprovador vê |
Camada API
Endpoints
| Rota | Permissão | O que faz |
|---|---|---|
| GET /approval-processes | read:approval-processes | Processos e seus parâmetros |
| PUT /approval-processes/{id} | update:approval-processes | Edita a ApprovalProcessSettings do processo — reserva, dedupe, SLA, escalonamento, alçada estourada, delegação, reatribuição, lote. O ApprovalProcess, o núcleo de controle e as travas do BANK_ACCOUNT_CHANGE não são editáveis: corpo que tente mudá-los é 409 (RN-AP-54). Parâmetro crítico também não muda por aqui (409, RN-AP-69) |
| POST /approval-processes/{id}/changes · …/{changeId}/apply · …/{changeId}/discard | update:approval-processes (propõe e efetiva) · delete:approval-processes (descarta) | Propõe, efetiva ou descarta mudança de parâmetro crítico do processo. Quem propõe não efetiva, nem quem a mudança põe como reserva (403); proposta velha é 409 (RN-AP-69) |
| GET /approval-settings | read:approval-settings | A linha do tenant: materialidade, teto de delegação, expediente e dias não úteis |
| PUT /approval-settings | update:approval-settings | Devolve o recurso, pela convenção de sucesso. Não afeta instância viva: o que a cadeia usou está congelado desde a submissão (RN-AP-06). A materialidade não muda por aqui (409, RN-AP-69) |
| POST /approval-settings/changes · …/{changeId}/apply · …/{changeId}/discard | update:approval-settings (propõe e efetiva) · delete:approval-settings (descarta) | Propõe, efetiva ou descarta mudança da materialidade. Quem propõe não efetiva (403); proposta velha é 409 (RN-AP-69) |
| GET /approval-policies | read:approval-policies | Filtra por processo, tipo, vigência e nó |
| POST /approval-policies | create:approval-policies | Cria em rascunho e grava o autor. Sobreposição entre bases é 409 |
| POST /approval-policies/{id}/publish | update:approval-policies (publica) · delete:approval-policies (publica versão com vigência encerrada, isto é, desativa) | Publica versão nova e aposenta a anterior. Quem publica não é quem criou nem ninguém que tenha editado o rascunho — A cria, B edita, C edita: nem A, nem B, nem C publica (403, RN-AP-55); verifica o núcleo de controle do processo (RN-AP-54), grava o diff e avisa a controladoria. Desativar política é publicar versão com vigência encerrada, pela mesma regra |
| GET /approval-policies/{id}/validate | read:approval-policies | Inconsistências: nó sem responsável, grupo vazio, papel sem membro, nível sem alvo, empresa alcançada pela política sem gente elegível bastante no alvo para o modo (RN-AP-40), tipo de aprovador ou escopo que o processo não admite, faixa de valor ou activationMinValue em processo sem valor, base sem nível que garanta minDistinctApprovers pessoas, cadeia que termina sem pessoa onde allowPolicyApproval é falso (RN-AP-54), papel que concede ao mesmo tempo a permissão do nível e a de propor o assunto — este como aviso (RN-AP-60), sobreposição com outra base de mesma prioridade, raiz de centro de custo sem Owner com hasUnlimitedAuthority em política com nível CostCenterOwnerUpToAmount, limite preenchido sem approvalLimitCurrency, faixa em que todos os níveis ficam abaixo do activationMinValue, nível com escalonamento sem alvo declarado, política que escala em processo que não escala e — em política Supplemental com critério estado do orçamento — os nós de centro de custo em RequireApproval que ficariam sem cobertura se ela for publicada com outro critério ou aposentada (RN-AP-41) |
| GET /approval-groups · POST · PUT | read:approval-groups (GET) · create:approval-groups (POST) · update:approval-groups (PUT) | Comitês e seus membros com vigência. Membro de grupo usado como reserva ou referenciado por nível de controle em política ativa ou na cadeia congelada de instância viva não muda por aqui (409, RN-AP-69) |
| POST /approval-groups/{id}/member-changes · …/{changeId}/apply · …/{changeId}/discard | update:approval-groups (propõe e efetiva) · delete:approval-groups (descarta) | Propõe, efetiva ou descarta mudança de membros de grupo usado como reserva de processo ou de política, ou referenciado por nível de controle em política ativa ou na cadeia congelada de instância viva. Quem propõe não efetiva, nem quem a mudança inclui ou prorroga (403); proposta velha é 409 (RN-AP-69) |
| GET /approval-delegations · POST | read:approval-delegations (GET) · create:approval-delegations (POST) · manage-own:approval-delegations [PERGUNTA AO ROGER: o escopo "própria" não é um dos cinco verbos; como fica a permissão de quem só mexe na própria delegação?] | As duas primeiras valem para qualquer delegação do tenant; a última só para a própria. Delegação com escopo por processo e teto de valor. Delegado com act:approvals e conta ativa; criada por terceiro, avisa o delegante (RN-AP-56) |
| POST /approvals:simulate | read:approval-policies | A cadeia de um documento hipotético, sem criar nada: corpo de POST /approvals + policyOverrides[] + asOf (RN-AP-26) |
| POST /approvals:preview | interna | A prévia da cadeia sobre o rascunho do documento, chamada pelo módulo dono na requisição de quem pode editá-lo |
| Rota | Permissão | O que faz |
|---|---|---|
| POST /approvals | interna | Submete um assunto. Chamada pelo módulo dono do documento, nunca pela tela. Leva a lista de excluídos (RN-AP-60), a versão e o hash do assunto (RN-AP-67), as linhas e as fatias com os atributos que os critérios leem, a conversão da moeda, o valor avaliado ou a marca de indisponível (RN-AP-62) e, quando o processo pede, activationNotBefore ou activationHeld (RN-AP-63). Corpo e resposta: §Contrato de submissão (RN-AP-72); 201 com a instância |
| GET /approvals | audit:approvals | A consulta transversal. Instâncias de qualquer documento do tenant, com filtro por documento (subjectType + subjectId), processo, estado, período, pessoa (requisitante, aprovador de direito ou quem agiu), rodada e marca de exceção — authorityExceeded, resolvedOutOfScope, resolvedEmptyOnActivation, budgetRequirementUnmet. É a rota da pergunta de auditoria que não é sobre um documento conhecido. Paginada e ordenada por submissão; a trilha de cada uma continua em /events |
| GET /approvals/{id} | read:approvals | A instância com passos, tarefas e a cadeia projetada até o fim |
| GET /approvals/reason-summary | audit:approvals | Por que os pedidos voltam. Contagem de rejeições e devoluções por processo × código de motivo × mês, com filtro de período, processo, empresa e ação; lida das colunas do ApprovalEvent, nunca do metadata. É o relatório que o Motivo delega a este módulo |
| POST /approvals/{id}/events/{sequence}/erase-comment | erase:approval-comments | Apaga o texto livre do comentário de um evento, com motivo, por eliminação LGPD sem base legal de guarda (RN-AP-48) |
| GET /approvals/inbox | act:approvals | A fila. Tarefas pendentes do usuário, incluindo as recebidas por delegação, com filtro por processo, valor e vencimento |
| POST /approvals/{id}/assignments/{aid}/approve | act:approvals | Aprova. Aceita ?withProviso= para ressalva |
| POST /approvals/{id}/assignments/{aid}/reject | act:approvals | Rejeita. Comentário obrigatório |
| POST /approvals/{id}/assignments/{aid}/return | act:approvals | Devolve para ajuste. Comentário obrigatório. Recusado em processo com allowReturn falso — o BANK_ACCOUNT_CHANGE (RN-AP-61) |
| POST /approvals/{id}/assignments/{aid}/request-info | act:approvals | Pergunta ao requisitante. Pausa o SLA e avisa quem tem que responder (nexio.approval-assignment.info-requested) |
| POST /approvals/{id}/answer-info | read:approvals · só o requisitante | A resposta à pergunta aberta. Retoma o SLA do passo, entra na trilha e avisa quem perguntou. Não decide nada e não muda a cadeia |
| POST /approvals/{id}/assignments/{aid}/decline | act:approvals | O delegado recusa a tarefa recebida. Ela volta ao titular — que recebe nexio.approval-assignment.assigned —, o evento registra a recusa com o id da delegação, e o documento continua sem decisão. Não existe para tarefa própria: quem é o aprovador de direito decide ou reatribui |
| POST /approvals/{id}/assignments/{aid}/reassign | reassign:approvals | Passa a outro elegível, uma vez |
| POST /approvals/batch-approve | act:approvals | N tarefas numa chamada, N eventos |
| POST /approvals/{id}/cancel | interna | Cancelamento vindo do documento. Libera empenho. É também a saída da contestação do fornecedor com a alteração bancária em aprovação (RN-AP-61) |
| POST /approvals/{id}/revaluate | interna | Disparado pela alteração do documento, com o retrato novo e o último editor. Com (a), (b) e (c) da RN-AP-21 é ajuste: a instância continua Approved e o desfecho aplica a diferença; sem elas, reabre com rodada do zero (RN-AP-71). Recusado no BANK_ACCOUNT_CHANGE: APPROVAL_TRANSITION_NOT_ALLOWED_CONFLICT · 409 (RN-AP-61) |
| POST /approvals/{id}/resubmit | interna | Reenvio do documento devolvido: rodada nova, com o retrato novo, o último editor e o documento reconvertido com a data do reenvio (Moeda e Câmbio, RN-CUR-22); só a partir de Returned |
| POST /approvals/{id}/activation-window | interna | O módulo dono fixa ou adia activationNotBefore enquanto nenhum passo ativou; nunca antecipa (RN-AP-63) |
| POST /approvals/{id}/steps/{stepId}/resolve | update:approval-processes | Refaz a resolução de um passo parado por falta de pessoas elegíveis, depois de o cadastro ser corrigido. Não decide nada e não reduz o modo (RN-AP-59) |
| GET /approvals/{id}/events | read:approvals | A trilha completa, ordenada |
Contrato
Contrato de submissão
O corpo de POST /approvals é o assunto inteiro, como o módulo dono o vê na hora do ato: linhas, fatias, a conversão da moeda, as flags, a empresa e o estabelecimento, os excluídos, a versão e o hash. O motor não lê tabela de módulo nenhum para casar critério — lê este corpo —, e por isso ele é fechado: campo que o processo exige e não veio, ou que o processo não admite e veio, é recusado, nunca adivinhado (RN-AP-72). A tabela é completa para a REQUISITION; os campos próprios do PURCHASE_ORDER — a instância de origem da RN-AP-68, a liquidação e o valor cancelado por fatia — nascem com a spec do Pedido, como acréscimo a este corpo.
O que o motor calcula não vem no corpo. O path do centro de custo, da categoria e da conta contábil, que ele resolve pelo contrato de cada dono e congela no retrato; o valor avaliado dos processos com linhas, que é a soma das linhas; o limiar de materialidade; o sinal de orçamento, que ele pede ao Orçamento com a data de necessidade da linha e o estágio do processo — Commitment na requisição, Obligation no pedido; e a cadeia.
| Ato | O que vai no corpo | Resposta |
|---|---|---|
| POST /approvals | O corpo inteiro. | 201 com a instância (tabela Resposta) |
| POST /approvals:simulate | O corpo inteiro, mais policyOverrides[] e asOf (RN-AP-26). Documento hipotético: subjectId, subjectVersion e subjectHash são opcionais, e activationNotBefore e activationHeld são ignorados. | 200 com a cadeia; nada é gravado |
| POST /approvals:preview | O corpo inteiro, montado sobre o rascunho. | 200 com a cadeia; nada é gravado |
| POST /approvals/{id}/resubmit | O corpo sem processCode, subjectType, subjectId, activationNotBefore e activationHeld, que vêm da instância. Rodada nova: a conversão já vem refeita com a data do reenvio (Moeda e Câmbio, RN-CUR-22). | 200 com a instância |
| POST /approvals/{id}/revaluate | O mesmo recorte do reenvio, pela taxa gravada: conversion é a da instância, e o functionalAmount de cada fatia é o valor novo convertido por ela. Se o motor decide reabrir, quem reconverte é o dono, no desfecho da reabertura (RN-AP-72). | 200 com a instância — Approved no ajuste, InApproval na reabertura |
| Campo | Tipo | Regra | |
|---|---|---|---|
| processCode | string | obrig | O code do ApprovalProcess (§Processos aprováveis). Decide o que mais é exigido: com valor ou sem, com empresa ou sem, com linhas e fatias ou sem, e quais flags valem. |
| subjectType · subjectId | string · Guid | obrig | O documento: o nome da raiz no módulo dono (Requisition, PurchaseOrder, Supplier…) e o id dela. Com instância InApproval ou Returned do mesmo assunto no mesmo processo — ou Approved, fora do caminho declarado da correção de identidade — é APPROVAL_REQUEST_ALREADY_EXISTS · 409: o aprovado muda por revaluate, e o devolvido volta por resubmit (RN-AP-72). |
| subjectVersion · subjectHash | int · string | obrig | A versão e o hash do conteúdo aprovável, calculados pelo dono (RN-AP-67). Ausentes: APPROVAL_SUBJECT_HASH_REQUIRED_UNPROCESSABLE · 422. |
| requesterUserId | Guid | obrig | Quem pede. É o requisitante que a instância avisa, que responde ao pedido de informação e que a GET /approvals filtra; precisa estar em excludedUsers com Requester, senão APPROVAL_EXCLUDED_USERS_UNPROCESSABLE · 422 (RN-AP-60). |
| excludedUsers | { userId, reason }[] | obrig | Quem não decide nesta instância, com a razão — os sete valores da RN-AP-60. Na conversão, os dois autores que a Moeda devolve no ConversionResult — capturedBy e overriddenBy — entram aqui como RateOverrider, e não em campo próprio (RN-AP-70). No reenvio e na reavaliação, só acrescenta. |
| companyId · establishmentId | Guid · Guid | cond | Obrigatórios quando o processo tem subjectHasCompany; recusados nos demais. establishmentId é quem recebe — o que define a empresa e a política de aprovação (Empresa) —, e companyId é a empresa derivada dele e congelada no documento; empresa que não é a do estabelecimento é recusada. requestingEstablishmentId não vem: nenhum critério o lê. |
| currency | char(3) | cond | A moeda do documento, ISO 4217. Obrigatória em processo com valor. |
| conversion | objeto? | cond | O ConversionResult congelado no documento (Moeda e Câmbio): rate, quoteUnit, rateType, rateDate, rateAgeDays, conversionDate, exchangeRateId? e isOverride. Obrigatório quando currency não é a moeda funcional da empresa; ausente quando é — aí o valor funcional é o próprio valor. É a taxa que a alçada usa (RN-AP-52) e que o ApprovalEvent grava como prova. Uma conversão por documento: o valor funcional é a soma das fatias convertidas, nunca o total convertido. |
| valueUnavailable | bool | cond | Só em processo com valor. Verdadeiro quando o dono não consegue calcular o valor — nunca zero no lugar dele (RN-AP-62). |
| evaluatedAmount | decimal? | cond | O valor avaliado, na moeda funcional, só em processo com valor e sem linhas — CONTRACT, BUDGET, BUDGET_CHANGE. Nos processos com linhas não vem: é a soma dos functionalAmount das linhas, e quem soma é o motor. Nulo com valueUnavailable. |
| subjectFlags | string[] | opc | As flags do assunto, calculadas pelo dono e nunca declaradas por quem pede, dentre as que o processo declara em ApprovalProcess.subjectFlags — nos seis processos com valor, exchangeRateOverridden, derivada do ConversionResult (RN-AP-70). Flag que o processo não declara é recusada. |
| activationNotBefore · activationHeld | timestamptz? · bool | cond | Só onde o processo adia a ativação — na v1, o BANK_ACCOUNT_CHANGE (RN-AP-63). |
| lines | Line[] | cond | 1..N nos processos com linhas — REQUISITION, QUOTATION e PURCHASE_ORDER; recusadas nos demais. |
| Campo | Tipo | Regra | |
|---|---|---|---|
| lineId | Guid | obrig | O id da linha no dono (RequisitionItem, PurchaseOrderItem…). Único no corpo. É o scopeRef do passo Line (RN-AP-46). |
| amount | decimal | obrig | Valor da linha na moeda do documento. |
| functionalAmount | decimal | obrig | Valor da linha na moeda funcional. Com fatias, é exatamente a soma dos functionalAmount delas; sem fatias, a conversão da linha pela taxa do documento. |
| itemId | Guid? | opc | Nulo = linha não catalogada, e aí o critério linha não catalogada casa e os que leem tipo e flags do item não casam. |
| categoryNodeId | Guid | obrig | A categoria da linha — do item, ou a informada na linha não catalogada (Item). |
| itemTypeId | Guid? | cond | O tipo do item. Obrigatório com itemId; nulo sem ele. |
| predominantUse | enum | obrig | ProductionInput · Resale · ConsumableUse · FixedAsset · PersonalBenefit — o da linha: padrão do item, sobrescrevível nela (Item). É o que o critério destinação lê. |
| itemFlags | string[] | cond | As flags do item — regulated, personalData e critical —, os mesmos valores que o processo ITEM declara em subjectFlags, calculadas pelo dono a partir do item na hora do ato (Cadastro de Item, RN-ITM-43). Vazio sem itemId. São da linha: "software com dado pessoal" casa numa linha só (RN-AP-66). A marca de dado pessoal declarada na própria linha, para o serviço genérico cujo item não a tem, é da spec da Requisition; quando existir, soma-se ao personalData do item neste mesmo campo. |
| supplierId | Guid? | opc | O fornecedor da linha, quando o documento o tem. |
| supplierConditions | string[] | cond | O estado do fornecedor que o critério fornecedor lê, calculado pelo dono na hora do ato: Prospect (relacionamento), Sanctioned (alerta de sanção aberto, RN-FOR-37) e DocumentExpired (documento vencido, RN-FOR-07) — regras do Fornecedor, que esta página nomeia e não redefine. Vazio sem supplierId. |
| deliveryStateCode · deliveryIsInterstate | char(2) · bool | cond | Da DeliveryAddressSnapshot da linha, já congelados (Local de Entrega) — nunca a comparação refeita na hora. Obrigatórios quando o documento tem entrega. |
| neededOn | date | cond | A data de necessidade da linha. Obrigatória com fatias: é o neededOn que o motor manda ao Orçamento em POST /evaluate:budget-state, para cada fatia da linha. |
| allocations | Allocation[] | cond | 1..N nos processos com fatia — REQUISITION e PURCHASE_ORDER; recusadas nos demais. Linha sem rateio explícito manda a única fatia de 100% (Centro de Custo). |
| Campo | Tipo | Regra | |
|---|---|---|---|
| allocationId | Guid | obrig | A identidade estável da fatia (RN-CC-33). Única no corpo. É o scopeRef do passo Slice e a chave do sinal de orçamento no retrato. |
| costCenterId | Guid | obrig | Nó com allowsPosting, ativo, vigente e da empresa do documento — ou corporativo (RN-CC-34); a regra é do Centro de Custo, e o dono a confere antes de submeter. |
| glAccountId | Guid | obrig | Obrigatória na submissão (RN-CC-29): fatia sem conta é recusada pelo dono, com ALLOCATION_GL_ACCOUNT_REQUIRED_UNPROCESSABLE, antes de chamar o motor. |
| amount | decimal | obrig | Valor da fatia na moeda do documento, com o resíduo da RN-CC-23 já aplicado. A soma das fatias é exatamente o amount da linha. |
| functionalAmount | decimal | obrig | Valor da fatia na moeda funcional, convertido pela taxa de conversion. É o número que a faixa de valor, a alçada, a materialidade e o Orçamento leem (RN-AP-15, RN-AP-52). |
| Campo | Tipo | Regra | |
|---|---|---|---|
| id · version | Guid · int | gerado | A instância e o token de concorrência para o próximo ato sobre ela (RN-AP-44). O dono guarda o id no documento. |
| status | enum | gerado | InApproval — ou Approved, só na aprovação pela política, com o desfecho Applied já gravado na mesma transação (RN-AP-32, RN-AP-71). O Refused da aprovação pela política não é resposta: é o código do dono, e a instância não nasce. |
| roundNumber | int | gerado | 1 na submissão (RN-AP-43). |
| evaluatedAmount · functionalCurrency | decimal? · char(3) | gerado | O valor avaliado que casou a faixa, na moeda funcional; nulo com valueUnavailable. |
| materialityThreshold · absorbedAllocationIds | decimal? · Guid[] | gerado | O limiar efetivo congelado e as fatias que ele absorveu para roteamento — que continuam consumidas no orçamento (RN-AP-15). |
| budgetSignals | { allocationId, signal, signalFrom }[] | gerado | O sinal de orçamento de cada fatia com que a rodada foi montada (RN-AP-21, RN-AP-53) — o mesmo semáforo que a tela do documento mostra. |
| criteriaSnapshotHash | string | gerado | O hash do retrato (RN-AP-72). |
| steps | Step[] | gerado | A cadeia congelada: stepId, stage, policyId, policyVersion, levelOrder, scope, scopeRef, scopeAmount, mode, quorum, status e as marcas authorityExceeded, resolvedOutOfScope e awaitingEligibleApprovers. Pessoas não vêm aqui: são resolvidas na ativação, e a tarefa de cada uma está em GET /approvals/{id}. |
O que a submissão pode devolver, além de 201: APPROVAL_SUBMISSION_CONTRACT_UNPROCESSABLE e APPROVAL_REQUEST_ALREADY_EXISTS (RN-AP-72); APPROVAL_SUBJECT_HASH_REQUIRED_UNPROCESSABLE e APPROVAL_EXCLUDED_USERS_UNPROCESSABLE; APPROVAL_NO_POLICY_UNPROCESSABLE e APPROVAL_POLICY_AMBIGUOUS_UNPROCESSABLE; APPROVAL_NO_APPROVER_UNPROCESSABLE, APPROVAL_AUTHORITY_EXCEEDED_UNPROCESSABLE, APPROVAL_VALUE_UNAVAILABLE_UNPROCESSABLE e APPROVAL_POLICY_APPROVAL_NOT_ALLOWED_UNPROCESSABLE; APPROVAL_LIMIT_CURRENCY_NOT_CONVERTIBLE_UNPROCESSABLE, repassado da Moeda; BUDGET_EXCEEDED_UNPROCESSABLE e BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE, repassados do Orçamento; e, na aprovação pela política, o código do Refused do dono (RN-AP-71). Todos com o código do dono do fato, sem reembrulho.
Contrato
Contrato de erros
Mesma convenção do resto do produto: o status vem do sufixo do código, nunca da mensagem.
| Código | HTTP | Quando |
|---|---|---|
| APPROVAL_POLICY_OVERLAP_CONFLICT | 409 | Duas políticas base com a mesma prioridade e critérios sobrepostos. Verificado na aplicação ao publicar, na mesma transação; guarda parcial no banco quando ele oferecer |
| APPROVAL_POLICY_IN_USE_CONFLICT | 409 | Desativar política com instância viva. A mensagem carrega a contagem |
| APPROVAL_GROUP_EMPTY_CONFLICT | 409 | Remover o último membro de grupo usado por política ativa ou pela cadeia congelada de instância em InApproval (RN-AP-33) |
| APPROVAL_ASSIGNMENT_ALREADY_DECIDED_CONFLICT | 409 | Duas decisões simultâneas na mesma tarefa. A segunda perde e é informada |
| APPROVAL_STEP_NOT_ACTIVE_CONFLICT | 409 | Decisão sobre passo que já fechou — tela desatualizada |
| APPROVAL_TRANSITION_NOT_ALLOWED_CONFLICT | 409 | Ato que a tabela de transições não prevê para o estado atual da instância — reenviar o que não está Returned, cancelar o que já é final, reabrir o que não está Approved (RN-AP-43), e reabrir (revaluate) qualquer instância do BANK_ACCOUNT_CHANGE (RN-AP-61) |
| APPROVAL_SETTINGS_TIME_ZONE_UNPROCESSABLE | 422 | timeZone que não é identificador IANA válido |
| APPROVAL_COMMENT_ALREADY_ERASED_CONFLICT | 409 | Apagar comentário já apagado, ou de evento sem comentário (RN-AP-48) |
| APPROVAL_REQUEST_VERSION_CONFLICT | 409 | expectedVersion diferente do version corrente da instância: outra decisão da mesma instância commitou primeiro. A segunda relê e decide de novo — a resposta traz o estado atual, não um erro genérico (RN-AP-44) |
| APPROVAL_NO_APPROVER_UNPROCESSABLE | 422 | Na submissão, um passo resolveu vazio depois de todo o encadeamento — inclusive Quorum(n) com menos de n pessoas elegíveis (RN-AP-59). A mensagem nomeia o passo. Nunca no BANK_ACCOUNT_CHANGE, que fica parado com alerta |
| APPROVAL_SELF_APPROVAL_UNPROCESSABLE | 422 | Requisitante ou outro excluído da instância tentando decidi-la. Defesa em profundidade: a RN-AP-08 e a RN-AP-60 já o removeram de toda tarefa; este erro pega reatribuição, delegação, escalonamento ou token que o traga de volta |
| APPROVAL_NO_POLICY_UNPROCESSABLE | 422 | Nenhuma política base casou e o processo não tem política de reserva |
| APPROVAL_POLICY_AMBIGUOUS_UNPROCESSABLE | 422 | Duas políticas base de mesma prioridade casaram o mesmo documento na submissão. A mensagem nomeia as duas. Nunca desempate silencioso |
| BUDGET_EXCEEDED_UNPROCESSABLE | 422 | Repassado do orçamento sem reembrulho, no mesmo padrão dos erros do GrydFiles. Política Block resolvida no nó e valor acima de disponível mais tolerância; traz available e wouldExceedBy por fatia. |
| BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE | 422 | Fatia lançada contra dimensão orçamentária que não existe no plano — sinal Uncovered. Também repassado do orçamento sem reembrulho, e é outro código: BUDGET_EXCEEDED_UNPROCESSABLE diz "estourou o disponível" e admite tolerância e política; este diz "não há verba cadastrada para esta conta neste centro de custo" e não admite nenhuma das duas. A mensagem nomeia centro de custo, conta e período, e sugere a linha coringa (RN-AP-53) |
| APPROVAL_DELEGATION_CHAIN_UNPROCESSABLE | 422 | Delegar para quem já delegou — cadeia não encadeia |
| APPROVAL_ESCALATION_TARGET_REQUIRED_UNPROCESSABLE | 422 | Publicar política com escalonamento ligado e nível que não caminha árvore sem escalationTarget |
| APPROVAL_ESCALATION_NOT_ALLOWED_UNPROCESSABLE | 422 | Escalonamento em processo com allowEscalation = false — o antifraude não muda de decisor por decurso |
| APPROVAL_DELEGATION_OVERLAP_CONFLICT | 409 | Duas delegações vigentes do mesmo delegante para o mesmo processo, ou uma de escopo nulo cruzando qualquer outra. A mensagem nomeia a que já existe |
| APPROVAL_DELEGATION_PERIOD_UNPROCESSABLE | 422 | Delegação sem validTo ou acima de maxDelegationDays |
| APPROVAL_SETTINGS_WORK_WEEK_EMPTY_UNPROCESSABLE | 422 | Semana útil sem nenhum dia — o SLA nunca venceria, e nada pode ficar pendente para sempre |
| APPROVAL_SETTINGS_WORKDAY_WINDOW_UNPROCESSABLE | 422 | workdayStart maior ou igual a workdayEnd |
| APPROVAL_SETTINGS_MATERIALITY_UNPROCESSABLE | 422 | Percentual fora de 0–100 ou valor negativo. Zero nos dois é válido e significa "nenhuma fatia é dispensada" |
| APPROVAL_LEVEL_TARGET_OUT_OF_COMPANY_SCOPE_UNPROCESSABLE | 422 | Publicar política com nível cujo alvo não tem, em alguma empresa que a política alcança, pessoas elegíveis bastantes para o modo; ou com reserva que não alcança todas as empresas ativas do tenant (RN-AP-40) |
| APPROVAL_DELEGATION_OUT_OF_COMPANY_SCOPE_UNPROCESSABLE | 422 | Delegado cujo conjunto de empresas não contém o do delegante — a tarefa chegaria a quem toma 404 ao abri-la (RN-AP-40) |
| APPROVAL_DELEGATION_NOT_ALLOWED_UNPROCESSABLE | 422 | Delegação que alcançaria processo com delegable = false |
| APPROVAL_REASSIGN_TARGET_UNPROCESSABLE | 422 | Destino da reatribuição não é elegível para o passo (RN-AP-59), é excluído da instância ou já tem tarefa ou decisão no mesmo passo (RN-AP-56) |
| APPROVAL_CONTROL_STEP_TARGET_UNPROCESSABLE | 422 | Reatribuição de passo de controle (RN-AP-64) a quem não pertence ao alvo do nível — papel, grupo ou permissão (RN-AP-59 (2)) —, ou a quem já decidiu outro passo da rodada (RN-AP-64) |
| APPROVAL_POLICY_NO_LEVEL_UNPROCESSABLE | 422 | Publicar política sem nível |
| APPROVAL_LEVEL_STAGE_UNPROCESSABLE | 422 | Publicar política com stage ausente, ou que não cresce estritamente com order dentro da política (RN-AP-65) |
| APPROVAL_SIMULATION_OVERRIDE_UNPROCESSABLE | 422 | policyOverrides com versão que não é Draft, ou de outro processo ou tenant (RN-AP-26) |
| APPROVAL_PROCESS_CONTROL_LOCKED_CONFLICT | 409 | PUT /approval-processes/{id} que tenta mudar o núcleo de controle do processo ou uma trava do BANK_ACCOUNT_CHANGE — reserva, dedupe, reatribuição, escalonamento, delegação. Imutável é 409 (RN-AP-54, RN-AP-61) |
| APPROVAL_CONTROL_STEP_SAME_PERSON_FORBIDDEN | 403 | Na decisão: quem decidiu outro passo da rodada — herdado incluído — tenta decidir passo de controle, ou quem decidiu passo de controle tenta decidir outro (RN-AP-64) |
| APPROVAL_RETURN_NOT_ALLOWED_UNPROCESSABLE | 422 | Devolver tarefa de processo com allowReturn falso — o BANK_ACCOUNT_CHANGE, cuja proposta é imutável (RN-AP-61) |
| APPROVAL_POLICY_FALLBACK_NOT_ALLOWED_UNPROCESSABLE | 422 | Publicar política com reserva em processo sem reserva — o BANK_ACCOUNT_CHANGE (RN-AP-61) |
| APPROVAL_POLICY_SEEDED_CONTROL_LOCKED_CONFLICT | 409 | Aposentar ou alterar complementar de controle semeada pelo produto — no BANK_ACCOUNT_CHANGE, a de taxa informada nos processos com valor e a de correção de identidade no SUPPLIER_REGISTRATION (RN-AP-61, RN-AP-70, tabela de §Processos) |
| APPROVAL_POLICY_SELF_PUBLISH_FORBIDDEN | 403 | Publicar versão que a própria pessoa criou ou editou em qualquer momento do rascunho (createdByUserId ou editedByUserIds), em qualquer processo (RN-AP-55) |
| APPROVAL_SETTINGS_CRITICAL_FIELD_CONFLICT | 409 | PUT que altera parâmetro crítico do motor ou membro de grupo usado como reserva ou por nível de controle em política ativa ou na cadeia congelada de instância viva sem passar pela proposta (RN-AP-69) |
| APPROVAL_SETTINGS_CHANGE_SELF_APPLY_FORBIDDEN | 403 | Efetivar mudança crítica que a própria pessoa propôs, ou que a inclui ou prorroga como membro de grupo, ou a põe — sozinha ou pelo grupo — como reserva; em grupo ou papel de nível de controle a comparação é por CPF verificado (RN-AP-69) |
| APPROVAL_SETTINGS_CHANGE_IDENTITY_NOT_VERIFIED_CONFLICT | 409 | Propor ou efetivar mudança de membro de grupo ou de papel referenciado por nível de controle sem identidade verificada com CPF único por tenant, ou incluir nesse grupo quem não a tem (RN-AP-69) |
| APPROVAL_SETTINGS_CHANGE_CONTROL_MEMBER_REMOVAL_FORBIDDEN | 403 | Propor ou efetivar a saída de membro — ou o fim da vigência dele — de grupo ou papel de nível de controle, ou de grupo usado como reserva de processo ou de política, sendo membro remanescente dele; ou remoção que deixa menos de dois membros vigentes em grupo referenciado por nível de controle Any, seja quem for quem propõe e quem efetiva; a comparação é por CPF verificado (RN-AP-69) |
| APPROVAL_SETTINGS_CHANGE_STALE_CONFLICT | 409 | Efetivar mudança crítica que não está Pending, ou cujo valor "antes" não é mais o vigente (RN-AP-69) |
| APPROVAL_LEVEL_NOT_ADMITTED_UNPROCESSABLE | 422 | Publicar nível com tipo de aprovador ou escopo que o processo não admite. A mensagem nomeia o nível e o que o processo admite (RN-AP-54) |
| APPROVAL_POLICY_VALUE_NOT_APPLICABLE_UNPROCESSABLE | 422 | Critério faixa de valor ou activationMinValue em processo sem valor (RN-AP-54) |
| APPROVAL_POLICY_APPROVAL_NOT_ALLOWED_UNPROCESSABLE | 422 | Na publicação, política que pode terminar sem decisão humana em processo com allowPolicyApproval falso; na submissão, a mesma checagem como defesa (RN-AP-54) |
| APPROVAL_POLICY_DISTINCT_APPROVERS_UNPROCESSABLE | 422 | Publicar política base sem nível incondicional em Quorum(n) com n ≥ minDistinctApprovers do processo (RN-AP-54) |
| APPROVAL_DISTINCT_APPROVER_UNPROCESSABLE | 422 | Na decisão: segunda aprovação do mesmo actingUserId no mesmo passo, ou conclusão da rodada com menos pessoas distintas que minDistinctApprovers (RN-AP-56) |
| APPROVAL_APPROVER_NOT_ELIGIBLE_FORBIDDEN | 403 | Na decisão: quem age perdeu a elegibilidade depois de receber a tarefa — conta desativada, permissão ou empresa retirada (RN-AP-59) |
| APPROVAL_DELEGATE_NOT_ELIGIBLE_UNPROCESSABLE | 422 | Criar delegação para quem não tem act:approvals, tem conta inativa ou é o próprio delegante (RN-AP-56) |
| APPROVAL_REASSIGN_NOT_ALLOWED_UNPROCESSABLE | 422 | Reatribuir tarefa de processo com allowReassign falso |
| APPROVAL_AUTHORITY_EXCEEDED_UNPROCESSABLE | 422 | Submissão em processo com authorityExceededBehavior = Refuse cuja cadeia projetada estoura a alçada na raiz ou cai no reserva. A mensagem nomeia o passo, o valor e o maior limite encontrado (RN-AP-58) |
| APPROVAL_VALUE_UNAVAILABLE_UNPROCESSABLE | 422 | Submissão com valor indisponível em processo cuja cadeia precisa comparar alçada ou materialidade (RN-AP-62) |
| APPROVAL_EXCLUDED_USERS_UNPROCESSABLE | 422 | Submissão ou reavaliação sem o Requester na lista de excluídos — ou com o requesterUserId fora dela —, sem o Proponent no BANK_ACCOUNT_CHANGE, ou com usuário de outro tenant (RN-AP-60) |
| APPROVAL_SUBJECT_HASH_REQUIRED_UNPROCESSABLE | 422 | Submissão, reenvio ou reavaliação sem subjectVersion e subjectHash (RN-AP-67) |
| APPROVAL_SUBMISSION_CONTRACT_UNPROCESSABLE | 422 | Corpo de submissão, reenvio, reavaliação, simulação ou prévia que não cumpre §Contrato de submissão: campo exigido pelo processo ausente ou não admitido presente, lineId ou allocationId repetido, soma das fatias diferente da linha em qualquer das duas moedas, conversion ausente com moeda diferente da funcional ou presente com a mesma — ou, no revaluate, diferente da gravada —, empresa que não é a do estabelecimento, flag que o processo não declara, valor negativo. A mensagem nomeia o campo e a linha (RN-AP-72) |
| APPROVAL_REQUEST_ALREADY_EXISTS | 409 | POST /approvals para assunto que já tem instância InApproval ou Returned no mesmo processo, ou Approved fora da correção de identidade de fornecedor. O aprovado muda por revaluate, e o devolvido volta por resubmit (RN-AP-72) |
| APPROVAL_ACTIVATION_WINDOW_UNPROCESSABLE | 422 | Antecipar activationNotBefore, ou mudá-la depois que um passo ativou (RN-AP-63) |
| APPROVAL_STEP_NOT_STALLED_CONFLICT | 409 | Pedir nova resolução de passo que não está parado por falta de pessoas elegíveis (RN-AP-59) |
| APPROVAL_PROCESS_NO_FALLBACK_UNPROCESSABLE | 422 | Ativar processo com valor sem aprovador de reserva (RN-AP-03). Nos processos sem valor com política semeada o reserva é opcional, e no BANK_ACCOUNT_CHANGE é proibido |
| APPROVAL_COMMENT_REQUIRED_UNPROCESSABLE | 422 | Rejeição, devolução, ressalva, reatribuição ou pedido de informação sem comentário |
| APPROVAL_LIMIT_CURRENCY_NOT_CONVERTIBLE_UNPROCESSABLE | 422 | Submeter documento cuja moeda não tem par de conversão vigente contra a moeda de um approvalLimit que a cadeia precisa avaliar. Repassado da Moeda sem reembrulho, no mesmo padrão do BUDGET_EXCEEDED_UNPROCESSABLE; a mensagem nomeia o par e o vínculo. Comparar sem par seria comparar números de moedas diferentes no ponto em que a alçada decide (RN-AP-52). Não é o CURRENCY_PAIR_NOT_CONFIGURED_UNPROCESSABLE da Moeda, que é conversão pedida num sentido sem par cadastrado, em qualquer chamada. |
| REASON_CODE_REQUIRED_UNPROCESSABLE | 422 | Rejeição ou devolução sem reasonCodeId. Repassado do catálogo de Motivo sem reembrulho, como os erros do GrydFiles |
| APPROVAL_NOT_FOUND | 404 | Instância, passo ou tarefa inexistente neste tenant |
Autorização
Permissões
| Permissão | Público | Cobre |
|---|---|---|
| read:approval-processes | NexioAdmin | Ler processos e parâmetros por processo |
| update:approval-processes | NexioAdmin | Editar parâmetros por processo, propor e efetivar mudança de parâmetro crítico e refazer a resolução de passo parado |
| delete:approval-processes | NexioAdmin | Descartar proposta de mudança de parâmetro crítico do processo |
| read:approval-settings | NexioAdmin | Ler a configuração de tenant do motor. Separada da de processo porque muda o comportamento de todos os processos de uma vez |
| update:approval-settings | NexioAdmin | Editar a configuração de tenant, propor e efetivar mudança da materialidade |
| delete:approval-settings | NexioAdmin | Descartar proposta de mudança da materialidade |
| read:approval-policies | NexioAdmin, Controladoria | Ler políticas, validar e simular |
| create:approval-policies | NexioAdmin | Criar política em rascunho |
| update:approval-policies | NexioAdmin | Editar e publicar política |
| delete:approval-policies | NexioAdmin | Desativar política: publicar versão com vigência encerrada |
| read:approval-groups | NexioAdmin | Ler comitês e membros |
| create:approval-groups | NexioAdmin | Criar comitê |
| update:approval-groups | NexioAdmin | Editar comitê e membros, propor e efetivar mudança de membros de grupo protegido |
| delete:approval-groups | NexioAdmin | Descartar proposta de mudança de membros de grupo protegido |
| read:approval-delegations | NexioAdmin | Ler qualquer delegação do tenant |
| create:approval-delegations | NexioAdmin | Criar delegação de qualquer pessoa do tenant |
| manage-own:approval-delegations | Aprovador | A própria delegação [PERGUNTA AO ROGER: o escopo "própria" não é um dos cinco verbos; como fica a permissão de quem só mexe na própria delegação?] |
| read:approvals | Requisitante, Aprovador, Auditoria | Instância, cadeia e trilha dos documentos a que a pessoa tem acesso |
| act:approvals | Aprovador | Decidir sobre tarefas atribuídas a si. Não é guarda-chuva: a tarefa precisa ser sua |
| reassign:approvals | Aprovador sênior, Admin | Passar tarefa a outro elegível |
| erase:approval-comments | Encarregado de dados (DPO) | Apagar o texto livre de um comentário da trilha por pedido de eliminação sem base legal de guarda (RN-AP-48). Ato de controle |
| audit:approvals | Auditoria | Ler qualquer instância do tenant, inclusive de documentos fora do alcance da pessoa. Exercida por GET /approvals e GET /approvals/reason-summary |
Quem são os públicos. A coluna é a semente de papéis, não autorização — o que autoriza é a permissão. NexioAdmin é o papel de administração do motor que o Nexio semeia no tenant criado, com chave estável e rótulo editável, ao lado dos papéis funcionais que a Notificação semeia (Controladoria, Compras, Catalogo); recebe as permissões read:, create:, update: e delete: de configuração deste módulo e, na semente, nenhuma de decidir ou auditar (act:approvals, reassign:approvals, audit:approvals), para que quem configura o controle não seja quem o exerce. Requisitante, Aprovador, Aprovador sênior e Auditoria são perfis sugeridos, sem semente. A semeadura de permissão de produto em tenant novo depende da plataforma.
A separação entre act:approvals e reassign:approvals é deliberada: quem aprova não necessariamente pode escolher quem aprova no lugar dele — que é como uma alçada é contornada sem violar nenhuma permissão.
Rastreabilidade
Regras de negócio
Uma política base vence; complementares empilham
A cadeia final é a união da base que venceu com todas as complementares que casaram, ordenada por stage e deduplicada (RN-AP-57, RN-AP-65). A base isDefault só vence quando nenhuma outra base casa.
Prioridade desempata bases; especificidade não
Menor número vence. Sobreposição com a mesma prioridade é recusada ao publicar — verificada na aplicação, na mesma transação, com guarda parcial no banco quando ele oferecer. Se ainda assim duas casarem em execução, a submissão responde 422 APPROVAL_POLICY_AMBIGUOUS_UNPROCESSABLE.
Todo processo tem política base de reserva, e o aprovador de reserva depende do processo
Exatamente uma política base com critérios vazios e isDefault. O aprovador de reserva — pessoa ou grupo — é obrigatório para ativar processo com valor; opcional nos processos sem valor que nascem com política semeada, onde a resolução vazia é 422 na submissão (RN-AP-09); e proibido no BANK_ACCOUNT_CHANGE, que com menos pessoas elegíveis que o quórum fica parado com alerta (RN-AP-59, RN-AP-61). Ativar processo com valor sem reserva é APPROVAL_PROCESS_NO_FALLBACK_UNPROCESSABLE · 422.
Critério de árvore guarda o nó e vale por rollup
Centro de custo e categoria, por prefixo de path. Descendentes criados depois já nascem cobertos; override no descendente é uma política de prioridade menor.
Política é versionada e a versão é imutável
Editar publica versão nova. Instâncias vivas continuam na versão que as formou.
A cadeia é congelada na submissão
Passos, escopos, modos, SLA, valor avaliado, taxa de câmbio, rateio, o limiar de materialidade efetivo, a lista de excluídos, o retrato dos critérios e o authorityExceededBehavior do processo. Nada disso muda sem reavaliação explícita — mexer na configuração no meio do caminho não reembaralha aprovadores de instância viva. O limiar resolve-se uma vez, na submissão, pela mesma razão da taxa de câmbio. Único passo que nasce depois da submissão: o de controladoria da RN-AP-58, e nasce da regra congelada aqui, não de configuração nova.
Os aprovadores são resolvidos na ativação do passo
Responsáveis, membros de grupo e delegação vigente são lidos na hora em que o passo ativa, não na submissão.
O requisitante e os excluídos nunca decidem a própria instância
Removidos de toda tarefa antes de qualquer outra regra, e não há chave que desligue isso — allowSelfApproval não existe. A lista de excluídos vem do módulo dono na submissão — requisitante, quem preparou em nome de, beneficiário, último editor, proponente — e só cresce (RN-AP-60). Se o passo esvaziar, sobe um nível nos tipos de árvore ou cai no reserva nos demais; se esgotar, 422 nomeando o passo e dizendo que o excluído era o único aprovador possível — salvo no BANK_ACCOUNT_CHANGE, que não tem reserva: lá a instância fica parada com alerta (RN-AP-59). Vale para todo processo, inclusive os do Fornecedor e do Item: é esta regra — e não uma checagem no domínio — que garante "aprovador ≠ solicitante" na alteração bancária (RN-FOR-12). APPROVAL_SELF_APPROVAL_UNPROCESSABLE fica como defesa em profundidade na decisão.
Passo sem aprovador é recusa na submissão e desfecho declarado na ativação
422 nomeando o passo, na submissão. Só activationMinValue, a dedupe de passo de uma decisão (RN-AP-57) e a dispensa da RN-AP-68 produzem passo pulado. A guarda de grupo ou vínculo vazio vale também para expiração por vigência — e, quando o esvaziamento acontece na ativação, o desfecho é o da RN-AP-45, não o 422. "Vazio" é relativo ao modo: Quorum(n) com menos de n pessoas elegíveis é vazio (RN-AP-59).
Paralelo é dentro do nível; entre níveis é sequencial
Any, All ou Quorum(n). Rejeição encerra a instância em todos os modos — em Quorum(n), fecha em n aprovações de pessoas distintas e a primeira rejeição encerra. Votação com dissidência tolerada é modo próprio de fase 2, Vote(n), restrito aos processos que o produto declarar (BUDGET, BUDGET_CHANGE, CONTRACT); nunca uma releitura do Quorum, nunca em BANK_ACCOUNT_CHANGE.
Nada é aprovado por decurso de prazo
SLA vencido notifica ou escalona. AutoApprove não existe como comportamento de escalonamento.
Escalonar acrescenta caminho em Any, e substitui nos demais modos
Em Any, a tarefa original continua viva, ambas fecham o passo, e o evento registra qual chegou primeiro. Em All e Quorum(n) a escalada substitui a tarefa do titular em vez de somar-se a ela (RN-AP-50).
SLA conta em horas úteis e pausa em pedido de informação
Hora útil é hora dentro do expediente do tenant, no fuso ApprovalSettings.timeZone (padrão America/Sao_Paulo), em dia da semana marcado como útil e fora da lista nonWorkingDates, semeada com os feriados nacionais e editável pelo tenant. Não há calendário por estabelecimento: a lista é uma só. O tempo pausado é registrado separadamente.
Regras são avaliadas por linha e fatia; a decisão é do documento inteiro
Critérios de linha casam na mesma linha e critérios de fatia na mesma fatia dessa linha (RN-AP-66). Fatias do mesmo centro de custo são agregadas antes de gerar passo.
Alçada é avaliada sobre a fatia, acima do limiar de materialidade, e reconferida depois da dedupe
Fatia imaterial é absorvida pela maior para efeito de roteamento e continua contabilizada integralmente. Esta é a regra única do produto para materialidade, e o limiar é um só por tenant: fatia é imaterial quando fica abaixo do menor entre allocationMaterialityPercent do valor do documento e allocationMaterialityAmount — o valor absoluto é o teto do limiar. Resolvido na submissão e congelado com a cadeia (RN-AP-06). O Centro de Custo cita esta regra (RN-CC-24) em vez de redeclará-la. Materialidade é regra de roteamento, e só: o orçamento sempre vê a decomposição integral — a fatia absorvida continua sendo consumida na linha dela —, e Block numa fatia absorvida recusa a submissão do mesmo jeito.
Reconferência depois da dedupe. Quando a etapa 4 do rateio junta numa tarefa as fatias de uma pessoa, a alçada dela é comparada com o valor somado, na moeda do limite (RN-AP-52). Não cobrindo, ela sai dessas fatias; cada fatia continua a subida a partir do nó acima daquele em que ela estava, e dedupe e reconferência se repetem até alguém cobrir a soma ou a subida chegar à raiz (RN-AP-16, RN-AP-58). Vale para CostCenterOwnerUpToAmount, o tipo que compara alçada. Verificado na ativação do passo; não tem erro próprio — o resultado é o aprovador, ou a marca de alçada estourada.
Caso de ataque. Num pedido de R$ 10 milhões, 5% davam R$ 500 mil de limiar, e uma fatia de R$ 400 mil em outro centro de custo nunca passava pelo dono dele. E Carla, com limite de R$ 150 mil, recebia duas fatias somadas em R$ 160 mil — fracionamento pelo rateio. Agora o limiar é o menor dos dois (R$ 1.000 no padrão), a fatia de R$ 400 mil gera passo próprio, e a tarefa de R$ 160 mil sobe até quem cobre a soma.
Alçada não soma entre pessoas; acumula subindo a árvore
Esgotada a subida, os Owners da raiz recebem a tarefa com authorityExceeded marcado na ativação, evento na trilha e aviso à controladoria antes da decisão (RN-AP-31) — e o processo decide o resto por authorityExceededBehavior: exige a decisão da controladoria em passo paralelo, ou recusa a submissão (RN-AP-58).
Delegação redireciona a tarefa e não encadeia
Titular e substituto ficam ambos na tarefa, com o id da delegação. Delegação de delegação é 422.
Rejeitar encerra; devolver encerra a rodada e mantém a instância viva
Toda devolução encerra a rodada. O reenvio abre rodada nova na mesma instância; cada passo é recriado e só herda a decisão anterior pela RN-AP-21 — os demais nascem Pending, e os da rodada anterior ficam Obsolete na trilha.
Comentário é obrigatório em toda ação que não seja aprovar
Rejeitar, devolver, pedir informação, reatribuir e aprovar com ressalva.
Toda alteração do assunto depois da submissão reavalia, sem tolerância
O módulo dono informa ao motor toda alteração do documento aprovado (POST /approvals/{id}/revaluate, RN-AP-43) e todo reenvio de documento devolvido, com o retrato novo e o último editor (RN-AP-60). O motor compara com o retrato da rodada anterior: no documento aprovado, com (a), (b) e (c) da RN-AP-21 é ajuste, sem rodada nova (RN-AP-71), e sem elas reabre com rodada do zero; no reenvio, decide pela RN-AP-21 o que cada passo herda. Não há tolerância de aumento, e a reavaliação não tem parâmetro por processo. No pedido que cita requisição aprovada, a reavaliação e o reenvio reconferem a soma da RN-AP-68 com o valor novo, sob a trava da instância de origem.
Preservar assinatura exige valor que não subiu, critério igual e cadeia contida
Na alteração de documento Approved não há herança: com (a), (b) e (c) não há rodada nova — é ajuste (RN-AP-71) —, e sem elas a rodada é do zero. No reenvio de documento devolvido, a rodada nova recria cada passo, e um passo herda a decisão aprovada da rodada anterior — inheritedFromStepId, evento approval-step.inherited, contando com o actingUserId original na RN-AP-56 — só se as três condições valem juntas: (a) o valor avaliado do assunto e o de cada fatia e de cada linha não subiu, nas duas moedas — a do documento e a funcional, convertida pela taxa da rodada (Moeda e Câmbio, RN-CUR-22) —; (b) nenhum atributo lido pelos critérios do processo mudou — centro de custo, rateio, conta contábil, categoria, tipo de item, destinação, empresa, estabelecimento, UF de entrega, fornecedor, flags do assunto, estado do orçamento —, o que se prova pelo criteriaSnapshotHash; (c) a cadeia recalculada contra as políticas vigentes (RN-AP-47) é igual ou subconjunto da anterior, comparando passo a passo por policyId, levelOrder, escopo e alvo; e, passo a passo, (d) quem decidiu passa hoje, na criação da rodada nova, nas quatro condições de elegibilidade da RN-AP-59 (1)–(4) para aquele passo — conta ativa, act:approvals e, em nível PermissionHolder, a permissão do nível — e, em passo de controle, pertencer hoje ao alvo do nível, membro vigente do papel ou do grupo —, escopo na empresa do documento, fora dos excluídos — e não violaria a RN-AP-64; e, onde a separação é por pessoa (passo de controle, BANK_ACCOUNT_CHANGE, correção de identidade), relê também a identidade verificada e a distinção por CPF: a decisão de quem perdeu a identidade verificada, ou cujo CPF deixou de ser distinto dos demais decisores e do proponente, não é herdada — aquele passo nasce Pending, em vez de a rodada nova nascer com uma decisão que o desfecho vai recusar de novo. A decisão de quem foi desligado, perdeu a permissão ou o escopo na empresa, saiu do papel ou do grupo de um passo de controle, ou virou LastEditor ao mexer numa descrição, não é herdada: aquele passo nasce Pending. Fora de (a), (b) e (c) é rodada nova do zero, com todas as decisões anteriores em Obsolete. Passo que saiu da cadeia vai a Obsolete; passo que ficou herda. No pedido que cita requisição aprovada, a cadeia recalculada traz a dispensa da base conferida de novo pela RN-AP-68 — soma com o valor novo, sob a trava da instância de origem —: se a soma estoura, a base volta, a cadeia não é subconjunto e a rodada recomeça do zero.
O retrato. Na submissão e em cada rodada o motor grava criteriaSnapshot e criteriaSnapshotHash na instância — o que entra em cada um é a RN-AP-72: os valores do documento, das fatias e das linhas e todo atributo que os critérios declarados pelo processo leem — não só os das políticas publicadas hoje, para que uma complementar publicada amanhã encontre a mudança. O retrato da rodada anterior fica no evento que a abriu. Verificado na reavaliação, dentro da transação da RN-AP-44. O retrato inclui o sinal de orçamento por fatia com que a rodada foi montada, e o motor o repassa ao contrato de desfecho: é com ele que o Orçamento distingue, na decisão final, a linha que piorou depois da rodada.
Caso de ataque. Pedido de R$ 80 mil, com a fatia de TI de R$ 60 mil aprovada por Carla, limite R$ 150 mil. Editado para R$ 250 mil, a fatia ia a R$ 187,5 mil e a assinatura dela era preservada acima da alçada; e editar a UF de entrega depois de aprovado escapava do fiscal. Agora o valor que sobe recomeça a rodada, e a UF está no retrato: mudou o hash, recomeça.
Aprovação em lote gera um evento por tarefa
Nunca um evento de lote.
Toda transição é evento, gravado com o estado
Append-only, com autor, papel pelo qual agiu, delegação em vigor e passo alcançado, gravado na mesma transação que muda o estado do agregado. O estado não é recalculado a partir dos eventos; o evento é a prova da transição (RN-AP-48).
Duas decisões simultâneas na mesma tarefa: a segunda é 409
Concorrência otimista na tarefa. A segunda pessoa é informada de quem decidiu e quando, não de um erro genérico. Não é a única trava: o que decide o avanço do passo é sempre uma leitura agregada de outras tarefas, e essa serializa na raiz pela RN-AP-44.
Aposentada — o efeito da aprovação é do contrato de desfecho
Não existe evento de empenho. Gravar ou liberar o Commitment, aplicar a proposta bancária, publicar o item — todo efeito da decisão que conclui acontece no contrato de desfecho, na transação da decisão (RN-AP-38). O número fica reservado para que nenhuma citação antiga aponte para outra regra.
A cadeia é consultável antes de existir
POST /approvals:simulate (read:approval-policies) roda a mesma seleção de POST /approvals, sem criar nada, com o mesmo corpo, versões em rascunho por policyOverrides e data de referência por asOf; pessoas resolvidas são sempre as de hoje. Override que não é Draft do mesmo processo e tenant é APPROVAL_SIMULATION_OVERRIDE_UNPROCESSABLE · 422. A prévia do requisitante é POST /approvals:preview, interna, chamada pelo módulo dono sobre o próprio rascunho.
Quem decide bloqueio por verba é o orçamento, não o motor
O motor consulta POST /evaluate:budget-state e obedece à overBudgetPolicy resolvida por nó, com a tolerância que o orçamento aplica. Não existe parâmetro de bloqueio no motor: dois lugares decidindo a mesma coisa divergem no primeiro documento com fatias em centros de custo diferentes — e quem erraria é o motor, que não conhece herança de política nem folga. É o princípio 6 desta spec aplicado a si mesma: o motor consome contratos, não caminha em árvores. Os sinais que o motor trata são quatro — Ok, Warning, Exceeded e Uncovered —, e Uncovered bloqueia sempre (RN-AP-53).
Toda aprovação humana é uma instância do motor
Onde uma segunda pessoa decide sobre o pedido de outra, o módulo dono do documento chama POST /approvals e aplica o efeito quando a instância termina. Nenhum módulo tem rota própria de aprovar ou rejeitar. Atos unilaterais com motivo (bloqueio, promoção manual, revogação, substituição, escape de duplicata, expurgo) não são aprovação. Princípio 7.
Processo sem valor nasce com política padrão semeada
A política de reserva vem publicada pelo produto, com um nível PermissionHolder na permissão do ato no módulo dono. O tenant edita, nunca apaga a última. Processo com dinheiro continua exigindo configuração para ativar (RN-AP-03). Dia 1 igual ao de ontem, com uma diferença deliberada: quem cadastrou não aprova (RN-AP-08). Tenant em que o solicitante é o único detentor da permissão cai na RN-AP-09 — configuração de onboarding (segundo detentor ou reserva do processo), não exceção; o painel de administração avisa "processo com um único aprovador possível". O BANK_ACCOUNT_CHANGE nasce com a definição normativa da RN-AP-61, sem reserva; o SUPPLIER_REGISTRATION nasce também com a complementar semeada e travada da correção de identidade (identityCorrection, tabela de §Processos).
O uso de motivo da rejeição é declarado pelo processo
rejectReasonUsage e returnReasonUsage vêm do produto, por processo. Código e comentário continuam obrigatórios juntos em toda rejeição e devolução (RN-AP-19), em todo processo.
Alçada estourada é marcada na ativação e avisada antes da decisão
ApprovalStep.authorityExceeded e o evento approval-step.authority-exceeded são gravados quando o passo ativa nos Owners da raiz sem ninguém com alçada para o valor; nexio.approval-step.authority-exceeded vai à controladoria com a tarefa ainda aberta, a tarefa mostra a marca, o simulador a devolve por passo e /validate aponta a raiz sem Owner de autoridade ilimitada. Quem cobre sem teto é quem tem hasUnlimitedAuthority (RN-AP-51). A marca não autoriza nada sozinha: o que acontece depois é da RN-AP-58.
Cadeia inteira pulada termina aprovada pela política, com evento próprio
Todos os níveis abaixo do activationMinValue: a instância termina Approved sem tarefa e grava approval-request.approved-by-policy com a política e a versão, e o desfecho é invocado na transação do envio; o Refused recusa o envio (RN-AP-71). É a única aprovação sem pessoa do MVP — declarada na política, visível no simulador; /validate avisa a faixa sem decisor humano. Só em processo com allowPolicyApproval verdadeiro (RN-AP-54).
Membro de grupo entra e sai com vigência, e o nível lê a vigente na ativação
ApprovalGroupMember guarda usuário, grupo e período. Dois períodos do mesmo usuário no mesmo grupo não se sobrepõem — verificado na aplicação, na mesma transação, com guarda de exclusão por interseção no banco quando ele oferecer o recurso — qual banco é decisão de implementação, e ela vive na Arquitetura, não aqui. Grupo referenciado por política ativa, ou pela cadeia congelada de instância em InApproval (RN-AP-69), não pode ficar sem membro vigente: APPROVAL_GROUP_EMPTY_CONFLICT.
Toda promessa do motor tem rota: a resposta ao pedido de informação e a recusa do delegado
O SLA que "retoma na resposta" precisa de POST /approvals/{id}/answer-info, e só o requisitante da instância a chama; a recusa que "devolve ao titular" precisa de POST /approvals/{id}/assignments/{aid}/decline, e só o delegado a chama. Nenhuma das duas decide o documento, as duas geram evento.
Escalonamento tem alvo declarado, e processo de controle não escala
Em nível que caminha árvore, Escalate é o responsável do nó pai. Em qualquer outro tipo, o nível declara escalationTargetType e escalationTargetRef, verificados no /validate e na publicação (APPROVAL_ESCALATION_TARGET_REQUIRED_UNPROCESSABLE). O alvo entra na cadeia projetada pelo simulador: escalar nunca amplia o conjunto de aprovadores além do que foi mostrado antes. Processo com allowEscalation = false — semente do BANK_ACCOUNT_CHANGE — recusa Escalate na publicação e no vencimento apenas lembra. Nível de árvore cujo nó é a raiz não tem pai: o vencimento vira lembrete ao titular e à controladoria, grava approval-step.sla-expired com o motivo, e nunca cai no reserva. Todo vencimento grava approval-step.sla-expired, e todo lembrete, approval-assignment.reminded.
Uma delegação vigente por delegante e por processo, com fim obrigatório
Chave (delegante, scope, período) sem sobreposição, com scope nulo lido como "todos os processos"; verificação na aplicação, na mesma transação, e guarda no banco quando ele oferecer exclusão por interseção. validTo obrigatório, limitado a maxDelegationDays (padrão 90). Uma só por pessoa não conviveria com dez processos: impediria delegar processos diferentes a pessoas diferentes.
A delegação move a tarefa dentro da alçada do titular, e não alcança processo não delegável
O passo foi resolvido com o approvalLimit de quem responde pelo nó; o delegado age em nome dele e o teto da delegação só reduz. Acima do teto, a tarefa fica com o titular. Processo com delegable = false não é alcançado por nenhuma delegação, nem pela de escopo nulo. A trilha continua gravando aprovador de direito, quem agiu e o id da delegação (RN-AP-17).
O desfecho da instância e o efeito no assunto commitam juntos, e o desfecho tem três resultados
O efeito que precisa ser atômico com o ato acontece na mesma transação — pelo próprio caso de uso, por chamada síncrona declarada entre contextos (.Contracts) ou pelo contrato de desfecho do motor; o evento de domínio é publicado depois do commit e serve para notificar, medir e integrar — nunca para produzir efeito de que o ato dependa. A decisão que conclui uma instância — aprovação final, rejeição, cancelamento —, a conclusão sem decisão humana e o ajuste do documento aprovado (RN-AP-71) e a reabertura invocam o contrato de desfecho registrado para o ApprovalProcessCode, dentro da transação da decisão (RN-AP-44). O contrato é um só — instância, desfecho e, por fatia, o sinal de orçamento com que a rodada foi montada (RN-AP-21) —; quem sabe o que fazer com isso é o módulo dono, e ele devolve um de três resultados.
Applied. Decisão, transição do assunto e efeito — a Requisição transiciona e grava o Commitment, o Fornecedor aplica a proposta e inicia a carência, o Item publica — gravados na mesma transação. O evento sai depois do commit.
Refused(code). Recusa de negócio sob a trava do próprio módulo. A decisão não é gravada e a tarefa continua ativa; nenhum efeito, nenhuma transição. A tentativa fica registrada em transação própria, como ApprovalEvent approval-request.decision-refused com o código, e o POST da decisão responde com o código do módulo, sem reembrulho. O aprovador vê o motivo e escolhe: devolver com motivo, ou aguardar o que resolve o impedimento — um remanejamento de verba, uma evidência — e decidir de novo. No BANK_ACCOUNT_CHANGE, que não devolve (RN-AP-61), a escolha é rejeitar ou aguardar. Requisitante e dono do impedimento (o dono da verba, no orçamento) são avisados. Recusas conhecidas hoje: BUDGET_EXCEEDED_UNPROCESSABLE — Block estourado sob a trava das linhas de orçamento candidatas, e também RequireApproval que chegou a Exceeded depois de a rodada ter sido montada com sinal melhor —; BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE; BUDGET_CHANGE_INSUFFICIENT_UNPROCESSABLE, do remanejamento cuja linha de origem não cobre mais o valor na decisão final; no BANK_ACCOUNT_CHANGE, BANK_CHANGE_CALLBACK_REQUIRED_CONFLICT, BANK_CHANGE_NOTICE_NOT_DELIVERED_CONFLICT, BANK_CHANGE_SIGNED_REQUEST_REQUIRED_CONFLICT (troca de canal com noTrustedPhone sem o anexo do pedido de troca assinado liberado pelo antivírus — nunca enviado, de outro tipo, revogado, eliminado ou sem veredito, RN-FOR-40), BANK_CHANGE_EVIDENCE_NOT_AVAILABLE_CONFLICT (o comprovante de titularidade ou o pedido assinado exigido na submissão foi revogado, eliminado ou deixou de estar Available, RN-FOR-35), BANK_APPROVER_IDENTITY_NOT_VERIFIED_CONFLICT (decisões da instância sem identidade verificada, ou cujos CPFs não são distintos entre si e do proponente, RN-FOR-36), BANK_CHANGE_APPROVER_IS_REQUESTER_CONFLICT (decisão vinda do CPF de pessoa ligada ao número do callback — aqui é Refused, não o 409 que o Fornecedor devolve nos atos dele) e BANK_CHANGE_IDENTITY_CORRECTION_LINKED_CONFLICT (decisão de pessoa ligada a uma correção de identidade daquele fornecedor, RN-FOR-41); e, no SUPPLIER_REGISTRATION de correção de identidade, SUPPLIER_IDENTITY_CORRECTION_ACCOUNT_MISMATCH_CONFLICT, SUPPLIER_IDENTITY_CORRECTION_IDENTITY_NOT_VERIFIED_CONFLICT e SUPPLIER_IDENTITY_CORRECTION_EVIDENCE_REQUIRED_CONFLICT (correção de identidade sem o anexo comprobatório do tipo declarado liberado pelo antivírus; o conteúdo do documento não é conferido, RN-FOR-39) — as do BANK_ACCOUNT_CHANGE e do SUPPLIER_REGISTRATION definidas no Fornecedor, a do remanejamento no Orçamento. A identidade não verificada de quem propõe não chega ao desfecho e não é Refused: é 409 no :submit do Fornecedor, e a instância nem abre. Módulo que criar recusa nova a declara na própria página, e esta lista a cita.
O motor não confere documento. Onde a regra do módulo dono exige documento comprobatório — a troca de canal de aviso sem telefone confiável e a correção de identidade do fornecedor —, a evidência é o anexo, e quem o exige é o módulo dono: que o anexo exista, seja do tipo declarado e esteja com o veredito do antivírus liberado, e que a transação da decisão grave o anexo, quem anexou, quem decidiu, a data e a hora. O Fluxo não guarda nem redeclara evidência, não confere assinatura digital, poderes do signatário, selo de cartório nem apostila, e …/assignments/{aid}/approve não tem campo para isso — o documento é escolhido pela norma da empresa, e nenhuma regra de tipo de documento por país entra aqui. Faltando o anexo, o desfecho devolve Refused com o código do Fornecedor, a decisão não é gravada e a tarefa continua ativa.
Failed. Falha técnica: rollback total, nada gravado — nem decisão, nem tentativa —, resposta com código de sufixo _UNAVAILABLE (503) ou erro interno (500). Repetir é seguro, porque a decisão carrega expectedVersion.
O sentido inverso usa o mesmo contrato. Cancelar depois de aprovado libera o efeito na transação do ato. Reabrir Approved (RN-AP-43) invoca o contrato na mesma transação, e o efeito é do módulo dono: a requisição reaberta não grava nada no razão, e a reaprovação grava só a diferença (Orçamento, RN-O45); a alteração que não reabre é ajuste, e grava a diferença na hora (RN-AP-71). Cancelar em InApproval — inclusive a contestação do fornecedor (RN-AP-61) — invoca o contrato sem efeito a desfazer. Toda devolução encerra a rodada: na rodada nova cada passo é recriado e só herda a decisão anterior pela RN-AP-21.
O que sai do processo. Publicação garantida de evento, entrega ao ERP e a espera visível de mensagem que não pode sair são requisitos registrados na página de Mensageria; a escolha de mecanismo e transporte é do estudo da plataforma.
Caso de ataque. Dois pedidos simultâneos contra a última fatia de verba: os dois aprovadores finais decidem ao mesmo tempo. Com o empenho por evento, os dois pedidos ficavam Approved e a verba ia negativa depois; com a decisão gravada e o estouro devolvido ao requisitante, a cadeia assinada sobrevivia a um efeito que não aconteceu. Agora o segundo recebe Refused(BUDGET_EXCEEDED_UNPROCESSABLE) sob a trava, a decisão dele não existe, e a tarefa continua esperando remanejamento ou devolução.
Aprovador fora de escopo é defesa em profundidade, e avisa
Quando o filtro por pessoa da RN-AP-59 retira, por escopo de empresa, todos os candidatos de um nível, o nível é tratado como resolvido vazio — cai no reserva da política, ou no do processo. A novidade é que isso deixa de acontecer calado: grava-se ApprovalStep.resolvedOutOfScope na ativação, com evento approval-step.resolved-out-of-scope e aviso à controladoria, na mesma forma que o authorityExceeded já usa (RN-AP-31) — porque os dois são o mesmo tipo de fato, uma cadeia que não saiu como o desenho previa.
Esta regra não deveria disparar nunca. O caminho normal é a RN-CC-30, que recusa o vínculo de quem não alcança a empresa no momento em que ele é criado. O que sobra aqui é o caso em que o escopo foi revogado depois — a pessoa mudou de área e o admin tirou a empresa dela sem lembrar dos vínculos. Nesse caso o vínculo fica dormente, e é melhor a controladoria descobrir por um aviso do que pela pergunta "por que este pedido foi parar no reserva?". O GET /approval-policies/{id}/validate acusa o mesmo antes da publicação, com o teste da RN-AP-40: o alvo tem de alcançar todas as empresas que a política alcança.
Alvo que não vem da árvore tem gente elegível em toda empresa que a política alcança
A RN-CC-30 guarda os tipos que caminham a árvore de centro de custo — o vínculo só é criado para quem alcança a empresa do nó. Os outros tipos não passam por vínculo nenhum: SpecificUser, Role, PermissionHolder e ApprovalGroup resolvem por papel, permissão ou lista, e nada neles conhece empresa. Para esses, a guarda é na publicação: GET /approval-policies/{id}/validate e a publicação recusam nível cujo alvo não tenha, em cada empresa que a política alcança — as do critério Empresa em lista, ou todas as ativas do tenant quando o critério é vazio —, pessoas elegíveis bastantes para o modo do nível. APPROVAL_LEVEL_TARGET_OUT_OF_COMPANY_SCOPE_UNPROCESSABLE · 422. Em processo cujo assunto não tem empresa, a guarda não se aplica. Na execução o filtro é por pessoa (RN-AP-59): um papel com gente de várias empresas publica, e cada documento chega só a quem enxerga a empresa dele.
O reserva alcança todas as empresas ativas do tenant. O reserva — pessoa ou grupo, da política e do processo — é o último recurso de qualquer empresa: a pessoa precisa ter escopo completo — linha vigente em todas as empresas ativas do tenant no instante da verificação (RN-EMP-27 da Empresa) —, e o grupo, ao menos um membro elegível em cada empresa ativa. Não existe concessão "todas as empresas" — o escopo é parametrizado empresa por empresa, e empresa nova é concedida só a quem a criou. Por isso o painel de administração avisa o processo cujo reserva deixou de alcançar uma empresa ativa, e a publicação seguinte recusa.
A delegação vale só onde o delegado enxerga. POST /approval-delegations exige que o conjunto de empresas do delegado contenha o do delegante — APPROVAL_DELEGATION_OUT_OF_COMPANY_SCOPE_UNPROCESSABLE · 422. Sem isso a tarefa delegada chegaria a quem toma 404 ao abrir o documento, e a delegação falharia justamente na semana de férias em que foi criada. O teto de valor continua só reduzindo (RN-AP-37); o escopo não é teto, é pré-requisito.
Política que cobre estouro de verba não some sem avisar quem depende dela
Um nó de centro de custo com overBudgetPolicy = RequireApproval declara uma intenção que este módulo é que cumpre: o passo a mais só existe se houver política Supplemental com o critério estado do orçamento alcançando aquele nó. As duas peças têm donos diferentes: a tela de orçamento avisa ao salvar o nó, e esta regra cobre o lado de cá. Aposentar ou deixar expirar a política sem checagem produziria um controle que morre em silêncio: o nó continua marcado, e no dia seguinte Exceeded passa como Warn.
A guarda é no /validate, e é aviso, não trava. Publicar uma Supplemental com critério de orçamento, mudar-lhe os critérios ou aposentá-la faz a tela listar os nós que ficariam órfãos, nomeando-os. Não recusa: o cliente pode legitimamente estar trocando uma política por outra, e travar a configuração o obrigaria a desmarcar todos os nós antes — o oposto de pôr o erro onde há contexto. É a mesma escolha da RN-CC-30, com a mesma razão: o erro aparece na tela de quem pode corrigi-lo, no momento em que ele nasce.
Não há código de erro nesta regra, e isso é decisão. Das quatro pontas que uma regra nova costuma ter — a regra, o campo, o slug e o erro —, esta tem três: o erro só existiria se a guarda recusasse, e ela não recusa. O que sobra em execução está na RN-AP-42.
Exigência de aprovação declarada e não cumprida é marcada e avisada
Se a cadeia resolvida não tem nenhum passo vindo de política com critério estado do orçamento, e há fatia cuja overBudgetPolicy resolvida é RequireApproval com sinal Exceeded, a instância grava ApprovalRequest.budgetRequirementUnmet na resolução da cadeia, emite approval-request.budget-requirement-unmet na trilha e avisa a controladoria por nexio.budget.approval-requirement-unmet, com o documento ainda em aprovação. A marca fica no documento, como a de estouro já fica.
É o terceiro caso da mesma família — authorityExceeded (RN-AP-31) e resolvedOutOfScope (RN-AP-39) são os outros dois, e os três significam "a regra não saiu como o desenho previa": o produto passa, marca, e conta a quem responde pelo controle enquanto ainda dá para agir. A marca vive na instância, não no passo, porque o fato aqui é a ausência de um passo — não há onde pendurá-la.
Por que não reaproveitar o nexio.budget.overrun-recorded: ele diz "estourou" e sua audiência são os CostCenterOwners. Quem precisa saber que um controle configurado deixou de rodar é a controladoria — e os donos do nó são, nesse caso, exatamente quem se beneficiou do silêncio. Fato diferente, destinatário diferente, slug diferente.
E por que não tratar como Block: contraria o "nunca bloqueia por omissão" que esta base fixou, e transforma configuração incompleta em compra travada. A saída adotada mantém a compra andando e põe o defeito na frente de quem o corrige.
Approved é terminal condicional, e a reabertura conta rodada
A alteração que não toca o que a aprovação leu não sai de Approved: é ajuste, sem rodada (RN-AP-71). A instância tem duas saídas declaradas de Approved: Approved → InApproval por revaluate da alteração que não é ajuste, com rodada do zero, e Approved → Cancelled por cancel. As duas gravam evento — approval-request.reopened e approval-request.cancelled, o único nome do cancelamento — e a reabertura incrementa roundNumber na instância; os passos da rodada anterior vão a Obsolete e os novos nascem com a rodada nova. Não nasce estado no enum. Reopened só diria "já esteve aprovado", e isso a rodada já conta; um estado a mais obrigaria todo módulo consumidor a tratá-lo, para não dizer nada que InApproval + roundNumber > 1 não diga.
A reabertura passa pelo contrato de desfecho no sentido inverso, na mesma transação (RN-AP-38), e o módulo dono decide o efeito — a requisição reaberta mantém o comprometido preso, e a reaprovação grava só a diferença (Orçamento, RN-O45). Documento em moeda diferente da funcional é reconvertido nesse desfecho, que devolve a conversão nova com que a rodada do zero é montada (RN-AP-72). No BANK_ACCOUNT_CHANGE não há reabertura: revaluate é APPROVAL_TRANSITION_NOT_ALLOWED_CONFLICT · 409. Sem a transição escrita, cada módulo transacional improvisaria a sua — uns reabrindo a instância, outros criando uma segunda —, e a pergunta "quantas rodadas este pedido teve", que é o que justifica a instância única, deixaria de ter resposta no primeiro que escolhesse diferente.
Toda decisão serializa na raiz da instância
ApprovalRequest tem version (int) e toda decisão passa expectedVersion; divergência é APPROVAL_REQUEST_VERSION_CONFLICT · 409, e a resposta traz o estado atual para que a pessoa releia e decida de novo. O fechamento do passo e a ativação do seguinte acontecem dentro da mesma transação da decisão — a mesma em que a RN-AP-38 grava o efeito.
Por que a raiz e não a tarefa. A RN-AP-24 já protegia duas decisões na mesma tarefa, mas o que decide o avanço é sempre uma leitura agregada de outras tarefas: contar aprovações em Quorum(n), fechar All, marcar as demais Obsolete em Any, ver que todas as fatias do nível fecharam. Quorum(2) com três pessoas decidindo ao mesmo tempo: duas transações leem uma aprovação, ambas gravam duas, ambas fecham o passo, ambas invocam o contrato de desfecho — e a verba é empenhada duas vezes. Simétrico: duas fatias paralelas ativam o nível seguinte duas vezes, e uma rejeição concorrente com a última aprovação deixa a instância Rejected com o efeito de aprovação aplicado. Um token por tarefa não pega nenhum dos três, porque em nenhum deles duas transações escrevem a mesma tarefa.
O sequence do ApprovalEvent é alocado nesta mesma transação, que é o que faz a trilha ter ordem total dentro da instância (RN-AP-48). O custo é o esperado: as decisões de uma instância viram fila. É o preço certo — a publicação de política já paga o equivalente com bloqueio de linha, e uma instância recebe unidades de decisão por dia, não milhares.
Ativação que resolve vazio por passagem do tempo tem desfecho, não erro
A RN-AP-07 resolve as pessoas na ativação do passo, e o tempo passa entre a submissão e a ativação: o validTo do último membro do comitê vence, o papel fica sem detentor, o Owner deixa de ser vigente. Percorrida a ordem completa da resolução, um passo que ativa vazio por esse motivo marca ApprovalStep.resolvedEmptyOnActivation, cai no aprovador de reserva — o da política, depois o do processo —, grava approval-step.resolved-empty-on-activation e avisa a controladoria, no mesmo molde do authorityExceeded (RN-AP-31) e do resolvedOutOfScope (RN-AP-39). É o quarto caso da família "a cadeia não saiu como o desenho previa".
A ativação vazia nunca derruba a decisão de quem acabou de aprovar. É o ponto inteiro da regra: se a ativação do nível 2 devolvesse 422 dentro do POST de aprovação da Carla, ou a aprovação legítima dela seria revertida e ela veria erro numa ação correta, ou a instância ficaria InApproval sem nenhuma tarefa aberta, invisível em toda fila, sem SLA correndo, sem evento e sem aviso. Expired não existe no enum, de propósito: a resposta é o desfecho declarado. O 422 da RN-AP-09 continua sendo a resposta da submissão, onde quem chama é o módulo dono e ainda dá para recusar sem desfazer nada.
E avisa antes de vencer. A guarda de grupo ou vínculo vazio da RN-AP-09 vale também para expiração por vigência: membro de grupo, vínculo de centro de custo ou delegação cuja validTo se aproxima, em grupo ou nó referenciado por política ativa, gera aviso de vencimento com antecedência. Corrigir um cadastro antes de ele vencer é barato; descobrir pelo passo que caiu no reserva é o caminho que esta regra existe para tornar raro. Se o reserva também esvaziar — ou não completar o modo —, o passo fica parado com alerta até o cadastro ser corrigido (RN-AP-59): nunca é pulado e nunca derruba a decisão anterior.
Escopo Line: um passo por linha que casa, sem materialidade
ApprovalScopeMode tem três valores — Document, Line e Slice. Um nível com scopeMode = Line gera um passo por linha do documento que satisfaz os critérios de linha da política de origem do nível (RN-AP-66), com scopeRef = id da linha e scopeAmount = valor da linha; os passos de linha do mesmo nível são paralelos entre si, como os de fatia. A materialidade não se aplica — ela dispensa fatia, e o que faz uma linha entrar aqui não é o valor dela, é o critério: uma linha interestadual de R$ 300 passa pelo fiscal exatamente como uma de R$ 300 mil. A dedupe é por (pessoa, linha), não por (pessoa, fatia): quem responde pelo fiscal de duas linhas recebe duas tarefas, porque o que ele examina é a linha, uma de cada vez.
A reavaliação recasa contra as políticas vigentes na data da reavaliação
O casador roda de novo contra o conjunto vigente na data da reavaliação, não contra o congelado na submissão. A instância guarda as duas tuplas — a do snapshot original e a da rodada corrente, cada uma com policyId e policyVersion —, e o evento approval-request.revaluated registra o que mudou entre elas. A RN-AP-06 continua íntegra: o que ela congela é o documento que ninguém reavaliou.
Era a única leitura que fechava as duas afirmações. O princípio 1 diz que editar política não muda documento em andamento; a RN-AP-20 manda reavaliar quando muda a categoria porque isso pode acionar controle complementar que não existia — e um controle que não existia na submissão só entra se o casador olhar para o presente. Congelar as versões antigas na reavaliação seria pior na prática: a devolução reenviada é o caminho mais comum do produto, e o documento voltaria a circular sob a regra que o cliente acabou de corrigir. Editar política não muda documento que não foi reavaliado — é o que o princípio 1 protege.
Consequência na RN-AP-21: é a cadeia recalculada contra as vigentes que se compara com a anterior, passo a passo por policyId, levelOrder, escopo e alvo — sem o levelOrder, um nível que mudou de posição herdaria a assinatura de outro degrau. Se a cadeia ganhou passo, a rodada recomeça do zero — inclusive quando a base dispensada do pedido volta porque a soma da RN-AP-68, reconferida com o valor novo sob a trava da instância de origem, estourou.
A trilha é imutável, ordenada por sequence e de ações fechadas
ApprovalEvent tem tabela de campos, enum fechado de ações e chave de ordenação (approvalRequestId, sequence), com o sequence monotônico dentro da instância e alocado na transação da decisão (RN-AP-44). Ordenar por occurredAt é proibido: aprovação em lote grava N eventos no mesmo instante, e fatias paralelas fecham juntas — o empate é a regra, não a exceção.
Imutável quer dizer sem rota de escrita. Não há PUT, não há DELETE e não há soft delete — a exclusão lógica que o resto do produto herda da plataforma não se aplica aqui, e a exceção é declarada. Corrigir um evento é gravar outro que o corrija, apontando no metadata o sequence corrigido. Mesma disciplina do razão orçamentário, mesma razão: trilha editável não prova nada, e a promessa "aprovação tem prova quando a auditoria pergunta quem autorizou" precisa ser verificável, não afirmada.
O que é coluna e o que é metadata: tudo por que a GET /approvals filtra é coluna. O metadata guarda o que é específico da ação — o alvo da reatribuição, o que mudou na reavaliação, qual passo a escalada acrescentou. Pôr ali um campo de filtro é como o esquema se perde de novo.
A delegação alcança o passo que ativa na vigência; tarefa aberta fica com o titular
Uma ApprovalDelegation vale para os passos que ativam enquanto está vigente (RN-AP-07): a tarefa nasce com o titular como aprovador de direito e o delegado como quem age, e grava approval-assignment.delegated com o id da delegação. Tarefas Pending do titular no início da vigência não são transferidas; o fim da vigência não devolve tarefas. O SLA é do passo e não muda por delegação.
Escalonamento se compõe com o modo do nível, e o alvo passa pelas mesmas guardas
Em Any, a escalada acrescenta um caminho e a tarefa do titular continua viva (RN-AP-12). Em All e Quorum(n) a escalada substitui a tarefa do titular, que vai a Obsolete. O número de pessoas distintas que o modo exige não muda nunca — é o que o modo significa.
O caso que fecha: num Quorum(2) de quatro olhos, com a escalada somando, o titular atrasa, o escalado entra, e o quórum fecha com uma assinatura de quem decidiu e outra de quem entrou por decurso de prazo. É a superfície que a spec já fecha no BANK_ACCOUNT_CHANGE com allowEscalation = false e deixava aberta no cadastro de fornecedor PF e no item crítico, que também usam quatro olhos. Trancar o processo inteiro resolveria um caso e deixaria os outros; compor com o modo resolve todos, e não custa configuração nenhuma ao cliente.
O alvo da escalada passa por RN-AP-08, alçada e escopo de empresa, como o alvo original: o requisitante nunca decide a própria instância nem por escalonamento; o escalado precisa cobrir o scopeAmount do passo, ou o passo herda authorityExceeded e segue authorityExceededBehavior — exige a controladoria, porque na ativação nunca se recusa (RN-AP-31, RN-AP-58); e quem não alcança a empresa do documento cai na RN-AP-39. Uma porta que a submissão fecha e o vencimento abre não está fechada.
Alçada ausente é ausência de alçada; ilimitada é ato de cadastro
CostCenterAssignment.approvalLimit nulo significa sem alçada — o vínculo não cobre valor nenhum, e a resolução sobe a árvore. Quem decide sem teto é quem tem hasUnlimitedAuthority = true, campo próprio, preenchido de propósito. As duas páginas dizem a mesma coisa: o Centro de Custo é dono dos campos, este documento é dono da leitura que o motor faz deles.
Por que limite nulo não é ilimitado. O campo é opcional: se o filtro aceitasse "limite nulo (sem teto próprio)", todo vínculo criado sem preencher a alçada — importação, migração do managerUserId, admin com pressa — nasceria com autoridade infinita do ponto de vista do motor, e o /validate chegaria a exigir uma raiz com Owner de limite nulo. Ariba, Coupa e Protheus tratam limite ausente como zero. Autoridade infinita tem de custar um ato de cadastro, não um campo em branco — e o /validate aponta a raiz sem ninguém de autoridade ilimitada, que é a mesma checagem com o sinal certo.
A alçada compara na moeda do limite, com a taxa do documento
approvalLimit anda com approvalLimitCurrency, obrigatória quando o limite está preenchido — inclusive no nó corporativo, que não tem empresa. A comparação converte o valor da fatia ou da linha para a moeda do limite, usando a taxa do documento — a da submissão ou a da última rodada nova (Moeda e Câmbio, RN-CUR-22) —, e o ApprovalEvent da decisão grava a exchangeRate e a rateDate usadas. Submeter documento cuja moeda não tem par de conversão vigente contra a moeda de um limite que a cadeia precisa avaliar é recusa declarada — APPROVAL_LIMIT_CURRENCY_NOT_CONVERTIBLE_UNPROCESSABLE, repassado da Moeda sem reembrulho —, nunca comparação silenciosa. O código genérico de conversão sem par cadastrado é outro, e não é este.
Moeda funcional BRL, documentos multimoeda. A functionalCurrency da empresa é BRL na v1; documentos, preços e limites de alçada seguem em qualquer ISO 4217. A regra de comparação é uma só: converte-se para a moeda do limite, com a taxa congelada. Com um documento em USD e o limite do nó corporativo em BRL, comparar sem conversão declarada seria comparar moedas diferentes exatamente no ponto em que a alçada decide.
A taxa é do documento, e o razão herda. A conversão acontece na submissão e se renova só no ato que abre rodada nova — reenvio de devolvido e reabertura com rodada do zero —; o ajuste e as decisões usam a taxa gravada (Moeda e Câmbio, RN-CUR-22); todo movimento do razão originado por ele herda exchangeRate e rateDate do documento, e a resolução própria de taxa do Orçamento vale só para movimento sem documento de origem. O Commitment é gravado depois da submissão; com resolução no movimento, a verba consumiria um número diferente do que foi aprovado.
O motor trata os quatro sinais do orçamento, e Uncovered bloqueia
POST /evaluate:budget-state devolve quatro sinais — Ok, Warning, Exceeded e Uncovered —, e os quatro são valores do critério estado do orçamento. Uncovered recusa a submissão sempre, independentemente da overBudgetPolicy resolvida no nó: fatia lançada contra dimensão orçamentária inexistente não é estouro tolerável, é ausência de verba, e quem o diz é o Orçamento — o motor obedece, como manda a RN-AP-27.
Sem esta regra, das duas uma: ou o motor deixaria passar fatia contra dimensão inexistente, ou devolveria um erro que nenhum contrato declara. A recusa é repassada do orçamento, porque quem produz o fato é o mesmo módulo — e o código é o BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE, que o Orçamento declara para este sinal, não o BUDGET_EXCEEDED_UNPROCESSABLE. São dois fatos diferentes — um tem tolerância e política, o outro não tem contra o que medir —, e cada um tem o seu código.
O núcleo de controle de cada processo é do produto, e a publicação o verifica
Cada ApprovalProcess declara, do produto e fora do alcance do tenant: se tem valor, se o assunto tem empresa, allowPolicyApproval, allowReturn (falso só no BANK_ACCOUNT_CHANGE), minDistinctApprovers, os tipos de aprovador e os escopos admitidos e, no BANK_ACCOUNT_CHANGE, as travas da RN-AP-61. Os valores estão na tabela Núcleo de controle. GET /approval-processes os devolve; PUT /approval-processes/{id} não os aceita — corpo que tente mudá-los é APPROVAL_PROCESS_CONTROL_LOCKED_CONFLICT · 409. Mudá-los é mudança de spec.
Verificado na publicação de toda versão de política, e listado antes pelo /validate: (a) nível com tipo ou escopo não admitido → APPROVAL_LEVEL_NOT_ADMITTED_UNPROCESSABLE · 422; (b) critério faixa de valor ou activationMinValue em processo sem valor → APPROVAL_POLICY_VALUE_NOT_APPLICABLE_UNPROCESSABLE · 422 — sem valor, a comparação com nulo não tem definição, e deixá-la indefinida é deixar o nível ligar ou desligar ao gosto da implementação; (c) com allowPolicyApproval falso, política cuja cadeia possa terminar sem decisão humana → APPROVAL_POLICY_APPROVAL_NOT_ALLOWED_UNPROCESSABLE · 422; (d) com minDistinctApprovers = k maior que 1, toda política base precisa de um nível sem activationMinValue em Quorum(n) com n ≥ k → APPROVAL_POLICY_DISTINCT_APPROVERS_UNPROCESSABLE · 422. É a única forma de garantir k pessoas na publicação: All depende de quantas o alvo resolve, e dois níveis podem cair na mesma pessoa. Complementares só acrescentam.
Defesa na execução. Na submissão, resolução que terminaria Approved sem pessoa em processo com allowPolicyApproval falso é recusada com o mesmo código de (c); na decisão, a rodada não conclui com menos de minDistinctApprovers pessoas distintas (RN-AP-56).
Caso de ataque. Um administrador troca o Quorum(2) da alteração bancária por Any, ou põe activationMinValue de R$ 0,01 em todos os níveis para a troca de conta terminar aprovada pela política. Agora a publicação recusa as duas versões, por (d) e por (b), antes de existir qualquer instância.
Publicar política exige uma segunda pessoa e deixa o diff
Em todos os processos, quem publica uma versão de política não é quem a criou nem ninguém que a tenha editado em qualquer momento do rascunho. A versão guarda createdByUserId, editedByUserIds — lista que cresce a cada edição do rascunho — e publishedByUserId; POST /approval-policies/{id}/publish por quem está no autor ou na lista é APPROVAL_POLICY_SELF_PUBLISH_FORBIDDEN · 403, verificado na transação da publicação. Desativar política é publicar versão com vigência encerrada, e passa pela mesma regra. As políticas semeadas nascem publicadas pelo produto; a primeira edição do tenant já exige duas pessoas.
A prova e o aviso. Publicar é ato de controle: a versão publicada é imutável (RN-AP-05) e carrega, além de autor, editores e publicador, o diff de critérios e níveis contra a versão que aposentou — gravado na mesma transação, isolado pelo tenant como toda linha de política. Depois do commit sai o evento approval-policy.published, e a controladoria recebe aviso obrigatório com o diff.
Caso de ataque. Um administrador sozinho publica, à noite, a versão que tira a controladoria da faixa acima de R$ 500 mil, aprova o que queria e republica a versão antiga de manhã. Agora a primeira publicação é recusada sem uma segunda pessoa, e cada versão publicada chega à controladoria com o diff.
Cada decisão conta por uma pessoa distinta, e nenhum excluído decide
Invariante do agregado ApprovalRequest, verificada na decisão, dentro da transação da RN-AP-44: (a) num passo, duas aprovações nunca têm o mesmo actingUserId; (b) nenhum actingUserId está na lista de excluídos (RN-AP-60); (c) a rodada não conclui com menos de minDistinctApprovers pessoas distintas, contadas as decisões herdadas (RN-AP-21). Violação de (a) ou (c) é APPROVAL_DISTINCT_APPROVER_UNPROCESSABLE · 422; de (b), APPROVAL_SELF_APPROVAL_UNPROCESSABLE · 422. Conta quem agiu, não o aprovador de direito.
Por que no agregado, e também nas rotas. Delegação, reatribuição e escalonamento trocam quem age em momentos e rotas diferentes; a garantia é uma só, na raiz, e as rotas recusam antes para a pessoa não ver erro na hora de decidir. A delegação (RN-AP-49) só entrega a tarefa a delegado elegível para o passo (RN-AP-59), fora dos excluídos e sem tarefa ou decisão no mesmo passo — senão a tarefa fica com o titular. A reatribuição com destino nessas condições é APPROVAL_REASSIGN_TARGET_UNPROCESSABLE · 422, e o alvo da escalada passa pelas mesmas guardas (RN-AP-50). Criar delegação exige delegado com act:approvals e conta ativa, diferente do delegante (APPROVAL_DELEGATE_NOT_ELIGIBLE_UNPROCESSABLE · 422).
Delegação criada por terceiro. Quem tem create:approval-delegations e cria a delegação de outra pessoa pratica ato de controle: a delegação grava quem a criou, e o delegante recebe aviso obrigatório. Não exige aceite — o aviso é o controle.
Caso de ataque. Os dois membros de um Quorum(2) delegam para a mesma pessoa, e um voto vira dois; a delegação cai no requisitante; um administrador cria a delegação do CFO para si mesmo. Agora a segunda tarefa não vai ao mesmo delegado, e se chegasse a segunda aprovação seria recusada por (a); o requisitante é excluído e nunca recebe; o CFO recebe o aviso com o nome de quem criou.
A dedupe remove a pessoa do passo seguinte, e nunca pula passo que exige mais de uma decisão
Decidida na ativação do passo, pelo actingUserId: quem agiu no degrau imediatamente anterior da mesma rodada (o stage anterior, RN-AP-65) sai das tarefas do passo que ativa. Passo de controle nunca é pulado — toda complementar, o passo de controladoria e o quórum do BANK_ACCOUNT_CHANGE (RN-AP-64): a pessoa sai dele, e os demais decidem. Decisão tomada em passo de controle nunca cobre outro passo: quem decidiu controle sai das tarefas do passo seguinte, de base ou de controle, e o passo continua com os demais, sem ir a Skipped (RN-AP-64). Só a decisão tomada em passo comum cobre passo comum de uma decisão (Any): o passo vai a Skipped com o motivo e approval-step.skipped. Em passo de mais de uma (All, Quorum(n)), a pessoa sai do conjunto, o passo continua com as demais, e a decisão anterior dela não conta para o modo; se sobrarem menos elegíveis do que o modo exige, vale a RN-AP-59. deduplicateConsecutive é do tenant, por processo, e falso e travado no BANK_ACCOUNT_CHANGE.
Caso de ataque. Quem aprova a fatia no degrau anterior está também no papel da complementar de verba estourada, Any: se a dedupe pulasse esse passo, a decisão da base cobriria o controle. E, num quórum antecedido por um passo Any sobre a mesma permissão, pular o quórum fecharia com uma assinatura. E se a decisão de controle cobrisse o passo comum seguinte, num remanejamento acima de R$ 50 mil o CFO que decide a complementar de investimento (stage 15), sendo membro da controladoria, levaria a base Role(Controladoria) · Any (stage 20) a Skipped — uma pessoa fecharia controle e base. Agora a pessoa sai do passo de controle e outra decide, o quórum nunca é pulado, e quem decidiu controle sai do passo seguinte em vez de cobri-lo.
Alçada estourada e reserva exigem a controladoria, ou recusam
authorityExceededBehavior, por processo, tem dois valores: RequireControllerApproval (semente) e Refuse. Vale para dois fatos: a subida que chega à raiz sem cobrir (RN-AP-16) e o passo que cai no aprovador de reserva em processo com valor — o reserva não tem alçada verificada, então aprovar por ele é alçada estourada por definição. Aprovar no topo só com aviso não existe: é controle só detectivo.
RequireControllerApproval. Na ativação do passo marcado, o motor cria no mesmo nível um passo paralelo Role(Controladoria) · Any — o papel funcional que a Notificação semeia no tenant —, com o mesmo escopo e valor, controlOfStepId apontando o passo marcado, e grava approval-step.controller-required. O nível só avança com os dois fechados, e quem decide o de controladoria é pessoa distinta de quem decide o marcado (RN-AP-56). Controladoria sem ninguém elegível não cai no reserva — o reserva não controla o reserva —: o passo fica parado com alerta (RN-AP-59). O simulador mostra o passo extra antes da submissão.
Refuse. Submissão cuja cadeia projetada já estoura ou já cai no reserva é recusada com APPROVAL_AUTHORITY_EXCEEDED_UNPROCESSABLE · 422, nomeando o passo, o valor e o maior limite encontrado. Estouro que só aparece na ativação — o vínculo mudou depois da submissão — não recusa, porque a ativação nunca derruba a decisão de quem acabou de aprovar (RN-AP-45): segue como RequireControllerApproval.
Toda tarefa que vem do reserva, em qualquer processo, tem origin = ByFallback e grava approval-step.resolved-by-fallback. O reserva é pessoa ou grupo (fallbackApproverType = User ou ApprovalGroup), resolvido pessoa a pessoa (RN-AP-59).
Caso de ataque. Num tenant com a árvore incompleta, o gestor da raiz — ou a pessoa única de reserva — aprovava um pedido de R$ 2 milhões sem ninguém com alçada para isso, e a controladoria só sabia pelo aviso. Agora o pedido não conclui sem a decisão de uma pessoa da controladoria, distinta de quem aprovou no topo; no tenant que escolheu Refuse, nem é submetido.
Elegível é quem tem conta ativa, permissão de agir e a empresa do documento — pessoa a pessoa
Toda resolução de tarefa — ativação, delegação, reatribuição, escalonamento e reserva — filtra cada pessoa candidata por quatro condições, lidas no momento: (1) conta ativa no tenant; (2) act:approvals e, em nível PermissionHolder, também a permissão do nível; em passo de controle (RN-AP-64), toda pessoa — o titular resolvido na ativação, quem chega por delegação, reatribuição ou escalonamento e quem entra pelo reserva — precisa pertencer hoje ao próprio alvo do nível: membro vigente do papel ou do grupo, detentor da permissão, a própria pessoa específica ou quem tem o vínculo vigente no nó; alvo de pessoa específica ou de árvore não admite ninguém de fora, e act:approvals sozinho não basta; (3) escopo por empresa que inclui a empresa do documento — por pessoa, nunca por nível; nos processos cujo assunto é cadastro do tenant e não tem empresa (SUPPLIER_REGISTRATION, SUPPLIER, BANK_ACCOUNT_CHANGE, ITEM), a condição não se aplica e vale só a permissão, como em toda leitura de cadastro do tenant; (4) fora da lista de excluídos (RN-AP-60). Quem não passa não recebe tarefa. Em passo de controle, a delegação que falha em (2) deixa a tarefa com o titular; a reatribuição é recusada com APPROVAL_CONTROL_STEP_TARGET_UNPROCESSABLE · 422; a escalada não acrescenta quem está fora do alvo — sem ninguém, segue só o lembrete —; e o reserva só completa o passo com quem pertence hoje ao alvo do nível: quem está fora não entra, e o passo fica parado, como em toda resolução vazia, em vez de ser completado por fora do alvo. Na decisão as quatro são relidas: quem perdeu a elegibilidade depois de receber a tarefa tem a decisão recusada com APPROVAL_APPROVER_NOT_ELIGIBLE_FORBIDDEN · 403 — inclusive o titular de passo de controle que saiu do papel ou do grupo do nível, mesmo mantendo act:approvals; a herança da rodada nova relê o mesmo (RN-AP-21 (d)).
Menos elegíveis que o modo é resolução vazia. Any e All com zero pessoas, Quorum(n) com menos de n. Segue a ordem da resolução: na submissão, reserva e então APPROVAL_NO_APPROVER_UNPROCESSABLE · 422; na ativação, reserva com a marca da RN-AP-45. O reserva completa o conjunto, nunca reduz o quórum. Se nem com ele o modo se completa, o passo fica parado: Active, com awaitingEligibleApprovers verdadeiro, evento approval-step.stalled e alerta ao administrador do tenant e à controladoria. Sai daí por POST /approvals/{id}/steps/{stepId}/resolve, que refaz a resolução depois de o cadastro ser corrigido (APPROVAL_STEP_NOT_STALLED_CONFLICT · 409 se o passo não está parado), ou pelo cancelamento vindo do documento. No BANK_ACCOUNT_CHANGE, que não tem reserva, menos de duas pessoas elegíveis deixa a instância parada já na submissão — não 422, porque a proposta precisa existir para ser vista e contestada.
Dependência de plataforma. A resolução de Role e PermissionHolder precisa perguntar ao GrydAuth "quais usuários ativos deste tenant detêm esta permissão, direta ou por papel", e essa consulta não existe na plataforma. Está na demanda de plataforma como o item P8 — problema, requisito e critério de aceite —; bloqueia a primeira instância com nível PermissionHolder — que é a semente de quatro processos.
Caso de ataque. Usuário desativado, ou sem act:approvals, continuava recebendo e decidindo tarefas porque o papel ainda o listava; num Quorum(2) com uma pessoa só no papel, a regra calava; num grupo multiempresa, um Role com pessoas de escopos diferentes ou não publicava, ou mandava o documento da Beta para quem só enxerga a Alfa. E um membro da controladoria delegava o passo de controle, com delegação de escopo nulo, a um colega com act:approvals que não era do papel — ou o reatribuía a ele. E, se o pertencimento fosse exigido só de quem chega por delegação, o membro retirado da controladoria que mantivesse act:approvals decidiria a tarefa ativada antes da retirada, e a rodada nova herdaria a decisão dele. Agora quem não é elegível não recebe nem decide, o quórum incompleto é completado pelo reserva ou para com alerta, o filtro de empresa é por pessoa, e passo de controle só vai a — e só é decidido por — quem é hoje do alvo do nível.
Os excluídos da instância vêm no contrato de submissão
POST /approvals e a reavaliação recebem excludedUsers: pares (userId, reason) com reason = Requester · PreparedBy · Beneficiary · LastEditor · Proponent · RateOverrider · IdentityCorrectionLinked. O módulo dono é obrigado a mandar todos os papéis que o documento tem: o requisitante sempre; quem preparou "em nome de", quando diferente; o beneficiário, quando o documento declara um; o último editor do assunto; na alteração bancária, o proponente; na correção de identidade de fornecedor aprovado (identityCorrection), quem a pediu, como Requester; na proposta de conta ou de canal de aviso de fornecedor que tenha correção de identidade pedida ou decidida, todos os usuários de cada chave de pessoa ligada, como IdentityCorrectionLinked — a lista é montada por chave de pessoa, nunca por usuário, e vale enquanto o fornecedor existir; quem é ligado, o alcance da ligação e a chave de pessoa são regra do Fornecedor (RN-FOR-41), que esta página cita e não redefine; e, da conversão, os dois autores que a Moeda nomeia — o capturedBy de toda taxa do tenant usada e o overriddenBy de quem registrou o exchangeRateOverride, com taxa informada ou só com o tipo —, com ou sem a flag exchangeRateOverridden (RN-AP-70). Lista sem Requester, sem Proponent no BANK_ACCOUNT_CHANGE, ou com usuário de outro tenant é APPROVAL_EXCLUDED_USERS_UNPROCESSABLE · 422, na submissão. Excluir não afrouxa nem devolve: se, depois da exclusão, não sobram elegíveis para o modo, vale a RN-AP-59 — no BANK_ACCOUNT_CHANGE, que não tem reserva, a instância fica parada com alerta já na submissão, com awaitingEligibleApprovers, aviso ao administrador do tenant e à controladoria, e a proposta não volta a quem propôs (RN-AP-61). A tarefa nunca é atribuída a quem o desfecho recusaria. Flag exchangeRateOverridden sem RateOverrider não é recusa: toda taxa que a levanta tem autor, e a Moeda o informa (RN-AP-70).
A lista vive em ApprovalRequestExcludedUser, filha da instância, e só cresce: cada reavaliação acrescenta o editor, ninguém sai. O motor trata cada excluído como o requisitante da RN-AP-08 — removido antes de qualquer regra, em qualquer passo, delegação, reatribuição, escalonamento e reserva — e a decisão repete a checagem (RN-AP-56).
Conflito de perfis. Cada módulo dono declara a permissão de propor o assunto do seu processo; o /validate acusa, como aviso, papel que concede ao mesmo tempo essa permissão e a permissão do nível. É aviso e não recusa porque a exclusão por pessoa já impede o ato; o aviso existe para a auditoria de perfis.
Caso de ataque. O gerente prepara a requisição "em nome de" um analista e depois a aprova como responsável do centro de custo; ou quem editou o pedido já aprovado, subindo o valor, aprova a reavaliação. E, sem a razão da ligação, quem pediu ou decidiu a correção de identidade do fornecedor recebia a tarefa da proposta de conta — ou de canal — daquele mesmo fornecedor: o desfecho recusava com o código do Fornecedor, a decisão não era gravada, a tarefa continuava ativa e ninguém era avisado — laço, não parada. Agora preparador e último editor estão na lista que o módulo manda, as pessoas ligadas entram por chave de pessoa, e o motor os remove de toda tarefa como remove o requisitante.
BANK_ACCOUNT_CHANGE tem uma definição normativa, e o tenant não a desliga
Processo sem valor. Base semeada: um nível PermissionHolder(approve:supplier-banks) · Quorum(2) · Document. Complementares semeadas, Role(Controladoria) · Any · Document, uma por flag que o Fornecedor calcula: titular ≠ fornecedor — só em fornecedor Foreign, porque nos demais tipos titular diferente é recusado no Fornecedor antes da submissão —, noTrustedPhone, previousNoticeUndeliverable e contestedChannel (tabela de §Processos). Pessoas distintas e diferentes do proponente (RN-AP-56, RN-AP-60). Todo decisor da instância — o quórum e as complementares, a da controladoria inclusive — precisa de identidade verificada e é comparado por CPF: os CPFs são distintos entre si e diferentes do CPF do proponente e das pessoas ligadas ao número do callback; e quem está ligado, sem prazo, a uma correção de identidade daquele fornecedor não decide — nem em proposta de conta, nem em proposta de canal de aviso: o Fornecedor manda, na submissão, todos os usuários de cada chave de pessoa ligada em excludedUsers, com reason = IdentityCorrectionLinked (RN-AP-60), e o motor os remove de toda tarefa antes de qualquer outra regra. As regras são do Fornecedor (RN-FOR-36 e RN-FOR-41), que o Fluxo cita e não redefine, e o desfecho as confere. Travado pelo produto: sem reserva, sem devolução, deduplicateConsecutive, allowReassign, allowEscalation e delegable falsos — PUT que tente mudar é APPROVAL_PROCESS_CONTROL_LOCKED_CONFLICT · 409. Verificado na publicação: o nível Quorum(n) da base é sempre PermissionHolder(approve:supplier-banks) — Role, ApprovalGroup e SpecificUser na base são APPROVAL_LEVEL_NOT_ADMITTED_UNPROCESSABLE · 422, porque trocar o alvo por um papel que dois administradores montam é esvaziar o quórum sem mexer no número —; reserva na política é APPROVAL_POLICY_FALLBACK_NOT_ALLOWED_UNPROCESSABLE · 422; e as quatro complementares semeadas pelo produto não são aposentadas nem alteradas: APPROVAL_POLICY_SEEDED_CONTROL_LOCKED_CONFLICT · 409. O tenant pode acrescentar complementares (Tesouraria para chave PIX não verificada) e endurecer a base (Quorum(3)); nunca publicar base sem Quorum(n) com n ≥ 2 (RN-AP-54). O Fornecedor cita esta definição e não a redeclara.
Janela de aviso. A submissão chega com activationNotBefore = envio do aviso + janela mínima, calculado pelo Fornecedor (RN-FOR-23), e nenhuma tarefa ativa antes (RN-AP-63). O aviso automático sai só por e-mail — não há SMS nem WhatsApp como canal —, e e-mail devolvido não inicia a janela: enquanto nenhum passo ativou, o Fornecedor adia a data pela rota da RN-AP-63; se a devolução chega com as tarefas já ativas, a guarda é do desfecho do Fornecedor.
Proposta imutável, sem devolução. O BANK_ACCOUNT_CHANGE só aprova ou rejeita: return é APPROVAL_RETURN_NOT_ALLOWED_UNPROCESSABLE · 422, não há reatribuição, e os dados da proposta não mudam depois de submetida. Corrigir é rejeitar e abrir proposta nova — instância nova, aviso novo, callback novo, carência nova. Por isso não há rodada nova nem herança de decisão neste processo.
Callback obrigatório, salvo noTrustedPhone — e aí controladoria e comprovante no lugar dele. A evidência é do Fornecedor, que é quem sabe de onde veio o telefone e quem pode registrar a ligação: telefone confiável, separação de pessoas e amarração ao hash dos dados da proposta são a regra dele (RN-FOR-29), e o Fluxo só a cita; na proposta de conta, a titularidade da conta nova é comprovada antes da submissão (RN-FOR-35). A tarefa mostra a evidência ao lado do diff. O motor não a guarda nem a redeclara: a decisão que concluiria a instância invoca o contrato de desfecho, e sem callback Confirmed o Fornecedor devolve Refused(BANK_CHANGE_CALLBACK_REQUIRED_CONFLICT) — a decisão não é gravada e a tarefa continua ativa. Callback Denied é contestação (RN-FOR-30). A exceção tem nome: noTrustedPhone, e vale nos dois casos do processo. Quando o Fornecedor a calcula na submissão, o callback não é exigido e a complementar semeada põe a controladoria na cadeia, sempre com o comprovante que o Fornecedor exige antes da submissão: na proposta de conta, a titularidade da conta nova (RN-FOR-35); na mudança de contato de aviso ou da cadeia, o anexo do tipo declarado que o Fornecedor exige na RN-FOR-40 — o pedido de troca assinado pelo fornecedor, existente, com veredito do antivírus Available — e a carência de 7 dias antes de o contato novo valer (RN-FOR-34). O que o Fornecedor exige é o anexo e o rastro, não a conferência do documento: não há exigência de ICP-Brasil nem de firma reconhecida, e Foreign segue exatamente este caminho. Nunca controladoria sem comprovante, nem comprovante sem controladoria. Com previousNoticeUndeliverable, o callback volta a ser exigido sem essa alternativa, e a controladoria também entra (RN-FOR-23). Com contestedChannel — o Fornecedor travou as propostas de conta por contestação de mudança de contato já aplicada (RN-FOR-38) —, a troca de canal que desbloqueia, e também a mudança de contato da cadeia enquanto a trava durar, abre esta instância com a controladoria na cadeia; o desfecho que a aplica limpa a trava. Consentimento do contato não é condição: é prevenção a fraude.
Contestação. O contato anterior contesta pelo canal que o Fornecedor oferece — link de uso único, sem login. Com a instância em aprovação, o Fornecedor chama POST /approvals/{id}/cancel, que vale em InApproval: a instância termina Cancelled com o motivo da contestação, o desfecho não aplica nada, e trocar a conta exige proposta nova. Depois de aprovada, a contestação segue a regra única do Fornecedor (RN-FOR-30), que vale até 30 dias depois de usableFrom. Quando a proposta contestada trazia contestedChannel ou o anexo do pedido de troca de canal, a contestação suspende o efeito e o caso vai ao complementar da controladoria, que decide com a evidência — não reverte sozinha ao valor anterior —, e a instância chega ao motor com a flag; e a contestação feita por dentro do Nexio, por quem tem read:supplier-banks, exige segunda pessoa, como todo ato de controle. As duas regras são do Fornecedor, e esta página as cita.
Caso de ataque. Se o processo avaliasse valor, todo fornecedor sem compras avaliaria zero, a política por faixa nunca casaria e a troca cairia num Any de uma assinatura; com dedupe, o quórum seria pulado; um administrador trocaria o modo; o aviso iria ao e-mail que a fraude pressupõe comprometido, e o fornecedor legítimo não teria por onde dizer "não fui eu". Aqui não há faixa a contornar, o quórum de duas pessoas é verificado na publicação e na decisão, a configuração está travada, a troca não conclui sem callback confirmado para telefone confiável — ou, com noTrustedPhone, sem controladoria e sem o anexo comprobatório que o Fornecedor exige e grava —, a contestação encerra a instância, e a troca de canal que desbloqueia fornecedor contestado passa pela controladoria.
Valor indisponível não casa faixa e não desliga nível
O módulo dono pode devolver o valor avaliado como indisponível — cálculo que depende de módulo que ainda não existe, fornecedor sem histórico —, e nunca como zero. A instância grava valueUnavailable e valor nulo. Na submissão: critério faixa de valor não casa; activationMinValue não pula o nível — o nível entra; e cadeia que precisa comparar alçada ou materialidade é recusada com APPROVAL_VALUE_UNAVAILABLE_UNPROCESSABLE · 422, porque não há com o que comparar.
Caso de ataque. "Fornecedor novo avalia zero" fazia de zero um valor legítimo, e activationMinValue de R$ 0,01 desligava todos os níveis. Agora indisponível não é zero, não casa faixa e não desliga nível: o erro de cálculo cai sempre para o lado de mais controle.
activationNotBefore adia a ativação, e só anda para frente
POST /approvals aceita activationNotBefore ou activationHeld. Com data, nenhum passo ativa antes dela: o mesmo relógio do motor que vence SLA ativa o primeiro passo na hora, como ação de sistema (actorUserId nulo), com o tenant gravado no próprio agendamento. Com activationHeld, a instância espera o módulo dono informar a data. POST /approvals/{id}/activation-window (interna) fixa ou adia a data; antecipar, ou mexer depois que um passo ativou, é APPROVAL_ACTIVATION_WINDOW_UNPROCESSABLE · 422. Antes da ativação não há tarefa, logo não há decisão possível. O SLA do primeiro passo conta da ativação. Consumidor na v1: a alteração bancária (RN-AP-61).
Caso de ataque. A janela mínima entre o aviso ao fornecedor e a decisão pedia uma ativação adiada que o motor não tinha: disparar o aviso e aprovar no mesmo minuto só documentava a fraude. Agora não existe tarefa antes da data, e a data só anda para frente.
Passo de controle se define pela posição na cadeia, e quem decide outro passo da rodada não o decide
Passo de controle é, e só é: (a) todo passo de nível de política complementar; (b) o passo de controladoria criado pela RN-AP-58 (controlOfStepId preenchido); (c) o passo Quorum da base do BANK_ACCOUNT_CHANGE (RN-AP-61). Nunca se reconhece pelo nome do papel: um nível Role(Controladoria) numa política base é passo comum, e uma complementar SpecificUser(CISO) é passo de controle. A condição é derivada na criação do passo, da política e do processo congelados (RN-AP-06) — nenhuma configuração do tenant a liga ou desliga. O passo marcado authorityExceeded não é passo de controle: o controle dele é o passo de controladoria, que nasce na ativação do marcado mesmo que a dedupe o leve a Skipped.
Invariante do agregado, verificada na decisão, dentro da transação da RN-AP-44: a mesma pessoa não decide um passo de controle e qualquer outro passo da rodada corrente — da base ou de controle —, contadas as decisões herdadas, em qualquer ordem. Violação é APPROVAL_CONTROL_STEP_SAME_PERSON_FORBIDDEN · 403.
Na ativação, quem já decidiu passo da rodada sai das tarefas do passo de controle, e quem já decidiu passo de controle sai das tarefas de todo outro passo — de base ou de controle —, que continua com os demais e nunca vai a Skipped por essa decisão; a dedupe nunca pula passo de controle nem conta decisão de controle como cobertura (RN-AP-57); delegação, reatribuição e escalonamento de passo de controle só alcançam quem pertence ao alvo do nível e não decidiu outro passo da rodada, e o próprio titular só decide enquanto pertencer a ele (RN-AP-59 (2)). Se ninguém sobra, vale a RN-AP-59 — o passo para com alerta, e o reserva nunca controla o que ele mesmo produziu (RN-AP-58). Consequência aceita: tenant pequeno precisa de mais uma pessoa para cada controle.
Caso de ataque. Se o controle fosse reconhecido pelo nome do papel, a complementar "estado do orçamento = Exceeded" com nível Role(Controller) · Any não seria controle; o Owner que aprovou a fatia no degrau anterior, sendo também desse papel, teria o passo levado a Skipped pela dedupe — quem aprovou a base passaria pelo controle da verba. E a gestora da raiz, sendo da controladoria, aprovaria o passo de controladoria que a própria marca criou. E, se a regra valesse só num sentido, quem decidiu a complementar poderia ter a decisão contada como cobertura do passo comum seguinte. Agora toda complementar é passo de controle, seja qual for o papel, e vale nos dois sentidos: a pessoa sai das tarefas dele, a segunda decisão da mesma pessoa é recusada com 403, e o controle só fecha com outra pessoa.
A cadeia se ordena por stage, na mesma régua para base e complementares
Todo ApprovalLevel tem stage, obrigatório, e dentro da política ele cresce estritamente com order — verificado na publicação (APPROVAL_LEVEL_STAGE_UNPROCESSABLE · 422). Na resolução, os níveis da base vencedora e das complementares que casaram são ordenados por stage; níveis com o mesmo stage formam um degrau: seus passos ativam juntos, em paralelo, e o degrau seguinte só ativa quando todos fecham. priority decide só qual base vence; isDefault só responde quando nenhuma outra base casa. "Passo anterior", para a dedupe, é o degrau anterior.
Caso de ataque. Com a posição dada pela prioridade da complementar e o order local da base, nada dizia se o controller do estouro de verba vinha antes ou depois do comitê, nem o que fazer com duas complementares de mesma prioridade — cada implementação intercalaria de um jeito, e a dedupe pularia degraus diferentes. Agora a ordem é um número publicado e o empate é paralelo, por regra.
Critério de linha casa na mesma linha, e critério de fatia na mesma fatia
Os critérios têm alcance de documento, de linha ou de fatia (ver Critérios disponíveis). A política casa quando o documento satisfaz os de documento e existe uma linha que satisfaz sozinha todos os de linha — e, havendo critério de fatia, uma fatia dessa linha que satisfaz todos. O escopo Line gera passo para cada linha que satisfaz os critérios de linha da política de origem do nível. Verificado na submissão, na reavaliação e na simulação; não tem erro próprio.
Caso de ataque. Uma linha de software sem dado pessoal somada a uma de hardware com dado pessoal acionava "software com dado pessoal", e na escolha da base podia vencer uma de menos controle só porque critérios de linhas diferentes se juntaram. Agora cada linha responde por si.
A trilha prova com base em quê se decidiu
Toda decisão grava subjectVersion e subjectHash do conteúdo aprovável do documento, e no metadata os anexos vistos (fileId e sha256) e, onde a separação é por pessoa — passo de controle, BANK_ACCOUNT_CHANGE e correção de identidade do fornecedor —, o CPF verificado de quem decidiu e o estado da identidade no momento da decisão, para que o desfecho compare pessoas sem depender de o CPF ainda estar lá quando ele conferir; que o CPF verificado não mude depois de gravado é requisito da demanda de plataforma (P17). A abertura de cada rodada grava o mesmo par; approval-step.activated grava as pessoas resolvidas e a origem de cada uma. O módulo dono calcula e manda versão e hash em POST /approvals, no reenvio e na reavaliação — submissão sem eles é APPROVAL_SUBJECT_HASH_REQUIRED_UNPROCESSABLE · 422. Revogar anexo depois não muda o que foi visto: o hash fica.
Caso de ataque. Sem versão nem hash do assunto, não se provava qual versão do documento foi aprovada, e sem as pessoas resolvidas a reexecução "exata" dependia de papéis que o GrydAuth não versiona. Agora a trilha responde "esta versão, com estes anexos, por quem estava habilitado naquele dia".
Pedido que nasce de requisição aprovada dispensa só a base, e a soma dos pedidos não passa do aprovado
Quando o PURCHASE_ORDER cita a instância aprovada da REQUISITION de origem, os níveis da política base nascem Skipped, com o motivo e a instância de origem no evento, só se as duas condições valem: (a) centro de custo, conta contábil e categoria de cada fatia são os aprovados; (b) a soma das fatias de todos os pedidos que citam a instância — os já submetidos, mais este —, agregada por (centro de custo, conta contábil, categoria), é menor ou igual ao aprovado para a mesma tupla, na moeda funcional em que a alçada foi avaliada. Cada pedido entra pelo valor das fatias menos o cancelado: o rejeitado não entra; o cancelado, total ou parcialmente, fica na soma pela parte que não foi cancelada — a recebida ou aceita antes do cancelamento — e nunca sai inteiro. As complementares rodam sempre: fornecedor sancionado, UF de entrega e estado do orçamento só existem de verdade no pedido. Fora dessas condições, a cadeia roda inteira. Pedido que cita mais de uma requisição, ou cuja instância de origem não está mais em Approved — requisição reaberta, por exemplo —, roda a cadeia inteira; a dispensa para N origens é decisão da spec do Pedido.
Onde e sob qual trava. Verificado na submissão, na reavaliação e no reenvio do pedido, sempre com o valor novo das fatias e sob a trava da instância de origem: cada um desses atos incrementa a version da ApprovalRequest da requisição na mesma transação (RN-AP-44), de modo que dois atos concorrentes não leem a mesma soma — o segundo relê e soma o primeiro. Se a soma estoura, a dispensa cai: a base volta a ser exigida, a cadeia recalculada deixa de ser subconjunto da anterior e a rodada recomeça do zero (RN-AP-21). O pedido que não mudou continua com a dispensa que já tinha; quem estoura a soma é o pedido que mudou. A soma pressupõe que o valor de um pedido nunca fica abaixo do recebido ou aceito nele — requisito da spec do Pedido. Só o pedido rejeitado sai inteiro da soma; cancelar libera só a parte cancelada, e o pedido seguinte cabe só nela. Os campos do contrato — entre eles o valor cancelado por fatia — nascem com a spec do Pedido.
Caso de ataque. "Não precisar de nova cadeia" deixava um pedido para fornecedor sancionado, ou com a UF de entrega trocada, passar sem o compliance e sem o fiscal. E, se cada pedido fosse comparado sozinho com o aprovado, dois pedidos de R$ 100 mil citando a mesma requisição de R$ 100 mil dispensariam a base duas vezes — R$ 200 mil com uma alçada de R$ 100 mil. E, se o pedido cancelado saísse inteiro da soma, na mesma requisição um primeiro pedido de R$ 100 mil recebido em R$ 60 mil e cancelado no resto deixaria o segundo, de R$ 100 mil, somar R$ 100 mil e dispensar a base de novo — R$ 160 mil comprados com uma alçada de R$ 100 mil. Com a parte recebida na soma, o segundo pedido de R$ 100 mil soma R$ 160 mil e roda a cadeia inteira, e só um segundo pedido de até R$ 40 mil dispensa. E, se a soma fosse conferida só na submissão, na mesma requisição de R$ 100 mil dois pedidos de R$ 50 mil com a base dispensada deixariam o primeiro ser editado para R$ 100 mil e reavaliado ainda sem a base — R$ 150 mil com uma alçada de R$ 100 mil. Com a soma de novo na reavaliação, o pedido editado soma R$ 150 mil e roda a cadeia inteira. Agora só a alçada já assinada é dispensada, só até o valor que ela assinou, e só se nada que ela leu mudou.
Configuração crítica do motor muda como a política: quem propõe não efetiva
São parâmetros críticos, e só estes: em ApprovalSettings, allocationMaterialityPercent e allocationMaterialityAmount; em ApprovalProcessSettings, fallbackApproverType · fallbackApproverRef, authorityExceededBehavior, deduplicateConsecutive e delegable; os membros — entrada, saída e vigência — de ApprovalGroup usado como reserva de processo ou de política, ou referenciado por nível de controle em política ativa ou na cadeia congelada de instância viva (RN-AP-64: nível de complementar, passo de controladoria, quórum do BANK_ACCOUNT_CHANGE); e os membros do papel funcional Controladoria e de todo papel referenciado por nível de controle em política ativa ou na cadeia congelada de instância viva. Instância viva, aqui, é a que está em InApproval: ela fica na versão de política que a formou (RN-AP-05) e lê os membros quando o passo ativa e de novo na decisão (RN-AP-07, RN-AP-59), então o grupo ou papel que só uma versão aposentada usa continua decidindo controle até a última instância dessa versão terminar. O reserva declarado na política já segue a RN-AP-55. A condição do grupo é lida na transação da mudança, contra as políticas publicadas e as cadeias congeladas das instâncias em InApproval, sob a trava por (tenant, processo) de todo processo com versão de política — ativa ou aposentada — que referencie o grupo, a mesma da publicação (princípio 3); a submissão toma essa trava em modo compartilhado, para que nenhuma instância se forme com uma versão que a mudança não viu. Grupo que passa a ser referenciado por nível de controle entra na regra pela publicação da política, que já exige segunda pessoa, e o diff dela mostra os membros vigentes do grupo (RN-AP-55).
A mecânica. PUT /approval-settings, PUT /approval-processes/{id} e a edição de grupo não alteram parâmetro crítico: corpo que muda um deles é APPROVAL_SETTINGS_CRITICAL_FIELD_CONFLICT · 409, e as travas do BANK_ACCOUNT_CHANGE continuam respondendo antes (RN-AP-54). A mudança nasce como ApprovalSettingsChange Pending pela rota de proposta do alvo, com o diff campo a campo — valor vigente e valor novo — e proposedByUserId; a proposta nunca é editada: mudar é descartar e propor de novo. Efetiva por …/apply, que grava o valor novo na mesma transação, por pessoa com a permissão do alvo, diferente de quem propôs e fora de quem a mudança beneficia — os membros que ela inclui num grupo ou cuja vigência prorroga, e o usuário, ou os membros do grupo, que ela põe como reserva. Qualquer dos dois casos é APPROVAL_SETTINGS_CHANGE_SELF_APPLY_FORBIDDEN · 403, e nada é gravado. A remoção tem guarda própria. Quem propõe e quem efetiva a saída de membro — ou o encerramento da vigência dele — de grupo ou papel referenciado por nível de controle, ou de grupo usado como reserva de processo ou de política, não pode ser membro remanescente desse grupo ou papel depois da mudança: é APPROVAL_SETTINGS_CHANGE_CONTROL_MEMBER_REMOVAL_FORBIDDEN · 403, conferido na proposta e de novo na efetivação, e nada é gravado. E a redundância tem piso. A remoção que deixa menos de dois membros vigentes em grupo referenciado por nível de controle cujo modo é Any responde o mesmo 403, seja quem for quem propõe e quem efetiva: é a redundância que a regra protege, e um Any de uma pessoa só fecha o controle com uma assinatura. O piso é do grupo, não da reserva declarada como pessoa (fallbackApproverType = User), que é de uma só por desenho. Onde a comparação desta cláusula é por CPF, esta também é. Sem ela, a guarda do beneficiário não alcança quem esvazia o grupo de fora: quem remove não é incluído, prorrogado nem posto como reserva, e duas pessoas de fora do grupo tiram os demais e deixam um membro único decidindo — ou um membro efetiva a proposta do colega que tira os outros e fica sozinho no Any do controle. Onde há nível de controle, a comparação é por pessoa, não por usuário. Onde a mudança toca grupo ou papel referenciado por nível de controle (RN-AP-64), quem propõe, quem efetiva e quem a mudança inclui num grupo ou cuja vigência prorroga precisam de identidade verificada, com CPF único por tenant — a mesma do Fornecedor (RN-FOR-36), que esta página cita e não redefine —; sem ela, a proposta e a efetivação respondem APPROVAL_SETTINGS_CHANGE_IDENTITY_NOT_VERIFIED_CONFLICT · 409, e nada é gravado. E "quem propõe ≠ quem efetiva ≠ quem é incluído" é conferido por CPF, nunca por userId: efetivar com o CPF de quem propôs, ou com o CPF de quem a mudança inclui ou prorroga, é o mesmo APPROVAL_SETTINGS_CHANGE_SELF_APPLY_FORBIDDEN · 403. O alcance da identidade verificada — membro de grupo e de papel de controle, e quem efetiva — é requisito da demanda de plataforma (P17). Fora daí, a comparação é por userId. Nos demais parâmetros críticos — allocationMaterialityPercent e allocationMaterialityAmount, fallbackApproverType e fallbackApproverRef, authorityExceededBehavior, deduplicateConsecutive e delegable —, "quem propõe ≠ quem efetiva" é conferido por userId e não se exige identidade verificada. Risco aceito e registrado: a mesma pessoa, com dois usuários, propõe e efetiva mudança de materialidade ou de alçada. O que fica contra ele: a dupla custódia por usuário, o ato registrado com quem propôs, quem efetivou e o diff campo a campo, o aviso obrigatório à controladoria depois do commit (approval-settings-change.applied) e o fato de a mudança não alcançar instância aberta — o que a cadeia congelou não muda (RN-AP-06). Não se promete mais do que isso. A dupla custódia da eliminação de anexo, que é do Anexo, continua sendo por usuário, e esta regra não a alcança. Se a proposta não está Pending ou o valor vigente não é mais o "antes" dela, APPROVAL_SETTINGS_CHANGE_STALE_CONFLICT · 409. …/discard encerra sem efeito. O que a cadeia congelou não muda (RN-AP-06); os membros do grupo, que a ativação e a decisão leem, mudam — e é por isso que o grupo da cadeia congelada está na regra.
A prova e o aviso. É ato de controle: a ApprovalSettingsChange guarda alvo, diff, quem propôs, quem efetivou ou descartou e quando, isolada pelo tenant, e não sofre escrita além da transição de status (Pending · Applied · Discarded). Depois do commit sai approval-settings-change.applied, e a controladoria recebe aviso obrigatório com o diff.
Os membros de papel vivem no GrydAuth. O Nexio não intercepta concessão de papel: para o papel Controladoria e todo papel referenciado por nível de controle em política ativa ou na cadeia congelada de instância viva, o evento de concessão e retirada com quem concedeu (P16) e a segunda pessoa, com recusa de conceder a si mesmo (P-T5-4), são requisito da demanda de plataforma. Quando o evento existir, o Nexio o consome e avisa a controladoria; até lá, a regra está declarada e não é verificável pelo motor, e isso bloqueia o primeiro tenant em produção.
Caso de ataque. Com a política sob duas pessoas, o mesmo administrador subia a materialidade para 40% e a fatia que exigiria o diretor era absorvida pela maior; ou apontava a si mesmo como reserva do processo, ou se incluía no grupo de reserva ou no grupo que decide a complementar de fornecedor sancionado, ou ligava a delegação no processo que a controladoria tinha fechado — o controle da RN-AP-55 contornado pela configuração ao lado, sem diff e sem aviso. Agora cada um desses parâmetros só muda com uma segunda pessoa e chega à controladoria com o diff. Duas brechas vizinhas fechariam o controle por outro lado: a versão nova da complementar de fornecedor sancionado troca o grupo por um Role, o administrador se inclui no grupo antigo — que nenhuma política ativa usa mais — e decide o controle do pedido formado na versão anterior; ou A propõe incluir C no grupo de controle, e o próprio C efetiva. Com o grupo da cadeia congelada na regra enquanto houver instância em InApproval, e com quem é incluído fora de quem efetiva, as duas pedem uma terceira pessoa. E a mesma porta existia pela reserva: com A, B e C no grupo de reserva do processo, D propunha tirar B e C e E efetivava — nenhum dos dois membro, nenhum dos dois incluído ou prorrogado —, e A virava aprovador de reserva único de todo passo parado. Agora o grupo de reserva está na cláusula da remoção, e o grupo de controle em Any não desce de dois membros, seja quem for quem propõe e quem efetiva. E, enquanto a comparação era por userId, C efetivava a própria inclusão pelo segundo usuário dele — a mesma pessoa, dois logins: por CPF verificado os dois são um só, e a efetivação é recusada.
Taxa informada no documento aciona a controladoria
Nos processos com valor, o tipo de taxa da conversão vem da configuração por processo — processRateTypes, uma entrada por processo com valor, na Moeda, que é a página dona do nome; o defaultRateType vale só para o movimento sem documento de origem —; quem pede, prepara ou submete não o escolhe. Documento convertido com exchangeRateOverride — taxa informada, ou de outro tipo, por quem tem a permissão de informar taxa no documento —, ou cuja taxa usada é do tenant e tem como autor (capturedBy) alguém entre os demais excluídos da RN-AP-60 ou quem submete, chega com a flag exchangeRateOverridden — derivada pela Moeda e calculada pelo módulo dono na submissão e na reavaliação, nunca declarada por quem pede. E o autor de toda taxa do tenant usada, com ou sem a flag, entra em excludedUsers com a razão RateOverrider, como quem informou a taxa no documento (RN-AP-60). As duas regras são da Moeda (RN-CUR-27); o Fluxo as cita com esses nomes. A complementar semeada Role(Controladoria) · Any · Document casa pela flag, é passo de controle (RN-AP-64) e é travada (APPROVAL_POLICY_SEEDED_CONTROL_LOCKED_CONFLICT · 409): decide pessoa distinta do requisitante e do autor da taxa; e o autor de taxa do tenant usada não decide nenhum passo da instância, com ou sem a complementar. A flag nunca chega sem a razão na lista. Mesmo no exchangeRateOverride só de tipo — sem rate informada — há RateOverrider: a Moeda preenche o capturedBy com o autor da linha de taxa efetivamente usada e o overriddenBy com quem registrou o override, e os dois entram em excludedUsers com essa razão — é o que impede que quem acionou a complementar seja quem a decide. Por isso flag sem RateOverrider não é recusa na submissão (RN-AP-60). Na submissão e na reavaliação — que é onde a flag pode nascer de novo — sai nexio.approval-request.exchange-rate-overridden à controladoria. A flag está no retrato (RN-AP-21): informar ou trocar a taxa depois de aprovado abre rodada nova. Que a taxa informada nunca vira taxa do tenant é regra da Moeda.
Caso de ataque. A taxa decide a alçada: num pedido de US$ 100 mil, o comprador informa 4,20 em vez de 5,40, o valor convertido cai de R$ 540 mil para R$ 420 mil, abaixo da faixa do diretor, e ninguém revê a taxa. E, se só a taxa informada contasse, o comprador gravaria antes uma taxa Manual de 4,20 no tenant e submeteria com ela; se escolhesse o tipo, submeteria com uma Contracted antiga, gravada para outro fim; e, se a exclusão olhasse só quem move o documento, um aprovador com create:exchange-rates gravaria a taxa baixa e decidiria o pedido do colega que caiu na alçada dele. Agora o tipo é da configuração, a taxa informada ou de outro tipo — ou a do tenant gravada por quem move o documento — põe a controladoria na cadeia, e o autor de toda taxa usada não decide nenhum passo.
Conclusão sem decisão humana e ajuste do aprovado passam pelo desfecho, e o Refused recusa o ato
Há dois caminhos em que nenhuma pessoa decide, e nos dois o contrato de desfecho do ApprovalProcessCode é invocado na transação do ato que os provoca, como na decisão humana (RN-AP-38).
Aprovação pela política (RN-AP-32), na transação do POST /approvals. Applied: a instância nasce Approved, grava approval-request.approved-by-policy e o módulo dono aplica o efeito — a requisição grava o Commitment. Refused(code): o envio é recusado — a instância não nasce, nada é gravado, e o POST /approvals responde com o código do módulo, sem reembrulho; o documento continua em rascunho para quem enviou corrigir e enviar de novo. Não há tarefa a manter ativa, porque não há aprovador, e Returned não serve: diria que alguém devolveu.
Ajuste do documento aprovado. A alteração em que valem as condições (a), (b) e (c) da RN-AP-21 — valor do documento, de cada fatia e de cada linha que não subiu, retrato de critérios igual (o criteriaSnapshotHash não mudou) e cadeia recalculada igual ou contida — não reabre: na transação do POST /approvals/{id}/revaluate a instância continua Approved, sem rodada nova, sem tarefa e sem mexer no roundNumber, grava approval-request.revaluated com o que mudou e a subjectVersion/subjectHash novas, e invoca o contrato de desfecho como ajuste. Applied: o módulo dono aplica o efeito da diferença — a requisição grava no razão só a diferença da parte ainda não pedida (Orçamento, RN-O45). Refused(code): a alteração é recusada — nada é gravado, o documento continua na versão aprovada, a tentativa fica registrada em transação própria como approval-request.decision-refused, o revaluate responde com o código do módulo, e requisitante e dono do impedimento são avisados. Alteração de taxa — incluir, trocar ou remover o exchangeRateOverride, ou trocar a moeda do documento — nunca é ajuste (RN-AP-70). O teste de ajuste roda pela taxa já gravada no documento, e o ajuste grava a diferença por ela; só a rodada do zero, já decidida, reconverte o documento com a data do ato (Moeda e Câmbio, RN-CUR-22) — assim o câmbio do dia nunca faz de uma reabertura um ajuste. Quem reconverte é o módulo dono, no desfecho da reabertura, que devolve a conversão nova ao motor (RN-AP-72). No BANK_ACCOUNT_CHANGE o revaluate continua recusado (RN-AP-61). Fora de (a), (b) e (c), a alteração reabre (RN-AP-43) e a rodada é do zero, com as tarefas criadas como na submissão; a última aprovação humana chama o desfecho, que grava a diferença.
Failed, nos dois caminhos: rollback total, como na RN-AP-38. No ajuste, quem decidiu continua valendo mesmo que hoje não passasse na RN-AP-59: a assinatura era válida quando foi dada, e o ajuste não pede assinatura nova.
Por quê: sem esta regra, a aprovação pela política terminava Approved sem desfecho — requisição aprovada sem Commitment —, e a alteração em que todos os passos herdavam deixava a instância em InApproval sem tarefa e sem transição que a levasse a Approved, com a diferença de verba sem ninguém para gravá-la. Nos dois caminhos quem age é o requisitante, e é a ele que a recusa volta: é quem pode corrigir a data, pedir remanejamento ou esperar a verba.
O corpo de submissão é um contrato fechado: o dono monta e converte, o motor confere e não adivinha
POST /approvals, :simulate, :preview, /resubmit e /revaluate recebem o corpo de §Contrato de submissão — envelope, linhas, fatias, conversão, flags e excluídos. O módulo dono monta o corpo e converte pela Moeda (Moeda e Câmbio, RN-CUR-22); o motor não lê tabela do dono para casar critério. É recusado com APPROVAL_SUBMISSION_CONTRACT_UNPROCESSABLE · 422, nomeando o campo e a linha: campo que o processo exige ausente, ou que ele não admite presente; lineId ou allocationId repetido; soma das fatias diferente da linha, na moeda do documento ou na funcional; conversion ausente com moeda diferente da funcional, ou presente com a mesma; empresa que não é a do estabelecimento; flag fora das que o processo declara; valor negativo. Verificado na submissão, no reenvio, na reavaliação, na simulação e na prévia.
O que o motor calcula, ele não recebe: o path dos nós, resolvido pelo contrato de cada dono e congelado no retrato; o valor avaliado dos processos com linhas, soma dos functionalAmount; o limiar de materialidade (RN-AP-15); o sinal de orçamento de cada fatia (RN-AP-27); e a cadeia.
O retrato e o hash. O criteriaSnapshot é o corpo sem os excluídos, a versão, o hash do assunto e a ativação, com os path resolvidos e o sinal de orçamento de cada fatia. O criteriaSnapshotHash cobre os atributos — empresa, estabelecimento, moeda, flags do assunto e, por linha e por fatia, tudo o que os critérios leem, com o sinal de orçamento — e não cobre valores nem a taxa: valor é a condição (a) da RN-AP-21, e atributo é a (b). Com o valor dentro do hash, toda redução de valor mudaria o retrato, e o ajuste da RN-AP-71 só existiria para edição que não mexe em número. Linha ou fatia que entra ou sai muda o hash. Linhas ordenadas por lineId e fatias por allocationId antes de calcular: a ordem de envio não muda o hash.
Reabertura e taxa. No revaluate o corpo vem pela taxa gravada: conversion tem de ser a da instância, e o teste de ajuste roda sobre ele (RN-AP-71). Se o motor decide reabrir, quem reconverte é o dono, no desfecho da reabertura (RN-AP-43), com a data do ato: o desfecho devolve Applied com a conversão nova — conversion, o functionalAmount de cada linha e de cada fatia, a flag exchangeRateOverridden e os RateOverrider dela —, e a rodada do zero é montada com esses números. Sem cotação, o desfecho devolve Refused(EXCHANGE_RATE_MISSING_UNPROCESSABLE), o revaluate é recusado e o documento fica onde estava. O reenvio, que é sempre rodada nova, já chega reconvertido. O que a reconversão grava no razão é do Orçamento (RN-O59).
Uma instância viva por assunto e processo. POST /approvals para assunto com instância InApproval ou Returned no mesmo processo — ou Approved, fora da correção de identidade de fornecedor aprovado — é APPROVAL_REQUEST_ALREADY_EXISTS · 409: o aprovado muda por revaluate, e o devolvido volta por resubmit.
Caso de ataque. Sem esquema, a requisição mandaria o total, e a cadeia sairia pelo total — o pedido rateado 90/10 exigiria o diretor da área que ficou com R$ 40 mil —; mandaria categoria e flags juntadas no documento, e "software com dado pessoal" casaria somando duas linhas (RN-AP-66); converteria o total em vez das fatias e criaria o segundo resíduo que a convenção de dinheiro proíbe; e mandaria a taxa do dia na reavaliação, e o câmbio faria de uma reabertura um ajuste. Agora cada linha e cada fatia chegam com a sua identidade e os seus atributos, a conversão é a do documento, e o que o corpo não diz o motor recusa.
Limites
Fora de escopo
- v2 ·
Panel— uma decisão de cada alvo (área) no mesmo nível. A semântica de rejeição, escalonamento e dedupe por alvo não estava fechada; na v1 as áreas decidem em níveis sequenciais. - v2 ·
CostCenterOwnerAtDistance(n)— o responsávelnníveis acima do nó da fatia, para exigir as assinaturas intermediárias. Na v1 a subida gera um passo só, no nó que cobre. - v2 · critérios faixa de risco do fornecedor e desvio do menor preço — o primeiro lê
riskTierda homologação, o segundo precisa da Cotação, que não tem spec. Entram com a Cotação. - v2 · transferir a fila aberta ao ativar a delegação — na v1 a delegação alcança só os passos que ativam na vigência (RN-AP-49); a tarefa aberta se move por reatribuição.
- Aprovação por e-mail —
ApprovalTokenestá modelado e desligado por parâmetro. - Aprovador externo sem conta — depende do token acima.
- Hierarquia de pessoas —
RequesterManagere o escalonamentoNotifyManagerexigem a árvore de subordinação no GrydAuth. Enquanto ela não existir, nenhum dos dois está no enum: opção que ninguém consegue executar é dívida, não configuração. - Responsável por categoria — exige um vínculo no
CategoryNodeequivalente ao do centro de custo. - Condições compostas — hoje os critérios são conjunção.
OUse faz com duas políticas complementares. - Votação com dissidência tolerada —
Vote(n), modo de fase 2 restrito aBUDGET,BUDGET_CHANGEeCONTRACT; nunca em processo de controle antifraude, nunca como releitura doQuorum.
- Editor de fluxo em diagrama. Política com critérios e níveis ordenados cobre o que compras precisa; um motor de BPMN traz desvio, subprocesso e evento temporal — e uma classe de bug que ninguém depura em produção.
- Aprovação parcial de documento. Aprovar 6 das 10 linhas parece útil e transforma um pedido em dois, quebrando negociação, rateio e recebimento. Se o requisitante quer separar, ele separa antes.
- Assinatura eletrônica com certificado. Trilha auditável não é assinatura ICP-Brasil. Se o cliente precisar, é integração, não motor.
- Autoaprovação — por decurso de prazo, por chave ou por exceção. Em nenhum processo, por nenhum parâmetro. O que existe é ausência declarada de aprovador:
activationMinValueno MVP e o nívelAutona fase 2, sempre na política, visíveis no simulador e na trilha. - Aprovação fora do motor. Nenhum módulo do Nexio tem rota própria de aprovar; o que parece exceção é ato unilateral com motivo, e está listado em "O que o motor não decide". Se um novo ato precisar de segunda pessoa, ele entra na semente — não ganha rota.
Registro
Histórico de revisões
| Versão | Data | O que mudou |
|---|---|---|
| v2.30 | 30/10/2026 | Permissões padronizadas em read/lookup/create/update/delete (decisão do Roger de 30/10/2026), ação a ação. De → para: manage:approval-processes → update:approval-processes (editar, propor, efetivar mudança, refazer resolução de passo) e delete:approval-processes (descartar proposta); manage:approval-settings → update:approval-settings e delete:approval-settings (descartar); manage:approval-policies → create:approval-policies (criar), update:approval-policies (editar e publicar) e delete:approval-policies (publicar versão com vigência encerrada); manage:approval-groups → read:approval-groups, create:approval-groups, update:approval-groups e delete:approval-groups (descartar proposta); manage:approval-delegations → read:approval-delegations e create:approval-delegations; manage:supplier-qualifications → update:supplier-qualifications; manage:exchange-rates → create:exchange-rates; override:exchange-rates sem permissão definida (pergunta ao Roger no RN-AP-70). manage-own:approval-delegations fica como está, com pergunta ao Roger. |
| v2.29 | 21/09/2026 | Validação de 18/09, achado 23 (decisão do Roger: completar). As flags do item viram critério configurável: linha nova "Flags do item" em §Critérios disponíveis, de alcance de linha, lendo itemFlags; o ApprovalCriteria passa a quinze critérios. As três flags ganham nome — regulated, personalData e critical — e origem no Cadastro de Item (RN-ITM-43): no processo ITEM são flags do assunto, nos processos com linhas chegam em itemFlags. A marca de dado pessoal declarada na linha fica para a spec da Requisition. O exemplo da REQUISITION passa a ser montável só com critérios da tabela. |
| v2.28 | 20/09/2026 | Recuos da validação de 18/09 (achados 17 e 19). RN-AP-68: ganha o padrão seguro — pedido que cita mais de uma requisição, ou cuja instância de origem não está mais em Approved, roda a cadeia inteira; a dispensa para N origens é da spec do Pedido. O que o motor não decide: o aceite de diferença de preço na nota (PriceVarianceAccept) entra na lista como ato unilateral ou processo a decidir pela spec da Nota. A mesma lista ganha o bloqueio e o desbloqueio da relação de fornecimento de um item (Cadastro de Item, RN-ITM-15). |
| v2.27 | 19/09/2026 | Decisão D4 (validação de 18/09). Conclusão sem decisão humana e ajuste do aprovado passam pelo desfecho (RN-AP-71): a aprovação pela política invoca o desfecho na transação do envio — a requisição grava o Commitment — e o Refused recusa o envio, com o documento em rascunho; a alteração de documento aprovado que não sobe valor, não muda o retrato nem a cadeia e não mexe na taxa é ajuste — continua Approved, sem rodada e sem tarefa, o desfecho grava a diferença e o Refused recusa a alteração —; fora disso, reabre com rodada do zero. A herança da RN-AP-21 fica só no reenvio de devolvido. Antes, a aprovação pela política não citava o desfecho e a alteração em que todos os passos herdavam deixava a instância em InApproval sem tarefa (RN-AP-20, 21, 32, 38, 43, tabela de transições, § Mudança depois de aprovado, exemplo 8). Decisão D3, no mesmo dia: a condição (a) da RN-AP-21 vale nas duas moedas, a funcional pela taxa da rodada; o teste de ajuste roda pela taxa gravada, e só a rodada do zero, já decidida, reconverte; “taxa alterada” é override ou troca de moeda; o reenvio reconverte com a data do ato; a alçada compara com a taxa do documento (RN-AP-21, 52, 71, tabela de transições, rota resubmit; Moeda e Câmbio, RN-CUR-22). Achado 7 [BT], no mesmo dia: o corpo de POST /approvals ganha esquema — §Contrato de submissão, com os atos que o usam, envelope, linha, fatia e resposta — e a RN-AP-72: contrato fechado, o que o motor calcula e não recebe, o hash do retrato cobre atributos e não valores nem taxa, na reabertura quem reconverte é o dono no desfecho, uma instância viva por assunto e processo. Erros novos APPROVAL_SUBMISSION_CONTRACT_UNPROCESSABLE e APPROVAL_REQUEST_ALREADY_EXISTS; as flags do item passam a critério de linha (§Política e critérios); RN-AP-21, RN-AP-71, linha do endpoint, campo criteriaSnapshot e erro de excluídos acertados. |
| v2.26 | 13/09/2026 | Correção da validação R7 da onda 1. Controle e antifraude: o enum de reason da RN-AP-60 ganha IdentityCorrectionLinked — a ligação da correção de identidade (EV-27) passa a ter efeito no motor: o Fornecedor manda todos os usuários de cada chave de pessoa ligada em excludedUsers, por chave de pessoa e nunca por usuário (RN-FOR-41, citada), na proposta de conta e na de canal de aviso; a RN-AP-60 declara que a exclusão não afrouxa nem devolve — sem elegíveis, a instância do BANK_ACCOUNT_CHANGE fica parada com alerta (RN-AP-59, RN-AP-61), e não em laço de Refused —, e a RN-AP-61 cita o mecanismo. O enum da entidade ApprovalRequestExcludedUser passa a listar RateOverrider e IdentityCorrectionLinked. Guarda da remoção (RN-AP-69): alcança também grupo usado como reserva de processo ou de política, com o mesmo APPROVAL_SETTINGS_CHANGE_CONTROL_MEMBER_REMOVAL_FORBIDDEN · 403; e a remoção que deixa menos de dois membros vigentes em grupo referenciado por nível de controle Any responde o mesmo 403, seja quem for quem propõe e quem efetiva. Menores: narrativa de mudança fora do corpo da RN-AP-70 (B18); o quoteUnit gravado na prova de alçada passa a dizer qual fator vale em cada ramo do override, como regra citada da Moeda. |
| v2.25 | 12/09/2026 | Correção da validação R6 da onda 1. Antifraude: a exigência de assinatura ICP-Brasil ou firma reconhecida sai da RN-AP-61 — o comprovante da troca de canal é o anexo do tipo declarado que o Fornecedor exige na RN-FOR-40, com a carência de 7 dias da RN-FOR-34, e Foreign segue o mesmo caminho (EV-30); as mesmas citações corrigidas na tabela de processos e no exemplo do BANK_ACCOUNT_CHANGE. A RN-AP-38 passa a citar os dois códigos do dono, BANK_CHANGE_SIGNED_REQUEST_REQUIRED_CONFLICT e SUPPLIER_IDENTITY_CORRECTION_EVIDENCE_REQUIRED_CONFLICT, e devolve o BANK_CHANGE_EVIDENCE_NOT_AVAILABLE_CONFLICT ao caso do Fornecedor (revogado, eliminado ou fora de Available). Dinheiro: RN-AP-60 e RN-AP-70 nomeiam os dois campos da Moeda — capturedBy e overriddenBy —, e no override só de tipo quem o registrou também entra em excludedUsers como RateOverrider. Controle: a RN-AP-69 ganha o escopo escrito da comparação por pessoa e o risco aceito dos demais parâmetros críticos, que ficam por userId (EV-31), e uma guarda própria para a remoção — quem propõe ou efetiva a saída de membro de grupo ou papel de nível de controle não pode ser membro remanescente (APPROVAL_SETTINGS_CHANGE_CONTROL_MEMBER_REMOVAL_FORBIDDEN · 403, novo). Slug do aviso alinhado ao catálogo: nexio.control-role-member.granted · revoked. B18 na RN-AP-47. |
| v2.24 | 12/09/2026 | Correção da validação R5 da onda 1. Separação por pessoa no controle: em grupo ou papel de nível de controle, quem propõe, quem efetiva e quem é incluído precisam de identidade verificada e são comparados por CPF — dois usuários da mesma pessoa não fazem as duas pontas (APPROVAL_SETTINGS_CHANGE_IDENTITY_NOT_VERIFIED_CONFLICT, novo; APPROVAL_SETTINGS_CHANGE_SELF_APPLY_FORBIDDEN passa a comparar por CPF; RN-AP-69). O motor não confere documento: onde a regra do módulo dono exige documento comprobatório, a evidência é o anexo com o rastro — quem anexou, quem decidiu, data e hora —, exigido e gravado pelo módulo dono; o Fluxo não confere assinatura, poderes nem selo, e o …/approve não tem campo para isso (RN-AP-38). Recusas conhecidas da RN-AP-38 acrescidas de BUDGET_CHANGE_INSUFFICIENT_UNPROCESSABLE, BANK_CHANGE_APPROVER_IS_REQUESTER_CONFLICT (como Refused), BANK_CHANGE_IDENTITY_CORRECTION_LINKED_CONFLICT e SUPPLIER_IDENTITY_CORRECTION_IDENTITY_NOT_VERIFIED_CONFLICT. Citações corrigidas: a identidade da correção é a RN-FOR-41; a troca de tipo recusada inclui de e para Foreign; os decisores do BANK_ACCOUNT_CHANGE têm CPFs distintos entre si e excluem quem está ligado, sem prazo, a uma correção de identidade do fornecedor. Tipo de taxa: o nome é processRateTypes (o defaultRateType vale só para movimento sem documento de origem), e a flag sem RateOverrider deixa de ser recusa — a Moeda preenche o capturedBy da linha usada (RN-AP-70). Contestação de canal comprometido suspende e vai ao complementar da controladoria, e a feita por dentro do Nexio exige segunda pessoa (RN-AP-61, citada do Fornecedor). Menores: aviso da taxa também na reavaliação; o reserva em passo de controle pertence ao alvo (RN-AP-59); a decisão grava o CPF verificado e o estado da identidade (RN-AP-67); a herança relê identidade e distinção por CPF (RN-AP-21 (d)); resíduos de narrativa e "o banco ainda não está definido". |
| v2.23 | 11/09/2026 | Correção da validação R4 da onda 1. Controle: SUPPLIER_REGISTRATION ganha a flag identityCorrection e a complementar semeada e travada Role(Controladoria), com quem pediu em excludedUsers e o cadastro continuando Approved (tabela de processos, tabela de complementares, cardinalidade da instância, RN-AP-29, RN-AP-60, exemplo); grupo e papel usados por nível de controle na cadeia congelada de instância em InApproval seguem a RN-AP-69 e a RN-AP-33, sob a trava por (tenant, processo); quem a mudança inclui, prorroga ou põe como reserva não a efetiva (APPROVAL_SETTINGS_CHANGE_SELF_APPLY_FORBIDDEN). Dinheiro: a soma da RN-AP-68 é conferida também na reavaliação e no reenvio, com o valor novo e sob a trava da instância de origem (RN-AP-20, RN-AP-21, RN-AP-47). Taxa: tipo de taxa da configuração; flag também quando o autor da taxa do tenant está entre os excluídos ou submete; autor de toda taxa do tenant usada como RateOverrider (RN-AP-60, RN-AP-70). BANK: todo decisor com identidade verificada e comparado por CPF, citado do Fornecedor (RN-AP-61, tabela de complementares). Coerência: recusas conhecidas do desfecho completadas e a identidade de quem propõe como 409 no Fornecedor (RN-AP-38); flags do assunto completas no critério; titularidade citada pela RN-FOR-35. |
| v2.22 | 11/09/2026 | Correção da validação R3 da onda 1. Controle: decisão em passo de controle nunca cobre outro passo — quem decidiu controle sai do passo seguinte, que não vai a Skipped (RN-AP-57, RN-AP-64, EV-7); exemplo BUDGET_CHANGE refeito com stage; em passo de controle o titular também precisa pertencer hoje ao alvo do nível, relido na decisão e na herança (RN-AP-59 (2), RN-AP-21 (d)); reatribuição de controle recusada também a quem decidiu outro passo da rodada; grupo referenciado por nível de controle em política ativa e papel usado por nível de controle seguem a RN-AP-69 (EV-17); o autor da taxa do tenant gravada por quem pede, prepara ou submete também é RateOverrider (RN-AP-70, EV-5); recusas por identidade não verificada citadas do Fornecedor (RN-AP-38, EV-13, EV-21). Dinheiro: pedido cancelado fica na soma da RN-AP-68 pela parte não cancelada; só o rejeitado sai inteiro (ET5-7). BANK: quarta complementar semeada e travada, contestedChannel (EV-12); noTrustedPhone vale também na mudança de contato, com controladoria, documento assinado SIGNED_CONTACT_CHANGE_REQUEST e carência de 7 dias (EV-18); variante de contato no exemplo. Coerência: variante de alçada estourada do PURCHASE_ORDER com o passo paralelo da controladoria (RN-AP-58); recusa de revaluate no BANK no endpoint e no erro. Retirado do corpo: a lede sobre a versão anterior sem execução; "até então" na trilha; "rota antiga" nas políticas semeadas; "passou a acusar" (RN-AP-39); "até agora só uma delas olhava" (RN-AP-41); a recomendação do relatório (RN-AP-42); "o padrão era falha aberta" (RN-AP-51); "passa a valer" (RN-AP-45); a data do relatório na dependência de plataforma (cita o P8 da demanda); "política publicada antes desta regra"; o título "a reabertura desfaz o efeito" e o "rejeitar depois de aprovado". |
| v2.21 | 11/09/2026 | Correção da validação R2 da onda 1. Controle: passo de controle definido pela posição — toda complementar, o passo de controladoria e o Quorum do BANK_ACCOUNT_CHANGE, nunca pelo nome do papel (RN-AP-64, EV-7); dedupe nunca pula passo de controle (RN-AP-57); delegação, reatribuição e escalonamento de passo de controle só para quem é do alvo do nível, erro APPROVAL_CONTROL_STEP_TARGET_UNPROCESSABLE (RN-AP-59); herança relê a elegibilidade inteira (RN-AP-21); pedido dispensa a base só até a soma dos pedidos vivos caber no aprovado, sob trava da instância de origem (RN-AP-68, ET5-7); publicação barra qualquer editor também no endpoint e no erro (RN-AP-55); configuração crítica do motor por proposta e efetivação por outra pessoa, entidade ApprovalSettingsChange e três erros (RN-AP-69, EV-6); taxa informada aciona a controladoria, flag exchangeRateOverridden e excluído RateOverrider (RN-AP-70, EV-5); tabela de complementares de controle semeadas. BANK: revaluate recusado; quatro olhos só sobre approve:supplier-banks; callback citado com a exceção noTrustedPhone e a separação do Fornecedor; terceira flag previousNoticeUndeliverable; titular ≠ fornecedor só em Foreign (EV-10); exceções do BANK na RN-AP-08 e na RN-AP-38. Coerência: reabrir Approved não grava nada no razão (ET2-3); exemplos pelo stage; carga inicial do catálogo citada (ET5-6); manage-own:approval-delegations numa forma só; v2 da fila só em §Fora de escopo. Retirado do corpo: a v1.0 definia paralelo entre níveis e delegação "no máximo uma ativa por pessoa"; notas "semente antiga" do BANK, "hoje a ativação…" (RN-AP-45) e "o critério listava três sinais" (RN-AP-53); exemplos com Role(Controller) passam a Role(Controladoria). |
| v2.20 | 11/09/2026 | Correções da validação de 10/09 e da validação da onda 1. Controles: núcleo de controle por processo (RN-AP-54); autor e editores do rascunho não publicam, em todos os processos (RN-AP-55); pessoas distintas e excluídos no agregado (RN-AP-56, RN-AP-60); dedupe que nunca pula quórum nem passo de controle (RN-AP-57, RN-AP-64); alçada estourada e reserva exigem a controladoria ou recusam — sai "aprovar no topo só com aviso" (RN-AP-58); elegibilidade pessoa a pessoa, com Deputy e passo parado (RN-AP-59); BANK_ACCOUNT_CHANGE normativo, sem valor e sem devolução, com o callback no Fornecedor (RN-AP-61); valor indisponível (RN-AP-62); activationNotBefore (RN-AP-63). Contratos e ciclo: desfecho Applied · Refused · Failed e RN-AP-25 aposentada (RN-AP-38); reaprovação sem tolerância, com retrato e herança (RN-AP-20, RN-AP-21); materialidade pelo menor limiar (RN-AP-15); stage (RN-AP-65); critérios por linha (RN-AP-66); prova do que foi visto (RN-AP-67); pedido que nasce de requisição dispensa só a base (RN-AP-68); tabela de transições; ApprovalProcess do produto × ApprovalProcessSettings do tenant; NexioAdmin; fuso; avisos; apagar comentário por LGPD; nomes de evento em kebab; APPROVAL_COMMENT_REQUIRED_UNPROCESSABLE. Para a v2: Panel, CostCenterOwnerAtDistance, critérios faixa de risco e desvio do menor preço, transferência da fila aberta na delegação. |
| v2.19 | 09/09/2026 | Leva transversal do acervo: permissões com recurso no plural, citações a outras páginas sem número de versão, ordem das RNs. |
| v2.18 | 09/09/2026 | Validação cruzada: códigos repassados corrigidos (BUDGET_UNCOVERED_ALLOCATION_UNPROCESSABLE, APPROVAL_LIMIT_CURRENCY_NOT_CONVERTIBLE_UNPROCESSABLE); signalFrom; quoteUnit no evento; ApprovalRequest dona de anexo. |
| v2.17 | 09/09/2026 | Approved terminal condicional e roundNumber (RN-AP-43); version na raiz (RN-AP-44); ativação vazia com desfecho (RN-AP-45); escopo Line (RN-AP-46); reavaliação contra as vigentes (RN-AP-47); esquema do ApprovalEvent e GET /approvals (RN-AP-48); escalonamento composto com o modo (RN-AP-50); alçada ausente = sem alçada (RN-AP-51); alçada na moeda do limite (RN-AP-52); quatro sinais do orçamento (RN-AP-53). |
| v2.9 – v2.16 | 08/09/2026 | Membro de grupo com vigência; Draft, Expired e NotifyManager fora; alvo de escalonamento declarado e delegação por processo (RN-AP-35 a RN-AP-37); efeito e desfecho na mesma transação (RN-AP-38); escopo por empresa (RN-AP-39, RN-AP-40); política de estouro e exigência não cumprida (RN-AP-41, RN-AP-42); calendário como lista do tenant; materialidade com dono único; ApprovalSettings separada de ApprovalProcessSettings. |
| v2.6 – v2.8 | 03/09 a 06/09/2026 | Motor universal: cadastro e homologação de fornecedor, alteração bancária e curadoria de item passam pelo motor (princípio 7); PermissionHolder e política padrão semeada; sobreposição de bases verificada na aplicação; alçada estourada marcada e aprovação pela política declarada (RN-AP-31, RN-AP-32). |
| v2.0 | — | Especificação reescrita sobre a de junho: instância, passo e tarefa; política base e complementares; alçada no vínculo de centro de custo; trilha como evento. |