Especificação · Transversal

Motivo

Todo ato negativo do sistema — rejeitar, devolver, cancelar, bloquear, revogar, substituir — hoje pede um motivo, e seis specs publicadas resolveram isso de seis maneiras diferentes: um campo de texto livre, outro com mínimo de vinte caracteres, um enum fechado, uma chave estrangeira apontando para uma tabela que não existe, uma justificativa em text livre. Esta spec escreve a sétima e apaga as outras seis. Uma entidade, um enum de dezoito ações, e a diferença entre saber que 40% das requisições voltam e ter quatrocentos comentários para ler.

consumido por Fluxo de Aprovação · Anexo · Item · Fornecedor · Moeda · Orçamento último transversal do gate da Requisition 1 entidade de configuração · 1 enum de 18 usos · 0 regras de comportamento v1.14 · 30/10/2026

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.

Decisão 1 · Adoção

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.

Decisão 2 · Estrutura

Um catálogo só, e cada código declara onde vale. appliesTo é uma lista de ReasonCodeUsage, enum fechado de dezoito 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.

Decisão 3 · Gravação

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.

Decisão 4 · Natureza

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?".

Decisão 5 · Complemento

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.

Decisão 6 · Obrigatoriedade

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.

Decisão 7 · Semente

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.

Decisão 8 · Alcance

As seis specs que já pedem motivo são corrigidas na mesma leva. Fluxo de Aprovação, Anexo, Cadastro de Item, Fornecedor, Moeda e Câmbio e Orçamento. Nenhuma tem código escrito, então o custo é edição de documento. Corrigir quatro e deixar duas seria, literalmente, criar a sétima 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

Dezoito usos, e nenhum foi inventado aqui: quinze 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.

Três deles existem porque o ApprovalReject não podia continuar sendo 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.

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.

O enum ReasonCodeUsage — e o que cada uso substitui
UsoO atoPisoComo estavaOrigem
ApprovalRejectAprovador rejeita e encerra a instânciaobrigcomment livre obrigatórioFluxo
ApprovalReturnAprovador devolve para ajuste sem encerrarobrigcomment livre obrigatórioFluxo
BankAccountChangeRejectProposta de alteração bancária recusada — rejeição da instância BANK_ACCOUNT_CHANGE, que declara este usoobrigcaía em ApprovalReject, com os códigos de requisiçãoFornecedor
BudgetRejectExercício orçamentário recusado — rejeição da instância BUDGETobrigcaía em ApprovalRejectOrçamento
BudgetChangeRejectPedido de mudança de verba recusado — rejeição da instância BUDGET_CHANGE, a única porta pela qual verba aprovada mudaobrigcaía em ApprovalRejectOrçamento
ItemRejectItem recusado na curadoria — rejeição da instância ITEM do motor, que declara este uso (RN-AP-30)obrigtexto livre, mínimo 20 caracteresItem
ItemDuplicateOverrideForçar item que casou em camada bloqueante de duplicidadeobrigjustificativa livre registrada no GrydAuditItem
SupplierRejectCadastro ou homologação de fornecedor recusados — rejeição das instâncias SUPPLIER_REGISTRATION e SUPPLIER, que declaram este usoobrigtexto livreFornecedor
SupplierBlockBloqueio de fornecedor — inteiro, no Fornecedor, ou só na relação de fornecimento de um item, no Cadastro de Itemobrigtexto livreFornecedor · Item
SupplierUnblockDesbloqueio, do fornecedor ou da relação — o ato de volta também pede motivo, e é o que a auditoria procuraobrigtexto livreFornecedor · Item
SupplierManualPromotionProspect → SpendAuthorized na mão, sem homologação completa. O ato é v2 — a promoção manual saiu da v1 com o autocadastro do fornecedor —, e o uso fica declarado, com a semente, para não nascer improvisado quando ele voltarobrigjustificativa livreFornecedor
AttachmentRevokeRevogar anexo de documento fora de rascunhoobrigreasonCodeId? apontando para o vazioAnexo
PersonalDataEraseEliminar dado pessoal a pedido do titular — o ato de eliminação do Anexo e o apagamento de comentário de aprovação no Fluxoobrigficava escondido em AttachmentRevokeAnexo · Fluxo
BudgetChangeJustifyPedir suplementação, remanejamento ou devolução de verba — o motivo que a controladoria lê em dezembroobrigjustification, text livreOrçamento
PriceVarianceAcceptAceitar diferença de preço no three-way match, que grava Actual adicionalobrigvalor PriceChange num enum de máquinadeclarado
ExchangeRateSupersedeSubstituir cotação vigente por retificaçãoobrigvarchar(200) livreMoeda
DocumentCancelCancelar requisição, pedido ou cotação — em qualquer estado, rascunho inclusiveobrig—declarado
SupplierAwardOverrideEscolher fornecedor que não é o de menor preço na equalizaçãoa definir pela spec da Cotação—declarado
Por que o desbloqueio está na lista

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.

Por que três, e não dez

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 que ficou de fora, e por quê

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

ReasonCode configuração · IOptionalTenant A entidade inteira. Código, rótulo, onde vale, se exige complemento e se está ativo. 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.
ReasonCodeUsage enum fechado · 18 valores As ações que aceitam motivo. Fechado de propósito: entrar na lista custa escrever onde o código é lido, gravado e exibido. Ver § Onde o motivo é exigido.
ApprovalAssignment Fluxo de Aprovação Ganha reasonCodeId ao lado do comment. É o consumidor de maior volume, e nele código e comentário são exigidos juntos — 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.
Attachment Anexo O 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.
Item · Supplier · ExchangeRate Cadastro de Item · Fornecedor · Moeda e Câmbio Trocam texto livre por código mais comentário opcional, nos cinco atos próprios da tabela de usos: sobreposição de duplicidade de item, bloqueio, desbloqueio e promoção manual de fornecedor, substituição de cotação. As rejeições dos três são do motor, no nó ao lado.
BudgetEntry · BudgetChangeRequest Orçamento Carregam a quádrupla anulável, preenchida só quando uma pessoa deu o motivo — o pedido de mudança de verba e o aceite da diferença de preço. O entryReason ao lado dela é enum de máquina e não vem deste catálogo.

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.

CampoTipoRegra
tenantIdGuid?geradoNulo é 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
codestring(40)obrigMaiú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
namestring(120)obrigO 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
descriptionstring(300)?opcQuando usar este e não o vizinho. Aparece como ajuda no combo, e é o que evita a lista em que oito itens parecem a mesma coisa
appliesToenum[]obrigReasonCodeUsage, 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
requiresCommentboolobrigPadrã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
isActiveboolobrigDesativado some do combo e continua válido no histórico. Não existe apagar — ver § Ciclo
sortOrderintobrigOrdem 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 seis módulos.

Campo no atoTipoPara quê
reasonCodeIdGuid?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
reasonCodestring(40)?Congelado. O código como era no instante do ato
reasonLabelstring(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
reasonCommentstring(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.

O relatório agrupa pelo código, e é por isso que o código é imutável

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 comoPor quê
ReasonCodecatálogo configurávelUma pessoa escolheu, entre alternativas, e a escolha diz algo sobre o negócio daquele cliente
revocationSourceenum · AnexoUser · OwnerDiscarded · OrphanSweep · FileInfected. Diz quem concluiu, não por quê. Quando é User, aí sim vem um reasonCodeId junto
BudgetEntry.entryReasonenum · OrçamentoPor que o consumo mudou — PriceRevision, 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
AbandonReasonenum · NotificaçãoFileInfected · PermissionLost · Expired… Ninguém configura o motivo pelo qual um antivírus recusou um arquivo
Hipótese de operação interestadualenum fechado · Local de EntregaA 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 arquivotexto · plataformaAto 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.

Motivo de sistema

O motivo do razão orçamentário

O Orçamento grava reasonCode em cada movimento do razão e diz que o catálogo canônico é o do Motivo. Ele é: a lista fechada está escrita aqui, e é enum, não catálogo configurável — pela regra de corte da seção anterior. Quem escolhe o valor é a máquina de estados do razão, o mesmo fato produz o mesmo valor em todo cliente, e nenhuma tela oferece as alternativas a ninguém. O que o Motivo faz por ele é o que faz pelos outros enums do acervo: guardar a lista num lugar só, para que o relatório de "por que o consumo mudou" não precise ser reconstruído de dez trechos de prosa.

O razão tem dois campos de motivo, e só um deles é este. entryReason é o enum abaixo, obrigatório em todo movimento, e responde "por que este movimento existe". Ao lado dele o BudgetEntry carrega a quádrupla anulável — reasonCodeId · reasonCode · reasonLabel —, preenchida só quando uma pessoa deu o motivo, e aí sim vinda deste catálogo. Um Adjustment de suplementação tem os dois: entryReason = Post e o código que o solicitante escolheu. Um estorno transitivo tem só o primeiro. O Orçamento diz isto com as mesmas palavras.

O enum BudgetEntryReason — quinze valores
ValorPor que o movimento existeEstágioOrigem
PostLançamento original — o consumo que nasce do documento, e a mudança de verba que nasce do pedido de mudançatodosRN-O08 · RN-O28
LiquidateNegativo que liquida uma entrada anterior ao avançar o estágio: a requisição empenhada que vira pedido, o pedido que vira notaCommitment · ObligationRN-O30
ReverseEstorno por propagação. A entrada caiu porque outra que a liquidava foi estornada — nada aconteceu com ela no mundotodosRN-O29
DocumentCancelledO documento foi cancelado. É a causa direta do estorno, e é o que o Reverse propaga cadeia acimatodosRN-O29
ReturnDevolução de mercadoria. Distinguir "devolveram" de "cancelaram o documento" é o que torna a devolução parcial legívelActual · ObligationRN-O29
ReceiptReversalRecebimento desfeito sem que o pedido morraActualRN-O29
QuantityChangeQuantidade do documento mudou — par de movimentosCommitment · ObligationOrçamento
DateChangeData de necessidade mudou — par de movimentos entre baldestodosRN-O11
PriceRevisionRevisão de preço do pedido aprovado, sobre a parte ainda não recebida. Sem par e sem liquidação: a diferença é consumo novo, não intenção da requisição. Não é a diferença aceita na nota, que é InvoicePriceVarianceObligationRN-O60
ResidueReleaseEncerramento explícito do documento libera o saldo abertoCommitment · ObligationRN-O05 · RN-O35
YearEndRolloverVirada de exercício. Negativo no exercício que fecha e positivo no novo, nos próprios estágios — desde que CarryIn/CarryOut saíram do enum de estágioCommitment · ObligationRN-O13
InvoicePriceVarianceDiferença de preço aceita na nota, no three-way match: grava Actual adicional, sem alvo a liquidar. É o único valor deste enum que tem a quádrupla do catálogo junto — o uso PriceVarianceAccept —, porque a máquina sabe que houve diferença e só a pessoa sabe por que ela foi aceitaActualRN-O31
ExchangeVarianceVariação cambial da nota. Actual sem alvo, e sem motivo de pessoa: ninguém decide uma variação de câmbio, ela aconteceActualRN-O31
RateCorrectionCorreção de taxa. Estorno do aberto e relançamento pela taxa certa, no próprio estágio — nunca por Adjustment, que mudaria a verba. Movimento de sistema, isento de Block: passa marcado e avisa os donos do nóCommitment · ObligationRN-O53
ReconversionReconversão da rodada nova. Estorno do aberto pela taxa antiga e relançamento pela taxa da rodada nova, no próprio estágio, quando o reenvio ou a reabertura reconverteu o documento. Movimento de sistema, sujeito a Block: o valor foi aprovado de novoCommitment · ObligationRN-O59
PriceChange saiu do enum — quem aceita é uma pessoa

Ele marcava a diferença de preço aceita no three-way match, e mudou duas vezes no mesmo dia. Primeiro de estágio: marcava Adjustment, que aumenta a verba — uma nota R$ 500 acima do pedido criava R$ 500 de orçamento pelo caminho de maior volume do sistema —, e passou a marcar Actual adicional. Depois de lado: aceitar uma nota mais cara é ato de uma pessoa, e a razão dela muda de cliente para cliente — variação cambial, frete não previsto, reajuste contratual, erro no pedido. Isso é a definição de ReasonCode, não de enum. O movimento passa a ter entryReason = InvoicePriceVariance e o reasonCode do uso PriceVarianceAccept, criado para ele. PriceRevision, que entrou depois, é outro fato: a revisão de preço do pedido antes do recebimento, decidida no documento e aprovada pelo motor (Orçamento, RN-O60). O nome é outro de propósito.

Quatro valores para estorno, e cada um responde uma pergunta

O estorno é transitivo: estornar uma entrada estorna toda entrada que a liquida, na mesma transação. Com um valor só, o extrato não distinguiria o pedido que morreu do recebimento que foi corrigido — e é essa a distinção que o comprador procura quando o saldo não bate. DocumentCancelled, Return e ReceiptReversal marcam a entrada estornada diretamente, cada um com a sua causa no mundo; Reverse marca as que caem por propagação, em que nada aconteceu — a requisição não foi cancelada, ela só deixou de estar liquidada. Quatro valores custam quatro linhas de enum; a alternativa custa um relatório que ninguém consegue ler.

Três valores saíram do enum, e nenhum deles se perdeu. PriceChange virou o uso PriceVarianceAccept deste catálogo, porque quem aceita é uma pessoa. Supplement e Transfer eram cópia do type do BudgetChangeRequest, que o movimento já alcança por sourceType e sourceId — duas fontes da verdade para o mesmo fato divergem na primeira correção, e a que fica é a do documento. Entraram no lugar Post, Liquidate e Reverse, que a página não tinha e sem os quais o campo obrigatório não fecha: todo movimento precisa de um valor, e a maioria dos movimentos do razão é lançamento ou liquidação. Os três repetem, de propósito, nomes do enum BudgetEntryOperation do Orçamento: entryReason é a coluna fina e operation é a grossa, derivada dela por tabela fixa — quatro razões de estorno dão uma operação de estorno. Não são dois campos independentes que podem divergir.

Estes quinze valores não entram em ReasonCodeUsage, não têm linha em ReasonCode, não aparecem em GET /reason-codes e não são sobrepostos por tenant — e é essa a razão inteira de eles não morarem no catálogo: um tenant que desativasse YearEndRollover quebraria a virada de exercício.

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. E a semente é conferida no startup: uso de piso obrigatório que fique sem nenhum código global ativo declarando-o em appliesTo derruba a inicialização (RN-RSN-21).

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.

Semente v1 — em português, porque é conteúdo de negócio e não identificador
CódigoRótuloVale emComplemento
FORA_DO_ORCAMENTOFora do orçamentoApprovalReject · ApprovalReturn—
PRECO_ACIMA_DO_MERCADOPreço acima do mercadoApprovalReject—
DEMANDA_NAO_JUSTIFICADADemanda não justificadaApprovalReject—
FORNECEDOR_NAO_HOMOLOGADOFornecedor não homologadoApprovalReject · SupplierAwardOverride—
DUPLICIDADEDuplicidadeApprovalReject · ItemReject · DocumentCancelexige
ESPECIFICACAO_INCOMPLETAEspecificação incompletaApprovalReturn · ItemRejectexige
FALTA_ANEXOFalta anexoApprovalReturnexige
CENTRO_DE_CUSTO_INCORRETOCentro de custo incorretoApprovalReturn—
QUANTIDADE_A_REVISARQuantidade a revisarApprovalReturn—
FALTA_COTACAOFalta cotaçãoApprovalReturn—
CATEGORIA_INCORRETACategoria incorretaItemReject—
DESCRICAO_INSUFICIENTEDescrição insuficienteItemReject—
MESMO_MPN_EMBALAGEM_DIFERENTEMesmo MPN, embalagem diferenteItemDuplicateOverrideexige
DOCUMENTACAO_INCOMPLETADocumentação incompletaSupplierReject—
NAO_ATENDE_ESCOPONão atende ao escopoSupplierReject—
SANCAO_PUBLICASanção públicaSupplierReject · SupplierBlockexige
CERTIDAO_VENCIDACertidão vencidaSupplierBlock—
DESEMPENHO_INSUFICIENTEDesempenho insuficienteSupplierBlockexige
SOLICITACAO_DO_FORNECEDORSolicitação do fornecedorSupplierBlock · SupplierUnblock—
PENDENCIA_REGULARIZADAPendência regularizadaSupplierUnblockexige
BLOQUEIO_INDEVIDOBloqueio indevidoSupplierUnblockexige
URGENCIA_OPERACIONALUrgência operacionalSupplierManualPromotionexige
FORNECEDOR_EXCLUSIVOFornecedor exclusivoSupplierManualPromotion · SupplierAwardOverride—
ARQUIVO_INCORRETOArquivo incorretoAttachmentRevoke—
VERSAO_SUPERADAVersão superadaAttachmentRevoke—
DADO_PESSOAL_INDEVIDODado pessoal indevidoPersonalDataEraseexige
ERRO_DE_DIGITACAOErro de digitaçãoExchangeRateSupersede—
RETIFICACAO_DO_BOLETIMRetificação do boletimExchangeRateSupersede—
PRAZO_DE_ENTREGAPrazo de entregaSupplierAwardOverride—
DEMANDA_CANCELADADemanda canceladaDocumentCancel—
TITULARIDADE_DIVERGENTETitularidade divergenteBankAccountChangeRejectexige
CANAL_ANTERIOR_NAO_CONFIRMOUCanal anterior não confirmouBankAccountChangeReject—
SOLICITACAO_NAO_RECONHECIDASolicitação não reconhecida pelo fornecedorBankAccountChangeRejectexige
PREMISSA_INCORRETAPremissa incorretaBudgetReject · BudgetChangeRejectexige
VALOR_ACIMA_DO_TETOValor acima do teto da áreaBudgetReject · BudgetChangeReject—
SEM_VERBA_NA_ORIGEMSem verba no nó de origemBudgetChangeReject—
REVISAR_NO_PROXIMO_CICLORevisar no próximo cicloBudgetReject · BudgetChangeReject—
DEMANDA_NAO_PREVISTADemanda não prevista no planejamentoBudgetChangeJustifyexige
REALOCACAO_DE_PRIORIDADERealocação de prioridadeBudgetChangeJustifyexige
ECONOMIA_EM_OUTRA_LINHAEconomia em outra linhaBudgetChangeJustify—
ERRO_DE_PLANEJAMENTOErro de planejamentoBudgetChangeJustifyexige
VARIACAO_CAMBIALVariação cambialPriceVarianceAccept—
FRETE_NAO_PREVISTOFrete não previsto no pedidoPriceVarianceAccept—
REAJUSTE_CONTRATUALReajuste contratualPriceVarianceAccept—
ERRO_NO_PEDIDOErro no pedidoPriceVarianceAcceptexige
Os códigos que valem em mais de um uso

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.

Nenhum "Outro" — e o que fazer com a pressão por ele

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 — mas não esvazia uso obrigatório

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. A guarda que faltava: o PUT que tira um uso de appliesTo passa pela mesma conferência da desativação — se aquele uso ficar sem nenhum código ativo na lista resolvida do tenant e for de piso obrigatório, a operação é recusada com REASON_CODE_LAST_ACTIVE_CONFLICT, dizendo qual uso ficaria vazio. Sem ela havia uma porta lateral: desativar o último código era recusado, tirar o uso do último código não era, e o efeito era o mesmo — a rejeição impossível na sexta-feira à tarde.

Sobreposição não afrouxa o piso de comentário

A sobreposição por tenant edita rótulo, descrição, ordem e estado — e pode acrescentar requiresComment onde o produto não exige. O que ela não faz é desligar o requiresComment de um código em que o produto o exige: a escrita é recusada com REASON_CODE_COMMENT_FLOOR_UNPROCESSABLE. É a decisão 6 aplicada ao campo, e não só ao uso.

Por quê: os códigos com comentário exigido não são os mais burocráticos, são os de controle — dado pessoal eliminado, substituição de cotação, promoção manual de fornecedor, aceite de diferença de preço. Um tenant que desligasse o comentário deles ficaria com a trilha inteira reduzida a um rótulo de lista fechada, que é exatamente o que não serve de prova.

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ãoSemeada emObservação
lookup:reason-codestodos os papéis operacionaisAbre só a rota do combo. Sem ela nenhum combo de motivo carrega — e como o motivo tem piso obrigatório em dezessete dos dezoito usos, sem ela metade do sistema para. É a permissão mais banal e a mais indispensável do produto. Não abre a tela de administração
read:reason-codesadministradores e supervisoresA listagem completa, a rota de usos, o item de menu e a tela de Administração › Motivos em leitura. Não implica lookup e vice-versa: cada rota exige uma só
create:reason-codes, update:reason-codes e delete:reason-codesAdministradorCriar, 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 de nomes do acervo: 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. O registro é gravado na mesma transação do ato (IAuditTrail.Stage, Gryd.IO 11.1.0), isolado por tenant, e fica legível depois da entrega pelo Worker.

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

Admin · catálogo de motivos

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 "Personalizado" (o código próprio do tenant continua sem marca); desativar faz o mesmo com isActive desligado — o mesmo desenho da tela de tipos de anexo.

Ao desativar, a tela diz que os atos já gravados continuam citando este motivo e não mudam — o catálogo não sabe onde o código foi usado (RN-RSN-14), então não há contagem. Ao tentar desativar o último de um uso obrigatório, diz qual operação ficaria impossível.

O combo, nos seis consumidores

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.

No histórico do documento

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".

O que não tem tela

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

RotaPermissãoDevolveErros
GET /reason-codes/lookuplookup:reason-codesReasonCodeLookupDto: o combo de um ato. usage é obrigatório; devolve só os códigos ativos do uso, já resolvidos e na ordem da listagem, mais o rótulo e o piso do uso — para o front não fixar quais usos são obrigatórios nem chamar /usages. Uso sem código ativo responde 200 com lista vazia400
GET /reason-codesread:reason-codesOs 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ório400
POST /reason-codescreate:reason-codesReasonCodeDto. Nasce ativo, do tenant. Recusa code repetido no tenant; igual ao de um global cria a sobreposição, e a resposta a marca como tal400 · 409 · 422
PUT /reason-codes/{id}update:reason-codesAltera 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 muda400 · 404 · 409 · 422
PATCH /reason-codes/{id}/deactivatedelete:reason-codesDesativa 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 resolvida404 · 409
PATCH /reason-codes/{id}/activateupdate:reason-codesReativa. Em id global, reativa a sobreposição do tenant se houver; sem ela, não há o que fazer. Idempotente404
GET /reason-codes/usagesread:reason-codesOs dezoito 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 dez sufixos — _NOT_FOUND → 404 · _ALREADY_EXISTS, _ALREADY_ASSIGNED e _CONFLICT → 409 · _FORBIDDEN, _ACCESS_DENIED e _BLOCKED → 403 · _UNAUTHORIZED → 401 · _UNPROCESSABLE → 422 · _UNAVAILABLE → 503 · sem sufixo → 400 —, e código exato vence sufixo.

CódigoHTTPQuando
REASON_CODE_NOT_FOUND404Não existe neste tenant e não é global
REASON_CODE_ALREADY_EXISTS409code 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_CONFLICT409Tentativa de alterar code depois do primeiro uso — ou, numa sobreposição, a qualquer momento
REASON_CODE_LAST_ACTIVE_CONFLICT409O ato deixaria um uso obrigatório sem nenhum código ativo na lista resolvida do tenant — desativar o último código que o declara, ou tirar o uso do appliesTo desse último código pelo PUT. Os dois caminhos, o mesmo código (RN-RSN-19). Nomeia o uso
REASON_CODE_COMMENT_FLOOR_UNPROCESSABLE422Sobreposição do tenant tentando desligar requiresComment de um código em que o produto o exige. Acrescentar continua permitido — o tenant endurece, nunca afrouxa
REASON_CODE_USAGE_UNKNOWN400Valor fora do enum de dezoito. Erro de programação do consumidor
REASON_CODE_USAGE_NOT_ALLOWED_UNPROCESSABLE422O 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_UNPROCESSABLE422Código desativado escolhido num ato novo. Acontece de verdade: tela aberta há uma hora, código desativado nesse meio-tempo
REASON_CODE_REQUIRED_UNPROCESSABLE422Ato 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_UNPROCESSABLE422Có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

RN-RSN-01Motivo é dado, nunca regra

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.

RN-RSN-02Todo ato com motivo grava a quádrupla

reasonCodeId, reasonCode, reasonLabel e reasonComment?, com os mesmos nomes nos seis consumidores. Uniformidade aqui é metade do valor do transversal: uma consulta atravessa os módulos sem tradução.

RN-RSN-03O rótulo exibido no histórico é o congelado

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.

RN-RSN-04O código só vale onde appliesTo diz

Verificado no servidor, no ato, não só no combo. REASON_CODE_USAGE_NOT_ALLOWED_UNPROCESSABLE.

RN-RSN-05O produto fixa o piso e o tenant só endurece

Os dezessete 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.

RN-RSN-06Código global não muda — o tenant sobrepõe

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.

RN-RSN-07code é 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.

RN-RSN-08Não existe apagar código

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.

RN-RSN-09Uso obrigatório nunca fica sem código ativo

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.

RN-RSN-10Desativar não é retroativo, e tirar um uso também não

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.

RN-RSN-11requiresComment é o único comportamento permitido

Comportamento 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.

RN-RSN-12Motivo de sistema continua enum

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.

RN-RSN-13O catálogo é do tenant, não da empresa

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.

RN-RSN-14O catálogo não sabe onde foi usado

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.

RN-RSN-15O motivo é gravado pelo ato, nunca por rota própria

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.

RN-RSN-16No motor de aprovação, código e comentário são exigidos juntos

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. 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.

E a devolução tem o seu próprio mapeamento, returnReasonUsage, que faltava escrito. Ele é ApprovalReturn em todos os processos, com uma exceção declarada: é nulo em BANK_ACCOUNT_CHANGE, porque troca de conta bancária não admite devolução — só aprova ou rejeita, e corrigir é rejeitar e abrir proposta nova. Uso nulo não é "cai no padrão": é a ausência do ato, e a rota de devolver não existe naquele processo. Enquanto os dois mapeamentos não estavam escritos lado a lado, a semente do motor gravava ApprovalReject para conta bancária e para orçamento, com códigos escritos pensando em requisição — e o relatório dizia que se rejeita conta bancária por "fora do orçamento".

O relatório que isto sustenta tem rota: GET /approvals/reason-summary no Fluxo, que agrega motivo × processo × período. É ele a resposta à pergunta da visão — "por que os pedidos voltam" —, e é ele que não fecha se o agrupamento for por reasonCodeId em vez de por reasonCode.

RN-RSN-17Motivo escolhido por máquina é enum, e a lista mora aqui

Valor de motivo que o sistema escolhe sozinho — BudgetEntryReason, revocationSource, o motivo de cancelamento de instância do motor, o motivo de revogação em cascata do portal, 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 valores do razão 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.

Os dois enums que entraram, e o que saiu. O cancelamento de instância do motor — a contestação do fornecedor cancela a proposta bancária em aprovação, e o valor é SupplierContested — é escolhido por máquina: ninguém abre um combo para dizer por que o fornecedor contestou. A revogação em cascata do portal — conta e links caídos porque o contato foi inativado, o e-mail dele mudou ou o fornecedor foi inativado — é do mesmo tipo: é consequência, não decisão. Os valores de cada um moram na página dona, e é ela que os lista; o que esta regra fixa é que nenhum dos dois vira código de catálogo, porque um tenant que desativasse SupplierContested desligaria a contestação. abandonReason saiu da lista com a fila de submissão diferida, que não existe mais na v1.

RN-RSN-18O relatório agrupa pelo código, não pelo id

Toda consulta agregada de motivo — GET /approvals/reason-summary à frente — agrupa por reasonCode, o texto congelado no ato, e nunca por reasonCodeId. O code é imutável desde a criação e é o mesmo no global e na sobreposição do tenant; o id, não. O rótulo que a barra exibe é o vigente — o name do código resolvido hoje —, e não o reasonLabel congelado em cada ato: quando os atos congelaram rótulos diferentes do mesmo code, eles somam numa barra só, com o rótulo de hoje. O congelado continua no detalhe do ato, que é onde ele responde o que estava escrito na tela naquele dia.

Por quê: sobrepor um código global materializa uma linha nova, com id novo. Agrupando por id, o mesmo motivo vira duas barras no relatório no dia em que o cliente troca o rótulo — metade dos atos antes da sobreposição, metade depois —, e a pergunta que esta spec existe para responder passa a ter duas respostas. É também a razão de a quádrupla congelar reasonCode junto com o id.

RN-RSN-19Nenhuma escrita deixa uso obrigatório sem código ativo

A guarda vale nos dois caminhos que esvaziam um uso, e os dois contam sobre a lista resolvida do tenant, sobreposições incluídas: desativar o último código ativo dele, e tirar aquele uso do appliesTo do último código que o declarava. Os dois respondem REASON_CODE_LAST_ACTIVE_CONFLICT, nomeando o uso.

Por quê: a regra existia só no primeiro caminho, e o segundo produzia o mesmo estado sem passar por ela — um uso de piso obrigatório sem nenhum código para escolher, e um ato que ninguém consegue completar. Guarda que cobre uma porta e não a outra não é guarda.

RN-RSN-20A sobreposição endurece o comentário e nunca o afrouxa

O tenant pode acrescentar requiresComment onde o produto não exige; desligar onde o produto exige é recusado com REASON_CODE_COMMENT_FLOOR_UNPROCESSABLE. Mesma direção única da decisão 6, agora escrita também para o campo.

Por quê: os códigos com comentário exigido são os de controle — dado pessoal eliminado, cotação substituída, diferença de preço aceita. Neles o rótulo de lista fechada é a classificação e o comentário é a prova; sem o comentário sobra um ato de controle cuja justificativa é uma palavra escolhida num combo.

RN-RSN-21A semeadura derruba se um uso obrigatório ficar sem código global

Na inicialização, para cada uso de piso obrigatório, o produto confere que existe ao menos um código global ativo que o declara em appliesTo. Uso obrigatório sem nenhum derruba a inicialização, nomeando o uso — a mesma disciplina da conferência de semente da Notificação.

Por quê: a RN-RSN-19 só alcança as rotas do tenant. Um release que tire um uso do appliesTo de um código global, ou que desative um código global, esvazia aquele uso em todo tenant sem sobreposição própria — e esvazia em silêncio, porque ninguém chamou rota nenhuma. Guarda que cobre a mão do cliente e não a do produto não é guarda.

RN-RSN-22Sobrepor um código global é ato de controle

Materializar a linha do tenant sobre um código global — por POST com o mesmo code, ou por qualquer escrita sobre o id global (PUT, deactivate, activate) — é ato de controle. Exige, nesta ordem: registro isolado por tenant, gravado na mesma transação do ato, com autor, code, valor global e valor sobreposto; e evento depois do commit — reason-code.created, marcado como sobreposição, ou reason-code.updated, .deactivated, .activated conforme o ato. O registro não sofre UPDATE destrutivo: corrige-se por versão nova, com autor. Enquanto o GrydAudit não estiver corrigido, o requisito fica declarado e a tela de sobreposição não entra em produção — mesma condição das outras telas de ato de controle do acervo.

Por quê: a sobreposição é o único caminho pelo qual o cliente muda o que o produto entregou, e ela muda o que o aprovador lê antes de decidir: desativar um código tira a opção honesta do combo, renomear troca a frase sob a qual a decisão passada foi tomada, e endurecer ou afrouxar requiresComment muda o que fica escrito. Nada disso aparece no ato auditado — lá fica o motivo congelado, correto e mudo sobre quem mexeu no catálogo. É a mesma razão pela qual mudança de escopo por empresa e substituição de taxa são atos de controle: o efeito é invisível no dado que ele produz.

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:

Sobre o próprio processo
  • 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 o AttachmentType já 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_ORCAMENTO pode significar orçamento apertado — ou linha de verba cadastrada errada.
Sobre controle e auditoria
  • 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

O conceito é antigo e tem nome

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.

O erro que todos cometem uma vez

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.

O segundo erro é o "Outro"

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.

E o terceiro é a lista longa demais

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 um a seis por uso — um só onde a decisão é binária, como a sobreposição de duplicidade de item e a eliminação a pedido do titular —, e a description existe para desempatar os parecidos. Crescer é fácil; podar depois é conversa política.

Limites

Fora de escopo

Fora — é de outro lugar
  • 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, AbandonReason e 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.
Fora agora, gancho previsto
  • 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 Locale vive no template de notificação; motivo em mais de um idioma só interessa com portal do fornecedor estrangeiro.
  • externalCode para o ERP. Sai junto com o ExternalRef, 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.

Registro

Histórico de revisões

VersãoDataO que mudou
v1.1430/10/2026Decisões de produto de 30/10, antes das telas. Nova permissão lookup:reason-codes e a rota GET /reason-codes/lookup: o combo deixa de depender de read, que passa a abrir só a administração (administradores e supervisores). As três permissões não se implicam. Tela: some a contagem de atos que usaram o código (inviável pela RN-RSN-14) e a marca "sobrepõe o produto" vira "Personalizado". Backlog: ordem do combo por uso (hoje sortOrder é único por código).
v1.1321/09/2026Validação de 18/09, achado 16 (decisão do Roger). BudgetEntryReason: catorze → quinze valores — entra PriceRevision, a revisão de preço do pedido aprovado (Orçamento, RN-O60). O nome não é PriceChange porque esse valor já existiu com outro sentido. De carona: o card de PriceVarianceAccept dizia entryReason = Post (é InvoicePriceVariance), e a linha da tabela de fronteira chamava o campo de reasonCode e citava PriceChange como valor vigente. Usos e semente inalterados.
v1.1221/09/2026Validação de 18/09, achado 21a (decisão do Roger). Piso de DocumentCancel: obrigatório, em requisição, pedido e cotação, em qualquer estado. Usos com piso obrigatório: dezesseis → dezessete dos dezoito; só SupplierAwardOverride segue a definir, pela spec da Cotação. A semente já cumpre a RN-RSN-21 para o uso: DUPLICIDADE e DEMANDA_CANCELADA o declaram em appliesTo.
v1.1120/09/2026Recuo da validação de 18/09 (achado 21b). O piso de SupplierAwardOverride passa de “a definir” para “a definir pela spec da Cotação”: não é pendência da base. SupplierBlock e SupplierUnblock passam a valer também para o bloqueio da relação de fornecimento de um item (Cadastro de Item, RN-ITM-15); nenhum uso novo — continuam dezoito.
v1.1019/09/2026Decisão D3 (validação de 18/09). BudgetEntryReason: treze → catorze valores — entra Reconversion, o estorno e relançamento do aberto quando a rodada nova de aprovação reconverte o documento (Orçamento, RN-O59; Moeda e Câmbio, RN-CUR-22). Diferente da RateCorrection, é sujeito a Block.
v1.913/09/2026Correção da validação R7. Contagens do cabeçalho e do mapa (BX-01): a linha de resumo dizia enum de 17 usos e o cartão do nó ReasonCodeUsage dizia 12 valores, contra os dezoito contados na tabela; os consumidores são seis — o Orçamento entrou na linha de resumo, na lede e na decisão 8, que ainda diziam cinco —, e o nó Item · Supplier · ExchangeRate volta a cinco atos próprios, que é o que a tabela tem, com os cinco nomeados. Ata B11: a § Trilha passa a carregar a ressalva do acervo — o GrydAudit está em correção pelo time do Gryd.IO e o Nexio não o mapeia até a correção da plataforma. RN-RSN-22 nova: sobrepor código global é ato de controle, com registro isolado por tenant na transação do ato e evento pós-commit — a B11 já o listava e a página dona não o declarava. REASON_CODE_LAST_ACTIVE_CONFLICT: a linha do contrato de erros passa a descrever os dois caminhos da RN-RSN-19, o deactivate e o PUT de appliesTo.
v1.812/09/2026Correção da validação R6. Contagens recontadas na página: os usos são dezoito em todos os lugares — a lede de § Onde o motivo é exigido e a rota GET /reason-codes/usages ainda diziam dezessete contra dezoito na tabela e no REASON_CODE_USAGE_UNKNOWN —, e a decomposição passou a quinze + três; o motivo tem seis consumidores, não cinco (o razão orçamentário carrega a quádrupla e entrou no mapa); o contrato de erros passa a dez sufixos, com _UNAVAILABLE → 503 vigente e código exato vencendo o sufixo (Ata B12). RN-RSN-21 nova: a semeadura derruba se um uso de piso obrigatório ficar sem nenhum código global ativo que o declare — a RN-RSN-19 só alcançava as rotas do tenant, e um release esvaziaria o uso em todo cliente sem sobreposição, em silêncio. RN-RSN-18: a barra do relatório exibe o rótulo vigente; o congelado fica no detalhe do ato. As duas guardas de uso vazio passam a contar sobre a lista resolvida do tenant, com a mesma redação. Forma (Ata B18): as notas de auto-história saíram do corpo normativo.
v1.712/09/2026Validação de 10/09 e as cinco rodadas de validação da onda 1. Uso novo PersonalDataErase (17 → 18): o DADO_PESSOAL_INDEVIDO sai de AttachmentRevoke, onde era enganoso — revogar fora de rascunho não libera a referência, e quem escolhia o código achava que tinha eliminado —, e passa a valer para o ato de eliminação do Anexo e para o apagamento de comentário de aprovação; dois consumidores, um uso. Integridade do catálogo: o relatório agrega por reasonCode e nunca por reasonCodeId, porque sobrepor materializa linha nova e partiria o número em dois (RN-RSN-18); a guarda de uso obrigatório vazio passa a valer também no PUT de appliesTo, que era a porta lateral da mesma regra (RN-RSN-19); a sobreposição do tenant não desliga requiresComment onde o produto exige — REASON_CODE_COMMENT_FLOOR_UNPROCESSABLE (RN-RSN-20). Motor: returnReasonUsage escrito ao lado do rejectReasonUsage, nulo em BANK_ACCOUNT_CHANGE porque troca de conta não admite devolução, e a rota do relatório nomeada (RN-RSN-16). BudgetEntryReason: onze → treze valores — sai BudgetBackfill com o fim do backfill; entram InvoicePriceVariance (o único do enum que traz a quádrupla junto), ExchangeVariance e RateCorrection. abandonReason sai da lista da RN-RSN-17 com a submissão diferida; entram o cancelamento de instância do motor e a revogação em cascata do portal, os dois como enum da página dona. SupplierManualPromotion marcado como ato de v2. Contagens contadas na página: dezoito usos, dezesseis com piso obrigatório, treze valores no enum do razão, semente de um a seis códigos por uso.
v1.609/09/2026Alinhamento com o Orçamento: usos BudgetChangeJustify e PriceVarianceAccept, semente em 45 códigos, BudgetEntryReason recomposto — PriceChange vira uso de catálogo porque quem aceita é uma pessoa, Supplement e Transfer saem por serem cópia do tipo do pedido de mudança, Post, Liquidate e Reverse entram — e a regra de que motivo escolhido por máquina é enum (RN-RSN-17).
v1.1 – v1.504–09/09/2026Motor universal: cada processo declara o uso da sua rejeição. Sobreposição por tenant com unicidade em duas camadas e code imutável. Os três usos de rejeição que saíram do balde único ApprovalReject — conta bancária, orçamento e mudança de orçamento —, pelo critério de que uso novo se justifica pelo catálogo e nunca pelo processo.
v1.003/09/2026Primeira versão: catálogo único com appliesTo, FK mais snapshot na quádrupla, motivo como dado e nunca como regra, requiresComment por código como única exceção, o produto fixando o piso e o tenant só endurecendo, semente global com sobreposição por tenant.