Ciclo de aprovação genérico
Como funciona o engine de aprovações que governa toda decisão financeira da plataforma
→ Conceito de Aprovações — leia primeiro se ainda não.
Anatomia de uma aprovação
1. Origem cria approval_request
- entity_type: ex 'cotacao_decisao'
- entity_id: id da cotação
- entity_value: valor para casar com policy
- metadata: dados extras (ex: vencedores_por_item)
↓
2. Engine resolve a policy aplicável
- filtra por organization_id, entity_type, condition
- escolhe maior priority cuja condição casa
↓
3. Cria assignments para o step 1
- resolve aprovadores (role / user / dinâmico)
- notifica aprovadores
↓
4. Aprovadores acessam Painel → Aprovações
- aprovam (avança step) ou rejeitam (encerra request)
- delegação por ausência roteia para delegatário
↓
5. Step completo → próximo step
- se requires_all=true: todos do step precisam aprovar
- se requires_all=false (default): qualquer um aprova
↓
6. Todos os steps OK → request status='aprovado'
- Engine dispara callback registrado
- Callback atualiza entidade subjacente automaticamente
↓
7. Auditoria
- approval_audit_log registra cada ação
- imutável — correção é evento compensatórioEntity types cobertos
Cada entity_type tem callback que atualiza a entidade:
| Entity type | Callback aprovação |
|---|---|
requisicao_compra | RC.status = aprovada |
cotacao_decisao | Cria PO automaticamente + RC pedido_emitido + cotação decidida |
pedido_compra | PO.status = confirmado |
contrato_empreitada | Contrato.status = aprovado |
recebimento_divergente | Recebimento aprovado + dispara entrada estoque |
aditivo_contrato | Aditivo.status = aprovado (depois aplica manualmente) |
liberacao_retencao | medição.retencao_liberada = true |
medicao_empreiteiro | medição.status = aprovada |
suprimento_fundos | fundo.status = aprovado |
pagamento | CAP.status = pendente (pronto para pagar) |
viabilidade | EVS.is_aprovado = true |
baseline | orçamento.is_baseline = true |
documento | doc.status = aprovado |
contrato_venda | contrato venda → pending (vai para assinatura) |
distrato | distrato.status = aprovado |
alteracao_orcamento | orçamento atualizado |
chamado_assistencia | chamado avança fase |
projeto_revisao | revisão aceita |
Políticas
Settings → Aprovações & Compras → Políticas de Aprovação
Estrutura:
entity_type— qual tipocondition_field(ex: "valor"),condition_operator(gt/gte/eq),condition_value(ex: 10000)priority— quando há várias policies que casam, ganha a de maior prioritysteps(jsonb array):[ {"step_order":1, "label":"Gerente", "approver_type":"role", "approver_role_slug":"gerente-projetos"}, {"step_order":2, "label":"Diretor", "approver_type":"role", "approver_role_slug":"diretor"} ]
Tipos de aprovador
- role — qualquer usuário com aquele role na organização
- user — usuário específico (caso especial)
- dynamic — regra (ex: "gerente do projeto" — busca em project_members)
Delegação
Settings → Aprovações → Delegações
Configurável por:
- Período (start_at / end_at)
- Escopo: todos os entity_types ou subset
- Empreendimentos específicos
- Razão (auditoria)
Quando delegado tem aprovação atribuída via delegação, sistema marca delegated_to no assignment.
Escalação automática
Cada policy pode ter SLA (default 24h úteis). Aprovador parado por mais que isso:
- Recebe lembrete por email
- Após threshold (default 48h), assignment é escalado automaticamente para o role do nível acima
- Notifica gerente original e novo aprovador
Settings → Aprovações → Regras de Escalação
Regra de ouro: solicitante ≠ aprovador
Engine bloqueia automaticamente que solicitante seja também aprovador da mesma request. Se a alçada cair em si mesmo, escala para o nível acima.
Idempotência dos callbacks
Callbacks são idempotentes — uma re-execução não duplica. Por exemplo:
cotacao_decisaoverifica se já há PO criada para a cotação antes de criargerar_entrada_compra_recebimentoverifica se já há movimentos para o recebimento antes de criar
Isso protege contra retentativas em caso de erro de rede.
Auditoria
Toda decisão fica em approval_audit_log:
created— quando request criadaapproved/rejected— quando assignment resolvidodelegated— quando delegadoescalated— quando escaladoexpired— quando passou o threshold sem ação
Imutável. Correção é via evento compensatório (não exclusão).