Ponto de partida
Oito decisões
A primeira decide o tamanho da spec; a quarta decide se ela envelhece bem. As outras seis são acabamento — e acabamento é onde um cadastro de apoio vira ou não vira dado utilizável.
O motor de aprovação passa a gravar código e comentário. ApprovalAssignment ganha reasonCodeId ao lado do comment que já existe, e em rejeição e devolução os dois são obrigatórios: o código classifica, o comentário explica aquele caso. Foi a decisão cara — mexe na spec mais central do produto —, e foi para o lado maior porque rejeitar e devolver são os motivos de maior volume que um sistema de compras produz. Deixá-los de fora faria o transversal nascer sem a pergunta que o justifica.
Um catálogo só, e cada código declara onde vale. appliesTo é uma lista de ReasonCodeUsage, enum fechado de dezessete ações — mesma disciplina do ownerType do anexo: ação fora da lista não existe. Isso mantém DUPLICIDADE como uma linha válida na rejeição de item e na rejeição de requisição, e faz "por que rejeitamos ou devolvemos" ser uma consulta, não a união de dez.
Chave estrangeira e cópia congelada. O ato grava reasonCodeId, para agrupar e filtrar, mais reasonCode e reasonLabel, que não mudam nunca mais. É a regra 1 do mapa aplicada a um cadastro pequeno: renomear "Fora do orçamento" para "Estouro de verba" em 2027 não pode reescrever o que foi decidido em 2026. Duas colunas a mais em cada ato, e a leitura do passado fica correta para sempre.
Motivo é dado, nunca regra. Nenhum código muda fluxo, estado ou permissão. Nada de "bloqueado por sanção impede pedido novo" ou "devolvido por falta de anexo reabre no passo três" — quem decide comportamento é o motor de aprovação, a política de orçamento e o estado do documento, que têm spec, tela e trilha. Um catálogo editável pelo cliente que dispara ação é um segundo motor de regras invisível, e a primeira pergunta insolúvel do suporte seria "por que este fornecedor parou de aceitar pedido?".
requiresComment por código, e nenhum "Outro" de fábrica. É a única exceção da decisão 4, e passa porque é comportamento de formulário: não toca fluxo, estado nem permissão. O "Outro" fica de fora da semente por um motivo medido em campo: catálogo que nasce com "Outro" concentra a maior parte do uso nele em poucos meses — e aí a tabela custou trabalho para reproduzir exatamente o texto livre que ela veio substituir. O tenant que quiser o dele, cria.
O produto fixa o piso; o tenant só endurece. Onde uma spec já escreveu "motivo obrigatório" — rejeição, devolução, revogação fora de rascunho, bloqueio de fornecedor, substituição de cotação — continua obrigatório e não há configuração que afrouxe. Onde não está escrito, o tenant pode passar a exigir. Transformar invariante publicada em caixa de seleção é como uma regra de auditoria some sem ninguém decidir que ela sumiu.
Catálogo global do produto, com sobreposição por tenant — o padrão do NotificationTemplate. tenantId nulo é código do produto, disponível para todos; o tenant não edita o global — sobrepõe: uma linha própria com o mesmo code vale no lugar da global só naquele tenant, e é assim que ele renomeia, restringe, exige complemento ou desativa o que o produto entregou. A linha global nunca muda para ninguém. É a terceira vez que esta mecânica se repete no acervo, junto com NotificationTemplate e AttachmentType, e repetir uma mecânica conhecida vale mais que inventar a quarta. Um cliente novo abre o sistema com relatório funcionando no primeiro mês.
As cinco specs que já pedem motivo são corrigidas na mesma leva. Fluxo de Aprovação, Anexo, Cadastro de Item, Fornecedor e Moeda e Câmbio. Nenhuma tem código escrito, então o custo é edição de documento. Corrigir três e deixar duas seria, literalmente, criar a sexta implementação divergente de motivo enquanto se escreve a spec que existe para acabar com elas.
A frase que este documento existe para cumprir está no mapa de domínio desde a primeira versão: "campo-texto livre aqui é dado que nunca vira relatório". O teste da spec inteira é simples — depois dela, "quais foram os cinco motivos mais frequentes de devolução no trimestre" tem que ser uma consulta com GROUP BY, e não um pedido para alguém ler quatrocentos comentários.
Levantamento
Onde o motivo é exigido
Dezessete usos, e nenhum foi inventado aqui: quatorze saem de atos já escritos nas specs publicadas, com o documento de origem na última coluna; três são declarados agora para agregados que ainda não existem, pela mesma razão que o AttachmentOwnerType já lista Requisition — acrescentar valor a enum fechado depois de haver dado é migração, e agora é uma linha.
Cinco entraram em 09/09/2026. Três pelo mesmo argumento: ApprovalReject era o balde único de dez processos do motor. O aprovador que barra uma troca de conta bancária suspeita registrava "Demanda não justificada", porque os cinco códigos semeados para o uso foram escritos pensando em requisição. Cada processo já declara o uso da sua rejeição (rejectReasonUsage); o que faltava eram os valores para ele declarar.
Os outros dois vieram do razão orçamentário, no alinhamento das duas páginas: eram os últimos dois atos do acervo em que uma pessoa dá um motivo e o motivo não era contável. A justificativa da mudança de verba era text livre — a sexta maneira de resolver motivo, sobrevivente numa página que a leva original não tocou —, e o aceite da diferença de preço no three-way match estava do lado errado da fronteira, escrito como valor de enum de máquina.
| Uso | O ato | Piso | Como estava | Origem |
|---|---|---|---|---|
| ApprovalReject | Aprovador rejeita e encerra a instância | obrig | comment livre obrigatório | Fluxo |
| ApprovalReturn | Aprovador devolve para ajuste sem encerrar | obrig | comment livre obrigatório | Fluxo |
| BankAccountChangeReject | Proposta de alteração bancária recusada — rejeição da instância BANK_ACCOUNT_CHANGE, que passa a declarar este uso. Novo em 09/09/2026 | obrig | caía em ApprovalReject, com os códigos de requisição | Fornecedor |
| BudgetReject | Exercício orçamentário recusado — rejeição da instância BUDGET. Novo em 09/09/2026 | obrig | caía em ApprovalReject | Orçamento |
| BudgetChangeReject | Pedido de mudança de verba recusado — rejeição da instância BUDGET_CHANGE, a única porta pela qual verba aprovada muda. Novo em 09/09/2026 | obrig | caía em ApprovalReject | Orçamento |
| ItemReject | Item recusado na curadoria — rejeição da instância ITEM do motor, que declara este uso (RN-AP-30) | obrig | texto livre, mínimo 20 caracteres | Item |
| ItemDuplicateOverride | Forçar item que casou em camada bloqueante de duplicidade | obrig | justificativa livre registrada no GrydAudit | Item |
| SupplierReject | Cadastro ou homologação de fornecedor recusados — rejeição das instâncias SUPPLIER_REGISTRATION e SUPPLIER, que declaram este uso | obrig | texto livre | Fornecedor |
| SupplierBlock | Bloqueio de fornecedor | obrig | texto livre | Fornecedor |
| SupplierUnblock | Desbloqueio — o ato de volta também pede motivo, e é o que a auditoria procura | obrig | texto livre | Fornecedor |
| SupplierManualPromotion | Prospect → SpendAuthorized na mão, sem homologação completa | obrig | justificativa livre | Fornecedor |
| AttachmentRevoke | Revogar anexo de documento fora de rascunho | obrig | reasonCodeId? apontando para o vazio | Anexo |
| BudgetChangeJustify | Pedir suplementação, remanejamento ou devolução de verba — o motivo que a controladoria lê em dezembro. Novo em 09/09/2026 | obrig | justification, text livre | Orçamento |
| PriceVarianceAccept | Aceitar diferença de preço no three-way match, que grava Actual adicional. Novo em 09/09/2026 | obrig | valor PriceChange num enum de máquina | declarado |
| ExchangeRateSupersede | Substituir cotação vigente por retificação | obrig | varchar(200) livre | Moeda |
| DocumentCancel | Cancelar requisição, pedido ou cotação | a definir | — | declarado |
| SupplierAwardOverride | Escolher fornecedor que não é o de menor preço na equalização | a definir | — | declarado |
Bloquear um fornecedor é visível e alguém cobra. Desbloquear é o ato silencioso — e é exatamente o que uma auditoria de compras procura primeiro: quem liberou, quando, e sob qual justificativa. A spec de Fornecedor já exigia motivo "nos dois sentidos"; aqui ele deixa de ser texto e passa a ser contável.
O motor tem dez processos e nem todos merecem uso próprio: o critério é o código semeado mudar. Cotação, pedido e contrato recusam pelas mesmas razões de uma requisição — preço, prazo, demanda —, e um uso próprio para cada só multiplicaria linhas idênticas. Conta bancária, exercício e mudança de verba recusam por razões que não existem em nenhum dos outros: divergência de titularidade, verba insuficiente na origem, premissa errada. Uso novo se justifica pelo catálogo, nunca pelo processo.
O expurgo manual de arquivo do GrydFiles exige motivo, mas é ato de plataforma, com permissão de plataforma, num módulo que não conhece o domínio de compras — a mesma fronteira que separa alerta de operador de notificação de tenant. E aprovar não entra: motivo de ato positivo é um campo que ninguém preenche com honestidade, e a alçada já responde por que aquela pessoa podia aprovar.
Modelo
Mapa de entidades
tenantId nulo é código do produto; linha do tenant com o mesmo code o sobrepõe — o mesmo padrão do NotificationTemplate e do AttachmentType.
reasonCodeId ao lado do comment. É o consumidor de maior volume, e nele código e comentário são exigidos juntos — desde 04/09/2026 isso vale para toda rejeição do Nexio, porque toda aprovação é uma instância do motor; o processo declara qual uso a rejeição grava.
reasonCodeId? deixa de ser gancho órfão. Continua anulável — revogação de rascunho não pede motivo —, mas agora a regra de "fora de rascunho exige motivo nomeado" pode ser cumprida.
Uma entidade e um enum. Não existe entidade de "ocorrência de motivo" — o motivo mora no ato que o produziu, congelado, e é lá que ele é consultado. Criar uma tabela central de "todos os motivos já dados" seria construir um segundo diário do sistema ao lado do GrydAudit, que já registra tudo isso com quem, quando e a partir de qual estado.
Convenção de nomes: o documento é escrito em português e todo identificador é em inglês — entidade, atributo, valor de enum, rota, permissão e código de erro. A exceção é o conteúdo: os códigos semeados são em português, porque são texto de negócio que o comprador brasileiro lê na tela, do mesmo jeito que SimplesNacional e as siglas de unidade.
Domínio
Campos · ReasonCode
Herda de BaseEntity e implementa IOptionalTenant. Id, CreatedAt, CreatedBy, UpdatedAt, UpdatedBy e o bloco de soft delete vêm da base e não estão repetidos.
Legenda: obrig sempre · cond conforme o caso · opc opcional · gerado calculado pelo sistema.
| Campo | Tipo | Regra | |
|---|---|---|---|
| tenantId | Guid? | gerado | Nulo é código do produto, disponível para todos e mantido por release. Preenchido é código do tenant, do token — e, com o mesmo code de um global, é a sobreposição dele naquele tenant. Ver § Semente |
| code | string(40) | obrig | Maiúsculas, sem acento, com _ como separador. Único em cada camada — entre os globais, e dentro de cada tenant. Igual ao de um global não é colisão, é sobreposição: um DUPLICIDADE do tenant vale no lugar do DUPLICIDADE do produto só naquele tenant, e a lista resolvida nunca traz o mesmo código duas vezes — é isso que impede as duas linhas de competir no relatório. Imutável depois de usado; na sobreposição, imutável desde a criação, porque trocá-lo faria o global reaparecer em silêncio |
| name | string(120) | obrig | O que aparece no combo e no documento. Editável — e é justamente por ser editável que o ato guarda uma cópia, ver § Como o documento guarda |
| description | string(300)? | opc | Quando usar este e não o vizinho. Aparece como ajuda no combo, e é o que evita a lista de dezessete itens em que oito parecem a mesma coisa |
| appliesTo | enum[] | obrig | ReasonCodeUsage, ao menos um valor. É o que faz o combo de devolver requisição não oferecer "certidão vencida". Editar a lista não afeta atos já gravados — eles têm o snapshot |
| requiresComment | bool | obrig | Padrão false. Quando verdadeiro, o ato exige o complemento em texto além do código. É o único campo de comportamento desta entidade, e ele é de formulário: não muda fluxo, estado nem permissão. Ver decisão 5 |
| isActive | bool | obrig | Desativado some do combo e continua válido no histórico. Não existe apagar — ver § Ciclo |
| sortOrder | int | obrig | Ordem no combo, dentro de cada uso. Padrão pelo name; existe porque a lista boa põe os três motivos reais em cima e não em ordem alfabética |
Unicidade em duas camadas entre os não excluídos: code único entre os globais (tenantId nulo) e (tenantId, code) único dentro de cada tenant — dois índices parciais, ou um único que trate nulos como iguais; depende do banco, e a verificação vale na aplicação. A mesma chave nas duas camadas é a sobreposição, não colisão. Não existe campo de cor, ícone, severidade, peso ou pontuação — cada um deles é a porta de entrada de "motivo com comportamento", que a decisão 4 fechou.
Decisão 3
Como o documento guarda o motivo
Todo ato que registra motivo grava quatro coisas, e a mesma quádrupla em todos os consumidores — parte do valor do transversal é isso ser idêntico em cinco módulos.
| Campo no ato | Tipo | Para quê |
|---|---|---|
| reasonCodeId | Guid? | A chave. É por ela que se agrupa, filtra e se descobre que o motivo mais comum de devolução mudou depois da última mudança de política. Sem chave estrangeira quando o ato está em outro bounded context — a mesma regra do resto do produto |
| reasonCode | string(40)? | Congelado. O código como era no instante do ato |
| reasonLabel | string(120)? | Congelado. O rótulo como a pessoa o leu quando decidiu. É o que a tela do histórico exibe, sempre — nunca o name atual do catálogo |
| reasonComment | string(1000)? | O complemento em texto. Obrigatório quando o código tem requiresComment, ou quando a spec do consumidor já o exige por conta própria — no motor de aprovação, sempre |
Por que congelar, se é só um cadastro de apoio
Porque o rótulo é a frase que a pessoa leu antes de decidir. Um aprovador que rejeitou sob "Fora do orçamento" em março não rejeitou sob "Estouro de verba — verificar com controladoria", que é como alguém renomeou o código em novembro. Exibir o rótulo atual no histórico é reescrever a decisão de outra pessoa, e é o tipo de coisa que ninguém percebe até uma auditoria perguntar.
E por que manter a chave, se já congelou
Porque agrupar por string é como uma correção de digitação vira duas categorias no relatório. A chave responde "quantos", o snapshot responde "o quê" — as duas perguntas são diferentes e as duas são feitas.
Editar o rótulo é permitido; editar o código, não
name muda porque texto de tela sempre muda. code é imutável depois do primeiro uso: ele é a chave estável do relatório e do ExternalRef quando a conciliação com o ERP existir. Quem quer outro código cria outro e desativa o primeiro.
O ato guarda; o catálogo não sabe de nada
ReasonCode não tem contador de uso, não tem "última vez usado" e não tem coleção de ocorrências. Quem responde isso é a consulta sobre os próprios atos, e o GrydAudit guarda quem fez, quando e a partir de qual estado. Duas fontes para o mesmo fato é como se descobre que elas divergem.
Limite
Motivo de usuário × motivo de sistema
Nem tudo que se chama "motivo" no acervo entra aqui, e a linha já estava traçada em duas specs antes desta existir.
| O quê | Fica como | Por quê |
|---|---|---|
| ReasonCode | catálogo configurável | Uma pessoa escolheu, entre alternativas, e a escolha diz algo sobre o negócio daquele cliente |
| revocationSource | enum · Anexo | User · OwnerDiscarded · OrphanSweep · FileInfected. Diz quem concluiu, não por quê. Quando é User, aí sim vem um reasonCodeId junto |
BudgetEntry.reasonCode | enum · Orçamento | Por que o consumo mudou — PriceChange, DateChange, YearEndRollover… Quem escolhe é a máquina de estados do razão, não uma pessoa, e a lista é a mesma em todo cliente. A lista fechada mora logo abaixo, porque é aqui que o acervo inventaria vocabulário de motivo. Novo em 09/09/2026 |
| AbandonReason | enum · Notificação | FileInfected · PermissionLost · Expired… Ninguém configura o motivo pelo qual um antivírus recusou um arquivo |
| Hipótese de operação interestadual | enum fechado · Local de Entrega | A lista é a legislação, não a preferência do cliente. Aquela spec recusou catálogo configurável de propósito, e usou o argumento deste documento para justificar |
| Expurgo de arquivo | texto · plataforma | Ato do GrydFiles, com permissão de plataforma, num módulo que não conhece compras |
A regra de corte, em uma frase. Se a lista de valores mudaria de cliente para cliente, é ReasonCode. Se ela é a mesma em todo cliente porque vem da lei, do protocolo ou da máquina de estados, é enum — e enum é mais barato, mais rápido e não tem tela. Os dois convivem no mesmo ato sem conflito: revocationSource = User diz que uma pessoa revogou, e o reasonCodeId diz o que ela alegou.
Contrato vigente · revisão consolidada
Motivo do razão e identidade do lançamento
entryReason é enum de sistema obrigatório. reasonCodeId, reasonCode, reasonLabel e comentário no ato de origem registram a justificativa humana quando houver. Os dois conceitos podem coexistir: nota mais cara aceita tem entryReason=Post e motivo do uso PriceVarianceAccept.
| BudgetEntryReason · 12 valores | Por que existe | Estágio | operation derivada |
|---|---|---|---|
| Post | Fato original ou ajuste de valor econômico | Todos | Post |
| Liquidate | Parcela avança ou transfere obrigação contratual ao pedido | Commitment / Obligation | Liquidate |
| Reverse | Reversão de efeito vinculado à parcela corrigida | Todos | Reverse |
| DocumentCancelled | Cancelamento válido de parcela aberta | Commitment / Obligation | Reverse |
| Return | Mercadoria devolvida, com destino da reposição explícito | Actual / Obligation | Reverse |
| ReceiptReversal | Correção de recebimento identificado | Actual | Reverse |
| QuantityChange | Alteração efetiva de quantidade por parcela | Commitment / Obligation | Rebucket |
| DateChange | Reclassificação de competência, preservando datas dos fatos | Todos | Rebucket |
| ResidueRelease | Encerramento do saldo remanescente | Commitment / Obligation | Release |
| YearEndRollover | Transferência balanceada entre exercícios | Commitment / Obligation | Post |
| BudgetBackfill | Imputação balanceada ao ativar orçamento iniciado | Mesmo estágio da origem | Post |
| BudgetReclassification | Reorçamento ou correção classificatória por par balanceado | Mesmo estágio da origem | Rebucket |
Motivo não identifica operação: postingGroupId e entryOrdinal distinguem cada fato e entrada. Duas liquidações legítimas no mesmo estágio não colidem por terem o mesmo motivo. Reversão aponta entrada e parcela, nunca propaga cegamente por todos os recebimentos da cadeia.
Reclassificação e backfill têm negativo e positivo vinculados; não duplicam consumo. A enumeração é do produto e não é editável pelo tenant. Justificativa humana não cria nova regra de orçamento, autoridade ou fiscal.
Decisão 7
Semente do produto e catálogo do tenant
O produto semeia códigos globais — tenantId nulo — por uso. O tenant não edita o global: sobrepõe o que quer diferente com uma linha própria de mesmo code, desativa o que não serve pelo mesmo caminho, e cria os seus. Mesma mecânica do NotificationTemplate e do AttachmentType, e é a terceira repetição dela no acervo.
Como a sobreposição funciona. Para cada code, vale a linha do tenant quando existe; senão, a global. Sobrepor é POST /reason-codes com o code do global — a tela pré-preenche com ele — ou qualquer escrita sobre o id global (PUT, deactivate, activate), que materializa a linha do tenant com o corpo enviado. A partir daí as duas linhas são independentes: release que mude o global não alcança quem o sobrepôs, e a sobreposição é uma linha do tenant como outra qualquer — quem quer o texto do produto de volta edita a linha. Desativar um global é uma sobreposição com isActive = false: ele some do combo daquele tenant e continua ativo em todos os outros. Foi assim que o NotificationTemplate resolveu o mesmo problema, e é o que fecha o achado A6 da contra-análise de 03/09/2026.
| Código | Rótulo | Vale em | Complemento |
|---|---|---|---|
| FORA_DO_ORCAMENTO | Fora do orçamento | ApprovalReject · ApprovalReturn | — |
| PRECO_ACIMA_DO_MERCADO | Preço acima do mercado | ApprovalReject | — |
| DEMANDA_NAO_JUSTIFICADA | Demanda não justificada | ApprovalReject | — |
| FORNECEDOR_NAO_HOMOLOGADO | Fornecedor não homologado | ApprovalReject · SupplierAwardOverride | — |
| DUPLICIDADE | Duplicidade | ApprovalReject · ItemReject · DocumentCancel | exige |
| ESPECIFICACAO_INCOMPLETA | Especificação incompleta | ApprovalReturn · ItemReject | exige |
| FALTA_ANEXO | Falta anexo | ApprovalReturn | exige |
| CENTRO_DE_CUSTO_INCORRETO | Centro de custo incorreto | ApprovalReturn | — |
| QUANTIDADE_A_REVISAR | Quantidade a revisar | ApprovalReturn | — |
| FALTA_COTACAO | Falta cotação | ApprovalReturn | — |
| CATEGORIA_INCORRETA | Categoria incorreta | ItemReject | — |
| DESCRICAO_INSUFICIENTE | Descrição insuficiente | ItemReject | — |
| MESMO_MPN_EMBALAGEM_DIFERENTE | Mesmo MPN, embalagem diferente | ItemDuplicateOverride | exige |
| DOCUMENTACAO_INCOMPLETA | Documentação incompleta | SupplierReject | — |
| NAO_ATENDE_ESCOPO | Não atende ao escopo | SupplierReject | — |
| SANCAO_PUBLICA | Sanção pública | SupplierReject · SupplierBlock | exige |
| CERTIDAO_VENCIDA | Certidão vencida | SupplierBlock | — |
| DESEMPENHO_INSUFICIENTE | Desempenho insuficiente | SupplierBlock | exige |
| SOLICITACAO_DO_FORNECEDOR | Solicitação do fornecedor | SupplierBlock · SupplierUnblock | — |
| PENDENCIA_REGULARIZADA | Pendência regularizada | SupplierUnblock | exige |
| BLOQUEIO_INDEVIDO | Bloqueio indevido | SupplierUnblock | exige |
| URGENCIA_OPERACIONAL | Urgência operacional | SupplierManualPromotion | exige |
| FORNECEDOR_EXCLUSIVO | Fornecedor exclusivo | SupplierManualPromotion · SupplierAwardOverride | — |
| ARQUIVO_INCORRETO | Arquivo incorreto | AttachmentRevoke | — |
| VERSAO_SUPERADA | Versão superada | AttachmentRevoke | — |
| DADO_PESSOAL_INDEVIDO | Dado pessoal indevido | AttachmentRevoke | exige |
| ERRO_DE_DIGITACAO | Erro de digitação | ExchangeRateSupersede | — |
| RETIFICACAO_DO_BOLETIM | Retificação do boletim | ExchangeRateSupersede | — |
| PRAZO_DE_ENTREGA | Prazo de entrega | SupplierAwardOverride | — |
| DEMANDA_CANCELADA | Demanda cancelada | DocumentCancel | — |
| TITULARIDADE_DIVERGENTE | Titularidade divergente | BankAccountChangeReject | exige |
| CANAL_ANTERIOR_NAO_CONFIRMOU | Canal anterior não confirmou | BankAccountChangeReject | — |
| SOLICITACAO_NAO_RECONHECIDA | Solicitação não reconhecida pelo fornecedor | BankAccountChangeReject | exige |
| PREMISSA_INCORRETA | Premissa incorreta | BudgetReject · BudgetChangeReject | exige |
| VALOR_ACIMA_DO_TETO | Valor acima do teto da área | BudgetReject · BudgetChangeReject | — |
| SEM_VERBA_NA_ORIGEM | Sem verba no nó de origem | BudgetChangeReject | — |
| REVISAR_NO_PROXIMO_CICLO | Revisar no próximo ciclo | BudgetReject · BudgetChangeReject | — |
| DEMANDA_NAO_PREVISTA | Demanda não prevista no planejamento | BudgetChangeJustify | exige |
| REALOCACAO_DE_PRIORIDADE | Realocação de prioridade | BudgetChangeJustify | exige |
| ECONOMIA_EM_OUTRA_LINHA | Economia em outra linha | BudgetChangeJustify | — |
| ERRO_DE_PLANEJAMENTO | Erro de planejamento | BudgetChangeJustify | exige |
| VARIACAO_CAMBIAL | Variação cambial | PriceVarianceAccept | — |
| FRETE_NAO_PREVISTO | Frete não previsto no pedido | PriceVarianceAccept | — |
| REAJUSTE_CONTRATUAL | Reajuste contratual | PriceVarianceAccept | — |
| ERRO_NO_PEDIDO | Erro no pedido | PriceVarianceAccept | exige |
DUPLICIDADE em três, SANCAO_PUBLICA e FORA_DO_ORCAMENTO em dois. São eles que provam a decisão 2: com um catálogo por ação, cada um seria duas ou três linhas independentes, com rótulos que divergem na primeira edição e um relatório que precisa saber que DUPLICIDADE_ITEM e DUPLICIDADE_REQ são a mesma coisa.
Ela vai aparecer na implantação, e a resposta não é ceder nem recusar: é olhar qual motivo real está faltando. Se o cliente precisa de "Outro", o catálogo dele está incompleto, e a tela de administração existe para consertar isso em trinta segundos. Um "Outro" com complemento obrigatório é aceitável como paliativo do tenant; de fábrica, ele vira o caminho de menor resistência antes de o produto aprender qualquer coisa.
Operação
Desativar, renomear e histórico
Não existe apagar
Um código já usado tem reasonCodeId apontando para ele em atos que não podem perder sentido. A rota de exclusão não existe — como não existe DELETE /files/{id} no GrydFiles, e pela mesma razão. O que existe é desativar.
Desativar tira do combo e não toca no passado
O código some das telas de escolha e continua aparecendo, com o rótulo congelado, em todo ato que o usou. Filtro de relatório continua oferecendo códigos inativos — senão o histórico ficaria incompleto exatamente na hora de comparar antes e depois.
Não se desativa o último código ativo de um uso obrigatório
Se ApprovalReject exige motivo e alguém desativa o último código que vale ali, a rejeição fica impossível — e o cliente descobre isso na sexta-feira à tarde. A operação é recusada com REASON_CODE_LAST_ACTIVE_CONFLICT, dizendo qual uso ficaria vazio. É a única regra desta spec que existe para proteger o cliente dele mesmo, e ela cabe aqui porque a consequência é operação travada.
Tirar um uso de appliesTo é permitido e não é retroativo
Atos já gravados continuam com o motivo que tinham. O que muda é o combo daqui para a frente. A alternativa — recusar a edição enquanto houver uso — congelaria o catálogo no primeiro mês.
Segurança
Escopo, permissões e trilha
O catálogo é do tenant, não da empresa. Não há recorte por UserCompanyScope: um motivo de rejeição não muda porque a compra é da filial de Manaus, e criar o recorte agora significaria manter dois catálogos quase iguais em todo cliente multiempresa. O gancho fica declarado no fora de escopo, sem campo.
| Permissão | Semeada em | Observação |
|---|---|---|
| read:reason-codes | todos os papéis operacionais | Sem ela nenhum combo de motivo carrega — e como o motivo é obrigatório em cinco atos, sem ela metade do sistema para. É a permissão mais banal e a mais indispensável do produto |
| manage:reason-codes | Administrador | Criar, editar, ativar e desativar códigos do tenant, sobreposições incluídas. Não alcança a linha global — nenhuma permissão alcança; escrita sobre id global materializa a sobreposição do tenant |
Trilha no GrydAudit, na convenção fechada em 03/09: reason-code.created — marcada como sobreposição quando o code é de um global —, reason-code.updated — com valor anterior e novo de name e appliesTo —, reason-code.deactivated e reason-code.activated. O uso do motivo não é auditado aqui: ele é parte do ato que o produziu, e a trilha daquele ato já o registra.
Por que a trilha de edição de rótulo importa mais do que parece. É a única forma de responder "o que estava escrito neste combo em março", e essa pergunta aparece toda vez que alguém compara o relatório de dois trimestres e acha uma variação que não bate com a operação. O snapshot no ato resolve a leitura de um caso; a trilha do catálogo resolve a leitura de uma tendência.
Interface
Telas
Lista única com filtro por uso, mostrando código, rótulo, usos, se exige complemento e se está ativo. Códigos globais aparecem com marca de "do produto"; personalizar abre o formulário pré-preenchido e grava a sobreposição, que passa a aparecer com a marca "sobrepõe o produto"; desativar faz o mesmo com isActive desligado — o mesmo desenho da tela de tipos de anexo.
Ao desativar, a tela diz quantos atos já usaram aquele código e que eles não mudam. Ao tentar desativar o último de um uso obrigatório, diz qual operação ficaria impossível.
Um select filtrado pelo uso daquela ação, ordenado por sortOrder, com a description como texto de apoio. Quando o código escolhido tem requiresComment, o campo de texto aparece e passa a ser obrigatório — não fica desabilitado, aparece.
No motor de aprovação o comentário está sempre lá, porque a spec dele já o exigia antes desta existir.
Sempre o reasonLabel congelado, nunca o name atual. Quando há complemento, ele vem abaixo, atribuído a quem decidiu. É a diferença entre "Fora do orçamento" e "Fora do orçamento — a verba de março já foi para o contrato de manutenção".
Não há tela de relatório aqui. Motivo é uma dimensão — ele entra como corte nos relatórios de aprovação, de cadastro e de fornecedor, que são dos módulos deles. Construir "relatório de motivos" seria criar a única tela do produto que ninguém sabe por que abriria.
API
Endpoints
| Rota | Permissão | Devolve | Erros |
|---|---|---|---|
| GET /reason-codes | read:reason-codes | Os códigos do tenant e os globais que ele não sobrepôs, já resolvidos numa lista só — para cada code, a linha do tenant quando existe, senão a global. Filtro por usage — que é como todo combo chama —, e includeInactive para tela de administração e filtro de relatório | 400 |
| POST /reason-codes | manage:reason-codes | ReasonCodeDto. Nasce ativo, do tenant. Recusa code repetido no tenant; igual ao de um global cria a sobreposição, e a resposta a marca como tal | 400 · 409 · 422 |
| PUT /reason-codes/{id} | manage:reason-codes | Altera name, description, appliesTo, requiresComment e sortOrder. code não é editável depois do primeiro uso, nem nunca numa sobreposição. Em id global, materializa a sobreposição do tenant com o corpo enviado e devolve o id dela — o global não muda | 400 · 404 · 409 · 422 |
| PATCH /reason-codes/{id}/deactivate | manage:reason-codes | Desativa código do tenant. Em id global, materializa a sobreposição do tenant com isActive = false — é o caminho do tenant para não usar o que o produto entregou, e o global continua ativo nos outros tenants. Recusa quando deixaria um uso obrigatório sem nenhum código ativo na lista resolvida | 404 · 409 |
| PATCH /reason-codes/{id}/activate | manage:reason-codes | Reativa. Em id global, reativa a sobreposição do tenant se houver; sem ela, não há o que fazer. Idempotente | 404 |
| GET /reason-codes/usages | read:reason-codes | Os dezessete usos com o rótulo em pt-BR, o piso do produto e quantos códigos ativos cada um tem. É o que a tela de administração usa para avisar antes de o cliente se trancar do lado de fora | — |
Não existe DELETE. E não existe rota de "registrar motivo": o motivo é gravado pelo ato, na rota do consumidor — POST /approvals/{id}/reject, POST /attachments/{id}/revoke. Uma rota própria criaria um segundo caminho para o mesmo dado, e os dois divergiriam na primeira validação nova.
Contrato
Contrato de erros
O status HTTP é derivado do sufixo do código, nunca da mensagem: o mapeador do Core reconhece nove sufixos — _NOT_FOUND → 404 · _ALREADY_EXISTS, _ALREADY_ASSIGNED e _CONFLICT → 409 · _FORBIDDEN, _ACCESS_DENIED e _BLOCKED → 403 · _UNAUTHORIZED → 401 · _UNPROCESSABLE → 422 · sem sufixo → 400. _UNAVAILABLE → 503 seria o décimo: é extensão pedida ao Core pelo épico do GrydFiles, ainda não vigente — conferido no código da plataforma em 08/09/2026.
| Código | HTTP | Quando |
|---|---|---|
| REASON_CODE_NOT_FOUND | 404 | Não existe neste tenant e não é global |
| REASON_CODE_ALREADY_EXISTS | 409 | code repetido no tenant — inclusive quando a linha existente é a sobreposição de um global; a mensagem aponta a linha a editar. Coincidir com um global não é este erro: é sobreposição |
| REASON_CODE_IMMUTABLE_CONFLICT | 409 | Tentativa de alterar code depois do primeiro uso — ou, numa sobreposição, a qualquer momento |
| REASON_CODE_LAST_ACTIVE_CONFLICT | 409 | Desativar deixaria um uso obrigatório sem nenhum código ativo. Nomeia o uso |
| REASON_CODE_USAGE_UNKNOWN | 400 | Valor fora do enum de dezessete. Erro de programação do consumidor |
| REASON_CODE_USAGE_NOT_ALLOWED_UNPROCESSABLE | 422 | O código existe, mas não vale naquela ação. É o erro que impede "certidão vencida" numa devolução de requisição |
| REASON_CODE_INACTIVE_UNPROCESSABLE | 422 | Código desativado escolhido num ato novo. Acontece de verdade: tela aberta há uma hora, código desativado nesse meio-tempo |
| REASON_CODE_REQUIRED_UNPROCESSABLE | 422 | Ato sem motivo onde o piso do produto ou a configuração do tenant o exige. Substitui ITEM_REJECTION_REASON_REQUIRED e ATTACHMENT_REVOCATION_REASON_REQUIRED_UNPROCESSABLE |
| REASON_CODE_COMMENT_REQUIRED_UNPROCESSABLE | 422 | Código com requiresComment sem complemento. No motor de aprovação o erro que aparece continua sendo APPROVAL_COMMENT_REQUIRED, que é mais específico e já existia |
Invariantes
Regras de negócio
Nenhum ReasonCode muda fluxo, estado ou permissão. Se um comportamento precisa depender do motivo, o lugar dele é o motor de aprovação, a política de orçamento ou a máquina de estados do documento — todos com spec, tela e trilha. Esta é a regra que impede o cadastro de virar um motor de regras que o cliente edita sem saber o que quebrou.
reasonCodeId, reasonCode, reasonLabel e reasonComment?, com os mesmos nomes nos cinco consumidores. Uniformidade aqui é metade do valor do transversal: uma consulta atravessa os módulos sem tradução.
Nunca o name atual do catálogo. Exibir o rótulo de hoje sobre uma decisão de ontem é reescrever a decisão de outra pessoa.
appliesTo dizVerificado no servidor, no ato, não só no combo. REASON_CODE_USAGE_NOT_ALLOWED_UNPROCESSABLE.
Os dez usos com piso obrigatório continuam obrigatórios em todo cliente. A configuração do tenant só acrescenta exigência onde o produto não exige — nunca remove.
Nenhuma escrita do tenant alcança a linha global. Editar, desativar ou reativar um código global materializa a linha do tenant com o mesmo code, e é ela que passa a valer ali; os outros tenants continuam vendo o global. Mesma regra do NotificationTemplate e do AttachmentType.
code é imutável depois do primeiro usoÉ a chave estável do relatório e do futuro ExternalRef. Numa sobreposição, imutável desde a criação: trocá-lo faria o global reaparecer em silêncio. name muda à vontade, porque texto de tela sempre muda — e é exatamente por isso que existe o snapshot.
Só desativar. Um código já usado é referenciado por atos que não podem perder sentido — a mesma razão pela qual não existe DELETE /files/{id} no GrydFiles.
Desativar o último é recusado — contando sobre a lista resolvida do tenant, sobreposições incluídas — e nomeia a operação que ficaria impossível. É a única regra desta spec que protege o cliente dele mesmo, e ela existe porque a consequência é operação travada.
Atos gravados não mudam. O que muda é o combo daqui para a frente. O filtro de relatório continua oferecendo os inativos, senão o histórico ficaria incompleto na hora de comparar dois períodos.
requiresComment é o único comportamento permitidoComportamento de formulário, não de domínio. Não existe cor, ícone, severidade, peso, pontuação nem gancho — cada um deles é uma porta de entrada para a RN-RSN-01 ser contornada.
revocationSource, AbandonReason e a hipótese de operação interestadual não são ReasonCode. A regra de corte: se a lista mudaria de cliente para cliente, é catálogo; se vem da lei, do protocolo ou da máquina de estados, é enum.
Sem recorte por UserCompanyScope. Um motivo de rejeição não muda porque a compra é de outra filial, e dois catálogos quase iguais é como se cria divergência sem ganho.
Sem contador, sem "último uso", sem coleção de ocorrências. Quem responde é a consulta sobre os atos, e o GrydAudit guarda quem, quando e a partir de qual estado. Duas fontes para o mesmo fato divergem.
Não existe endpoint de "registrar motivo". Ele viaja no corpo de reject, return, revoke, block e supersede, validado no mesmo caso de uso que executa o ato.
Rejeição e devolução pedem os dois. O código classifica para o relatório; o comentário explica o caso para quem vai corrigir. Um sem o outro perde metade da função: só código não diz o que arrumar, só comentário não vira número. Desde 04/09/2026 toda aprovação humana é uma instância do motor, e cada processo declara o uso da sua rejeição (ItemReject em ITEM, SupplierReject em SUPPLIER_REGISTRATION e SUPPLIER, BankAccountChangeReject em BANK_ACCOUNT_CHANGE, BudgetReject em BUDGET, BudgetChangeReject em BUDGET_CHANGE, ApprovalReject nos demais) — logo o comentário passou a ser obrigatório também na recusa de item e de fornecedor, que antes exigiam só o código. requiresComment do código continua valendo por cima.
Valor de motivo que o sistema escolhe sozinho — BudgetEntryReason, revocationSource, abandonReason, hipótese de operação interestadual — não vira linha em ReasonCode: não tem tela, não tem sobreposição por tenant e não passa por requiresComment. O que o Motivo garante é que a lista fechada esteja escrita em um lugar só, com o estágio ou o estado em que cada valor aparece.
Por quê: o Orçamento já apontava para cá — "o catálogo canônico é o do Motivo" —, e sem esta regra a frase tinha duas leituras erradas: ou o razão ganhava tela de administração e sobreposição por tenant, ou os onze valores continuavam espalhados por dez trechos de prosa, com o relatório de "por que o consumo mudou" reconstruído de memória a cada release.
O porquê
O que isto responde
Um catálogo de motivos não é um cadastro de apoio — é a dimensão que faltava para o sistema dizer alguma coisa sobre o processo do cliente. Estas perguntas passam de "alguém lê os comentários" para GROUP BY:
- Por que as requisições voltam. Se 40% das devoluções são
FALTA_ANEXO, o conserto não é treinar comprador — é tornar o anexo obrigatório na submissão, e oAttachmentTypejá sabe fazer isso. - Quanto tempo a devolução custa. Cruzando o motivo com o tempo entre submissão e aprovação final, aparece quais motivos são caros e quais são triviais.
- Se a política de alçada está calibrada. Muitos
FORA_DO_ORCAMENTOpode significar orçamento apertado — ou linha de verba cadastrada errada.
- Quem desbloqueou fornecedor e sob qual alegação. A pergunta que uma auditoria de compras faz primeiro, e que hoje só se responde lendo texto.
- Com que frequência se força um item duplicado.
ItemDuplicateOverrideé exceção legítima; frequência alta é sinal de que a regra de duplicidade está mal calibrada. - Quantas promoções manuais de fornecedor por urgência. Uma é operação; vinte por mês é o processo de homologação sendo contornado.
Nenhuma dessas leituras é feita nesta spec — o motivo entra como corte nos relatórios dos módulos donos. O que esta spec garante é que o dado exista, em forma comparável, desde o primeiro dia de uso. É a diferença entre um sistema que registra decisões e um que aprende com elas.
Aferição
Referência de mercado
SAP chama de reason code e os usa em devolução, bloqueio de fatura e ajuste de estoque; a Oracle tem tabelas de lookup por tipo de transação; ServiceNow tem close codes. Todos convergiram no mesmo desenho — lista configurável, amarrada ao tipo de ato — porque texto livre não sobrevive ao primeiro relatório trimestral.
Deixar o motivo disparar comportamento. É a customização mais pedida e a mais cara: em pouco tempo há regra de negócio morando num cadastro que o cliente edita, e a área de suporte passa a investigar por que um documento se comportou diferente sem que nada tenha sido "programado". Daí a RN-RSN-01 ser a primeira desta spec.
Aparece como conveniência e vira o campeão de uso, com o texto livre de volta atrás dele. A prática que funciona é a que está aqui: não semear, e tratar pedido de "Outro" como sintoma de catálogo incompleto — que se resolve criando o motivo que falta.
Catálogo com quarenta opções por ação faz o operador escolher a primeira plausível, e o dado fica pior do que texto livre porque parece confiável. A semente traz de três a seis por uso, e a description existe para desempatar os parecidos. Crescer é fácil; podar depois é conversa política.
Limites
Fora de escopo
- Qualquer comportamento derivado do motivo. Bloquear, reabrir, escalonar, exigir aprovação extra: motor de aprovação, política de orçamento, máquina de estados do documento.
- Motivo de sistema.
revocationSource,AbandonReasone a hipótese de operação interestadual permanecem enums nas specs deles. - Expurgo de arquivo. Ato de plataforma do GrydFiles, com permissão de plataforma.
- Relatórios. Motivo é dimensão; a leitura mora nos módulos donos dos atos.
- Catálogo por empresa. Sem campo e sem tabela. Se aparecer o caso real, o desenho conhecido é o do
GLAccountCompany— e ele custa uma tabela de ligação, não uma migração. - Tradução do rótulo. O
Localevive no template de notificação; motivo em mais de um idioma só interessa com portal do fornecedor estrangeiro. externalCodepara o ERP. Sai junto com oExternalRef, que é o último transversal — e é ele que resolve "este motivo é o 27 do Protheus".- Motivo em ato positivo. Aprovar não pede motivo. Se um cliente quiser justificar aprovação excepcional, o lugar é a ressalva do motor, que já existe.
Com esta spec o gate da Requisition fica com dois itens, e nenhum deles é transversal. Sobra decidir se a linha nasce com contractId antes de o Contract existir, e reconfirmar que estoque continua fora — as duas são decisões de escopo, não de modelagem. O barramento que toda linha de documento vai congelar está inteiro: item, unidade, categoria, centro de custo com conta e percentual, fornecedor, fator de conversão, preço, as sete colunas de entrega, o anexo, o aviso e agora o motivo.
Contrato vigente · revisão consolidada
Motivos nas transações e na IA
Alteração/cancelamento de pedido, devolução, reclassificação, override de escolha de fornecedor e decisão humana sobre recomendação guardam motivo codificado e comentário quando necessário, ligados à revisão e à operação. Acrescentar usos específicos ao catálogo exige definição e semente na especificação do módulo; não reutilizar um uso incompatível só para evitar nova enumeração.
InsufficientData, ReturnedForBudget e falha de integração são resultados de sistema, não motivos escolhidos pelo usuário. A explicação da IA referencia dados e hipóteses; o motivo humano registra por que a pessoa aceitou ou alterou a sugestão, sem converter sugestão em aprovação.