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 doze 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
Doze usos, e nenhum foi inventado aqui: dez saem de atos já escritos nas specs publicadas, com o documento de origem na última coluna; dois 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.
| 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 |
| 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 |
| 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 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 doze 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 |
| 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.
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 | — |
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-code | 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-code | 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-code | 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-code | 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-code | 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-code | 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-code | 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-code | Os doze 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 doze. 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, 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.
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.