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.
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. Entre níveis é sempre sequencial. A v1.0 definia o oposto, e nunca leu o campo — trocar a semântica ainda é de graça, e esta é a 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. 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 instância é reconstruível a partir deles. ApprovalEvent é append-only, com autor, papel pelo qual agiu, delegação em vigor e o passo alcançado. O estado do agregado é projeção, não fonte. Sem isso, "por que este pedido foi aprovado" só tem resposta na cabeça de quem estava lá.
Fundamentos
Seis princípios
1. Configuração e execução são agregados diferentes
Editar uma política não pode mudar um documento em andamento. 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.
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. 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.
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 salvar, com 409. A garantia é uma restrição EXCLUDE com btree_gist no Postgres sobre tenant, processo, prioridade, faixa de valor, nó de centro de custo e nó de categoria — não uma varredura em memória, que perde a corrida de dois POST simultâneos e é exatamente o defeito da implementação anterior.
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.
Modelo
Mapa de entidades
Configuração — o que o cliente parametriza
| Entidade | Cardinalidade | O que é |
|---|---|---|
| ApprovalProcess | configuração · por tenant | Qual documento é aprovável. Define o valor avaliado, os critérios disponíveis, as ações permitidas e o aprovador de reserva. Tipos-semente: REQUISICAO, COTACAO, PEDIDO, CONTRATO, FORNECEDOR, ALTERACAO_BANCARIA, ALTERACAO_ORCAMENTARIA. |
| ApprovalPolicy | N por processo · versionada | A regra. kind = Base ou Complementar, prioridade, critérios, vigência e níveis. Versão imutável. |
| ApprovalCriteria | value object da política | Faixa de valor, nó de centro de custo, nó de categoria, tipo de item, fornecedor, empresa, destinação, flags do item, estado do orçamento. Critério ausente casa tudo. |
| 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. |
| 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. |
| CostCenterAssignment | do Centro de Custo | Onde a alçada da pessoa mora (approvalLimit). O motor lê; não escreve. |
| ApprovalDelegation | N por usuário | Ausência temporária. Herdada da v1.0, que já estava completa, mais escopo opcional por processo e teto de valor. |
| ApprovalSettings | 1 por tenant × processo | Os parâmetros globais: dedupe, autoaprovação, materialidade do rateio, tolerância de reavaliação, SLA padrão, comportamento de escalonamento. |
Execução — o que o motor escreve
| Entidade | Cardinalidade | O que é |
|---|---|---|
| ApprovalRequest | aggregate root · 1 por documento vivo | A instância. subjectType + subjectId, empresa, requisitante, valor avaliado, moeda, estado, e o snapshot das políticas que a formaram com suas versões. |
| ApprovalStep | 1..N por instância | Um nível resolvido. Ordem, origem (qual política e nível), escopo, modo, SLA, estado. |
| 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 | Toda transição, com autor, ação, alvo e metadados. Fonte da verdade da trilha. |
| 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 |
|---|---|---|---|
| REQUISICAO | Total estimado, por fatia de rateio | valor, centro de custo, categoria, tipo de item, destinação, orçamento | Libera para cotação ou para pedido |
| COTACAO | Valor da proposta escolhida | valor, categoria, fornecedor, desvio do menor preço | Libera a emissão do pedido |
| PEDIDO | Total do pedido, por fatia | valor, centro de custo, categoria, fornecedor, empresa, orçamento | Autoriza o envio ao fornecedor |
| CONTRATO | Valor total comprometido | valor, categoria, fornecedor, prazo, cláusula de exclusividade | Coloca o contrato em vigência |
| FORNECEDOR | — | categoria de homologação, faixa de risco, sanção pública | Muda o estado para homologado |
| ALTERACAO_BANCARIA | — | fornecedor, valor médio histórico | Aplica a proposta ao registro vivo |
| ALTERACAO_ORCAMENTARIA | Delta da verba | valor, centro de custo, conta contábil | Aplica o remanejamento |
Dois desses processos não têm valor, e é por isso que eles precisam existir na lista. Homologação de fornecedor e alteração de dado bancário 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.
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 Complementar. Decide se a política concorre ou empilha. Imutável depois de publicada. |
| priority | int | Só tem efeito em Base: menor número vence. Em Complementar, ordena a posição na cadeia. |
| isDefault | bool | Exatamente uma política Base por processo é a de reserva, com critérios vazios. É a RN-CF03 da spec de junho, que o agregado nunca teve. |
| 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. |
| fallbackApproverId | Guid? | Usado quando um nível resolve vazio. 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 do tenant |
| 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 |
| Tipo de item | em lista | ItemType das linhas |
| Destinação | igualdade | predominantUse — CAPEX ou OPEX, sem depender do centro de custo |
| Empresa | em lista | legalEntityId do documento |
| Fornecedor | em lista · ou estado | Fornecedor da linha, ou o fato de ele ser Prospect, sancionado ou com documento vencido |
| Flag do item | presença | Regulado, com dado pessoal, crítico — as flags do catálogo, nunca nós inventados na taxonomia |
| Orçamento | estado | DentroDaVerba, AcimaDoAlerta, Estourado |
| Desvio do menor preço | percentual | Só no processo de cotação |
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 salvar
Duas políticas base com a mesma prioridade e critérios que se cruzam nunca coexistem. A garantia vive no banco, com EXCLUDE e btree_gist sobre (tenant =, processo =, prioridade =, faixa de valor &&, nó de CC =, nó de categoria =) filtrado por política ativa. Isso move a invariante para fora da aplicação e elimina a varredura O(n) que a implementação anterior fazia em memória, sem lock.
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 | Sequencial dentro da política. Entre níveis é sempre sequencial. |
| approverType | enum | Ver a tabela seguinte. Determina a estratégia de resolução. |
| target | Guid? / string? | Usuário, papel, grupo, e-mail externo ou distância de subida, conforme o tipo. |
| mode | enum | Qualquer · Todos · Quorum(n). É aqui que vive o paralelo. |
| 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. |
| scopeMode | enum | Documento · Fatia. Um nível de alçada de centro de custo é normalmente por fatia; um de Jurídico é sempre do documento. |
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 effectiveOwners do nó da fatia, com rollup. 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 |
| CostCenterOwnerAtDistance(n) | O responsável n níveis acima do nó da fatia. É o "gestor do gestor" sem depender de hierarquia de pessoas. | MVP |
| ApprovalGroup | Os membros ativos do grupo. Comitês. | 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. Qualquer: a primeira decisão fecha o nível — e as demais tarefas viram Obsoleta, não desaparecem, porque a trilha precisa mostrar quem mais estava na fila. Todos: o nível só fecha quando todos decidem; uma rejeição encerra a instância imediatamente. Quorum(n): fecha em n aprovações, ou na primeira rejeição que torne o quórum inalcançável — e é a semântica que comitê de investimento pede.
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 | Filtra quem tem approvalLimit ≥ valor da fatia, ou limite nulo (sem teto próprio). |
| 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 e registra authorityExceeded no passo — a aprovação acontece, e o evento diz que ninguém na cadeia tinha alçada formal para o valor. Recusar seria pior: travaria a compra por um cadastro incompleto. |
| 4 | Raiz também vazia | Aprovador de reserva do processo. Se não houver, a submissão é recusada com 422 nomeando o passo. |
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.
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 de todos os níveis intermediários é uma configuração legítima, e se faz com CostCenterOwnerAtDistance repetido, explicitamente.
A moeda é a do tenant, e a conversão é congelada
Documento em outra moeda é convertido pela taxa vigente no momento da submissão, e a taxa fica no snapshot da instância. 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.
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.
Herdada da v1.0, que já estava completa: delegante, delegado, período, no máximo uma ativa por pessoa, encerramento automático por rotina diária, e registro em auditoria com delegante, substituto e referência à delegação. Ganha dois campos: escopo por processo (posso delegar pedido e não delegar alteração bancária) e teto de valor (delego até R$ 50 mil; acima disso continua comigo).
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.
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 e devolver, o que gera evento e notifica A.
Execução
Instância, passo e tarefa
| Entidade | Estados | Transição terminal |
|---|---|---|
| ApprovalRequest | Rascunho · EmAprovacao · Aprovada · Rejeitada · Devolvida · Cancelada · Expirada | Aprovada, Rejeitada, Cancelada e Expirada são finais. Devolvida volta ao requisitante e a instância continua viva. |
| ApprovalStep | Pendente · Ativo · Aprovado · Rejeitado · Pulado · Obsoleto | Pulado só por activationMinValue ou por dedupe — nunca por falta de aprovador. Obsoleto é o que a reavaliação produz. |
| ApprovalAssignment | Pendente · Aprovada · Rejeitada · Devolvida · Obsoleta · Reatribuida | Obsoleta quando outra tarefa do mesmo nível já fechou o passo em modo Qualquer. A linha permanece: a trilha mostra quem mais estava na fila. |
| 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 | Documento, Linha ou Fatia, 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. |
| 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 | Direta, PorPapel, PorGrupo, PorDelegacao, PorEscalonamento, PorReatribuicao — com o id da delegação, do grupo ou do papel. |
| decision · comment · decidedAt | tarefa | A decisão, o comentário e o momento. Comentário é obrigatório em rejeição e devolução. |
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 Obsoleto. 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, sequência de passos com tipo, escopo, modo e SLA | 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: a mesma pessoa não assina duas vezes seguidas
Se Ana é responsável pelo centro de custo e membro do comitê, e os dois passos são consecutivos, ela aprova uma vez e o segundo passo é marcado Pulado com o motivo. Configurável por processo — há empresas que exigem a dupla assinatura de propósito, e há auditoria que a proíbe. O que não pode existir é o comportamento implícito.
O requisitante é removido de toda tarefa da própria instância
Antes de tudo. Se a remoção esvaziar o passo, o motor sobe um nível; se esgotar, cai no aprovador de reserva. Se nem isso, a submissão é recusada com 422, e a mensagem diz qual passo ficou sem ninguém — não "erro ao aprovar".
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 → rollup na árvore → delegação vigente → aprovador de reserva da política → aprovador de reserva do processo → recusa explícita.
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 de allocationMaterialityPercent e de allocationMaterialityAmount — o que for maior — é absorvida pela maior fatia para efeito de fluxo. Continua contabilizada integralmente. |
| 3 | Gera passos por escopo | Níveis com scopeMode = Fatia geram um passo por fatia material, em paralelo entre si. 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. |
| 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.
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 Obsoleto; o reenvio abre uma rodada nova na mesma instância. | 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. 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 padrão | Parâmetro |
|---|---|---|
| Valor sobe além da tolerância | Reavalia. Passos já aprovados que continuam na cadeia nova são preservados; os que deixaram de existir viram Obsoleto; os novos entram como Pendente. | revaluationTolerancePercent · Amount |
| Valor desce | Não reavalia. Quem aprovou mais já aprovaria menos. | — |
| Muda centro de custo ou rateio | Reavalia sempre, independentemente de tolerância — mudou quem paga. | — |
| Muda categoria ou tipo de item | Reavalia sempre — pode acionar controle complementar que não existia. | — |
| Muda fornecedor | Reavalia se houver política com critério de fornecedor; senão, registra evento e segue. | — |
| Muda quantidade sem mudar valor | Registra evento e segue. | — |
Preservar aprovação ainda válida é o que separa reavaliação de reinício. Se o gestor do centro de custo já aprovou e continua na cadeia nova com o mesmo escopo e a mesma política, sua assinatura vale — e o evento de reavaliação registra isso explicitamente, com o que mudou. Reiniciar do zero a cada centavo é o que faz o usuário aprender a submeter o pedido pelo valor certo depois de aprovado.
Execução
SLA e escalonamento
O vencimento é calculado na ativação do passo, em horas úteis conforme o calendário do tenant — feriado nacional e expediente configuráveis. Contar em horas corridas faz todo passo ativado na sexta vencer no domingo, e o escalonamento vira ruído semanal.
Por política, em ordem: Notificar (lembrete ao aprovador e ao requisitante), NotificarGestor, Escalonar (cria tarefa para o nível acima na árvore de centro de custo, mantendo a original viva) e Nenhum. Não existe AutoAprovar.
A tarefa original continua pendente e ainda pode ser decidida. A escalada acrescenta um caminho, não substitui — e as duas fecham o passo, com o evento dizendo qual delas chegou primeiro.
Enquanto uma solicitação de informação está aberta, o SLA do passo pausa e retoma na resposta. 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ê | budgetStateFor(empresa, nó, conta, período, valor) devolve DentroDaVerba, AcimaDoAlerta ou Estourado. É o que permite a política "estouro de verba exige o controller", em vez de bloquear a compra. |
| Empenho na aprovação | o motor escreve | Na transição para Aprovada, o motor emite um evento de domínio com as fatias e os valores. Quem cria o Commitment é o módulo de orçamento — o motor não conhece verba. |
| Liberação no cancelamento | o motor escreve | Cancelar ou rejeitar depois de aprovado emite o evento inverso. Empenho que não é liberado é o defeito que faz o orçamento "acabar" em setembro. |
| Bloqueio no estouro | o motor lê | budgetBlockBehavior por tenant: Avisar ou Bloquear. Bloquear recusa a submissão com 422 e nomeia o nó e o período — nunca um erro genérico. |
| 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. |
Diferencial
Simulador
POST /approvals/simulate recebe um documento hipotético — valor, empresa, linhas com centro de custo e categoria, requisitante — e devolve a cadeia que ele produziria, sem criar nada. 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 mesma consulta alimenta a prévia mostrada na submissão: "este pedido passará por Ana, depois pelo comitê". Expectativa correta é metade das reclamações de fluxo de aprovação.
Reexecutar uma instância antiga contra a configuração da época e comparar com o que de fato aconteceu. Como a instância guarda as versões das políticas, a comparação é exata — e é a resposta a "prove que o controle funcionava em março".
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 |
|---|---|---|---|
| businessCalendarId | Guid | Brasil · 9h-18h | Base do cálculo de SLA em horas úteis |
| allocationMaterialityPercent | numeric(5,2) | 5,00 | Fatia abaixo disso não gera passo próprio |
| allocationMaterialityAmount | numeric(19,2) | 1.000,00 | Vale o que for maior entre os dois |
| budgetBlockBehavior | enum | Avisar | Comportamento no estouro de verba |
| costCenterMaxDepth | int? | nulo | Teto de profundidade da árvore — política, não estrutura |
| Parâmetro | Tipo | Padrão | Efeito |
|---|---|---|---|
| fallbackApproverId | Guid | — | Último recurso quando toda resolução esvazia. Obrigatório para ativar o processo |
| allowSelfApproval | bool | false | Alterar exige justificativa e fica na trilha do tenant |
| deduplicateConsecutive | bool | true | Mesma pessoa em níveis consecutivos assina uma vez |
| revaluationTolerancePercent | numeric(5,2) | 0,00 | Aumento tolerado sem reavaliar a cadeia |
| revaluationToleranceAmount | numeric(19,2) | 0,00 | Vale o que for maior entre os dois |
| defaultSlaHours | int | 48 | SLA herdado por política e nível |
| escalationBehavior | enum | Notificar | Nenhum · Notificar · NotificarGestor · Escalonar |
| requireCommentOnApprove | bool | false | Rejeição e devolução já exigem sempre |
| allowReassign | bool | true | Permite passar a tarefa a outro elegível, uma vez |
| allowBatchApproval | bool | true | Aprovar várias tarefas numa ação — sempre um evento por tarefa |
| emailApproval | bool | false | Liga o ApprovalToken. Fase 2 |
| Parâmetro | Nível | Efeito |
|---|---|---|
| priority · isDefault · kind | política | Como a política concorre ou empilha |
| criteria | política | Dez critérios; ausência casa tudo |
| validFrom · validTo | política | Vigência da regra |
| fallbackApproverId | política | Sobrepõe o do processo |
| approverType · target | nível | Estratégia de resolução |
| mode · quorum | nível | Qualquer · Todos · Quorum(n) |
| scopeMode | nível | Documento ou Fatia |
| activationMinValue | nível | Abaixo disso o nível não entra |
| maxApprovalHours | nível | SLA próprio; nulo herda |
| label | nível | O nome que o aprovador vê |
Camada API
Endpoints
| Rota | Permissão | O que faz |
|---|---|---|
| GET /approval-processes | approval.process:read | Processos e seus parâmetros |
| PUT /approval-processes/{id} | approval.process:manage | Parâmetros do processo e aprovador de reserva |
| GET /approval-policies | approval.policy:read | Filtra por processo, tipo, vigência e nó |
| POST /approval-policies | approval.policy:manage | Cria em rascunho. Sobreposição entre bases é 409 |
| POST /approval-policies/{id}/publish | approval.policy:manage | Publica versão nova e aposenta a anterior |
| GET /approval-policies/{id}/validate | approval.policy:read | Inconsistências: nó sem responsável, grupo vazio, papel sem membro, nível sem alvo |
| GET /approval-groups · POST · PUT | approval.group:manage | Comitês e seus membros com vigência |
| GET /approval-delegations · POST | approval.delegation:manage · :self | Herdado da v1.0, mais escopo e teto |
| POST /approvals/simulate | approval.policy:read | A cadeia de um documento hipotético, sem criar nada |
| Rota | Permissão | O que faz |
|---|---|---|
| POST /approvals | interna | Submete um assunto. Chamada pelo módulo dono do documento, nunca pela tela |
| GET /approvals/{id} | approval:read | A instância com passos, tarefas e a cadeia projetada até o fim |
| GET /approvals/inbox | approval:act | 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 | approval:act | Aprova. Aceita ?withProviso= para ressalva |
| POST /approvals/{id}/assignments/{aid}/reject | approval:act | Rejeita. Comentário obrigatório |
| POST /approvals/{id}/assignments/{aid}/return | approval:act | Devolve para ajuste. Comentário obrigatório |
| POST /approvals/{id}/assignments/{aid}/request-info | approval:act | Pergunta ao requisitante. Pausa o SLA |
| POST /approvals/{id}/assignments/{aid}/reassign | approval:reassign | Passa a outro elegível, uma vez |
| POST /approvals/batch-approve | approval:act | N tarefas numa chamada, N eventos |
| POST /approvals/{id}/cancel | interna | Cancelamento vindo do documento. Libera empenho |
| POST /approvals/{id}/revaluate | interna | Disparado pela alteração do documento. Preserva o que continua válido |
| GET /approvals/{id}/events | approval:read | A trilha completa, ordenada |
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. Garantido no banco |
| 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 |
| 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_NO_APPROVER_UNPROCESSABLE | 422 | Um passo resolveu vazio depois de todo o encadeamento. A mensagem nomeia o passo |
| APPROVAL_SELF_APPROVAL_UNPROCESSABLE | 422 | Requisitante tentando decidir a própria instância |
| APPROVAL_NO_POLICY_UNPROCESSABLE | 422 | Nenhuma política base casou e o processo não tem política de reserva |
| APPROVAL_BUDGET_EXCEEDED_UNPROCESSABLE | 422 | Estouro com budgetBlockBehavior = Bloquear. Nomeia nó e período |
| APPROVAL_DELEGATION_CHAIN_UNPROCESSABLE | 422 | Delegar para quem já delegou — cadeia não encadeia |
| APPROVAL_REASSIGN_TARGET_UNPROCESSABLE | 422 | Destino da reatribuição não é elegível para o passo |
| APPROVAL_POLICY_NO_LEVEL_UNPROCESSABLE | 422 | Publicar política sem nível |
| APPROVAL_PROCESS_NO_FALLBACK_UNPROCESSABLE | 422 | Ativar processo sem aprovador de reserva |
| APPROVAL_COMMENT_REQUIRED | 400 | Rejeição, devolução, ressalva, reatribuição ou pedido de informação sem comentário |
| APPROVAL_NOT_FOUND | 404 | Instância, passo ou tarefa inexistente neste tenant |
Autorização
Permissões
| Permissão | Público | Cobre |
|---|---|---|
| approval.process:read · :manage | NexioAdmin | Processos e parâmetros globais |
| approval.policy:read | Admin, Controller | Ler políticas, validar e simular |
| approval.policy:manage | NexioAdmin | Criar, editar e publicar política |
| approval.group:manage | NexioAdmin | Comitês e membros |
| approval.delegation:manage | NexioAdmin | Qualquer delegação do tenant |
| approval.delegation:self | Aprovador | A própria delegação |
| approval:read | Requisitante, Aprovador, Auditoria | Instância, cadeia e trilha dos documentos a que a pessoa tem acesso |
| approval:act | Aprovador | Decidir sobre tarefas atribuídas a si. Não é guarda-chuva: a tarefa precisa ser sua |
| approval:reassign | Aprovador sênior, Admin | Passar tarefa a outro elegível |
| approval:audit | Auditoria | Ler qualquer instância do tenant, inclusive de documentos fora do alcance da pessoa |
A separação entre approval:act e approval:reassign é 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 ordenada e deduplicada da base com todas as complementares que casaram.
Prioridade desempata bases; especificidade não
Menor número vence. Sobreposição com a mesma prioridade é recusada ao salvar, no banco.
Todo processo tem política e aprovador de reserva
Exatamente uma política base com critérios vazios e um fallbackApproverId. Sem os dois, o processo não ativa.
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 e rateio. Nada disso muda sem reavaliação explícita.
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 nunca decide a própria instância
Removido de toda tarefa antes de qualquer outra regra. Se o passo esvaziar, sobe um nível; se esgotar, cai no reserva.
Passo sem aprovador é recusa, nunca pulo
422 nomeando o passo. Só activationMinValue e dedupe produzem passo pulado.
Paralelo é dentro do nível; entre níveis é sequencial
Qualquer, Todos ou Quorum(n). Rejeição encerra a instância em qualquer modo.
Nada é aprovado por decurso de prazo
SLA vencido notifica ou escalona. AutoAprovar não existe como comportamento de escalonamento.
Escalonar acrescenta caminho, não substitui
A tarefa original continua viva. Ambas fecham o passo; o evento registra qual chegou primeiro.
SLA conta em horas úteis e pausa em pedido de informação
Calendário por tenant. O tempo pausado é registrado separadamente.
Regras são avaliadas por linha e fatia; a decisão é do documento inteiro
Fatias do mesmo centro de custo são agregadas antes de gerar passo.
Alçada é avaliada sobre a fatia, acima do limiar de materialidade
Fatia imaterial é absorvida pela maior para efeito de fluxo e continua contabilizada integralmente.
Alçada não soma entre pessoas; acumula subindo a árvore
Esgotada a subida, aprova o topo e o passo registra authorityExceeded.
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 mantém a instância viva
O reenvio abre rodada nova na mesma instância, com os passos anteriores marcados como obsoletos.
Comentário é obrigatório em toda ação que não seja aprovar
Rejeitar, devolver, pedir informação, reatribuir e aprovar com ressalva.
Aumento acima da tolerância reavalia; redução não
Mudança de centro de custo, rateio, categoria ou tipo de item reavalia sempre, sem tolerância.
A reavaliação preserva aprovação ainda válida
Passo aprovado que continua na cadeia nova, com mesmo escopo e mesma política, mantém a assinatura. O evento registra o que mudou.
Aprovação em lote gera um evento por tarefa
Nunca um evento de lote.
Toda transição é evento, e o estado é projeção
Append-only, com autor, papel pelo qual agiu, delegação em vigor e passo alcançado.
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.
A aprovação final emite evento de empenho; o cancelamento emite o inverso
O motor não conhece verba — quem cria e libera o Commitment é o módulo de orçamento.
A cadeia é consultável antes de existir
O simulador roda a mesma seleção, sem criar nada, inclusive contra política em rascunho e contra a configuração de uma data passada.
Limites
Fora de escopo
- Aprovação por e-mail —
ApprovalTokenestá modelado e desligado por parâmetro. - Aprovador externo sem conta — depende do token acima.
- Hierarquia de pessoas —
RequesterManagerexige a árvore de subordinação no GrydAuth. - 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.
- 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. Em nenhuma configuração, por nenhum parâmetro.
Pendência que fica registrada: o processo REQUISICAO avalia valor estimado, e o PEDIDO avalia o valor negociado, que costuma ser menor. Falta decidir se um pedido cujo valor ficou abaixo do já aprovado na requisição precisa de nova cadeia. A recomendação é não precisar, com um parâmetro por processo para os clientes que exigem a dupla aprovação — mas isso depende de a Requisição existir, e volta como decisão daquela spec.