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 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.
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 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.
| Uso | O ato | Piso | Como estava | Origem |
|---|---|---|---|---|
| ApprovalReject | Aprovador rejeita e encerra a instância | obrig | comment livre obrigatório | Fluxo |
| ApprovalReturn | Aprovador devolve para ajuste sem encerrar | obrig | comment livre obrigatório | Fluxo |
| BankAccountChangeReject | Proposta de alteração bancária recusada — rejeição da instância BANK_ACCOUNT_CHANGE, que declara este uso | obrig | caía em ApprovalReject, com os códigos de requisição | Fornecedor |
| BudgetReject | Exercício orçamentário recusado — rejeição da instância BUDGET | obrig | caía em ApprovalReject | Orçamento |
| BudgetChangeReject | Pedido de mudança de verba recusado — rejeição da instância BUDGET_CHANGE, a única porta pela qual verba aprovada muda | obrig | caía em ApprovalReject | Orçamento |
| ItemReject | Item recusado na curadoria — rejeição da instância ITEM do motor, que declara este uso (RN-AP-30) | obrig | texto livre, mínimo 20 caracteres | Item |
| ItemDuplicateOverride | Forçar item que casou em camada bloqueante de duplicidade | obrig | justificativa livre registrada no GrydAudit | Item |
| SupplierReject | Cadastro ou homologação de fornecedor recusados — rejeição das instâncias SUPPLIER_REGISTRATION e SUPPLIER, que declaram este uso | obrig | texto livre | Fornecedor |
| SupplierBlock | Bloqueio de fornecedor — inteiro, no Fornecedor, ou só na relação de fornecimento de um item, no Cadastro de Item | obrig | texto livre | Fornecedor · Item |
| SupplierUnblock | Desbloqueio, do fornecedor ou da relação — o ato de volta também pede motivo, e é o que a auditoria procura | obrig | texto livre | Fornecedor · Item |
| SupplierManualPromotion | Prospect → 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 voltar | obrig | justificativa livre | Fornecedor |
| AttachmentRevoke | Revogar anexo de documento fora de rascunho | obrig | reasonCodeId? apontando para o vazio | Anexo |
| PersonalDataErase | Eliminar dado pessoal a pedido do titular — o ato de eliminação do Anexo e o apagamento de comentário de aprovação no Fluxo | obrig | ficava escondido em AttachmentRevoke | Anexo · Fluxo |
| BudgetChangeJustify | Pedir suplementação, remanejamento ou devolução de verba — o motivo que a controladoria lê em dezembro | obrig | justification, text livre | Orçamento |
| PriceVarianceAccept | Aceitar diferença de preço no three-way match, que grava Actual adicional | obrig | valor PriceChange num enum de máquina | declarado |
| ExchangeRateSupersede | Substituir cotação vigente por retificação | obrig | varchar(200) livre | Moeda |
| DocumentCancel | Cancelar requisição, pedido ou cotação — em qualquer estado, rascunho inclusive | obrig | — | declarado |
| SupplierAwardOverride | Escolher fornecedor que não é o de menor preço na equalização | a definir pela spec da Cotação | — | declarado |
Bloquear um fornecedor é visível e alguém cobra. Desbloquear é o ato silencioso — e é exatamente o que uma auditoria de compras procura primeiro: quem liberou, quando, e sob qual justificativa. A spec de Fornecedor já exigia motivo "nos dois sentidos"; aqui ele deixa de ser texto e passa a ser contável.
O motor tem dez processos e nem todos merecem uso próprio: o critério é o código semeado mudar. Cotação, pedido e contrato recusam pelas mesmas razões de uma requisição — preço, prazo, demanda —, e um uso próprio para cada só multiplicaria linhas idênticas. Conta bancária, exercício e mudança de verba recusam por razões que não existem em nenhum dos outros: divergência de titularidade, verba insuficiente na origem, premissa errada. Uso novo se justifica pelo catálogo, nunca pelo processo.
O expurgo manual de arquivo do GrydFiles exige motivo, mas é ato de plataforma, com permissão de plataforma, num módulo que não conhece o domínio de compras — a mesma fronteira que separa alerta de operador de notificação de tenant. E aprovar não entra: motivo de ato positivo é um campo que ninguém preenche com honestidade, e a alçada já responde por que aquela pessoa podia aprovar.
Modelo
Mapa de entidades
tenantId nulo é código do produto; linha do tenant com o mesmo code o sobrepõe — o mesmo padrão do NotificationTemplate e do AttachmentType.
reasonCodeId ao lado do comment. É o consumidor de maior volume, e nele código e comentário são exigidos juntos — 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.
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.
| 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 em que oito itens 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 seis 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.
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 como | Por quê |
|---|---|---|
| ReasonCode | catálogo configurável | Uma pessoa escolheu, entre alternativas, e a escolha diz algo sobre o negócio daquele cliente |
| revocationSource | enum · Anexo | User · OwnerDiscarded · OrphanSweep · FileInfected. Diz quem concluiu, não por quê. Quando é User, aí sim vem um reasonCodeId junto |
BudgetEntry.entryReason | enum · Orçamento | Por 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 |
| 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.
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.
| Valor | Por que o movimento existe | Estágio | Origem |
|---|---|---|---|
| Post | Lançamento original — o consumo que nasce do documento, e a mudança de verba que nasce do pedido de mudança | todos | RN-O08 · RN-O28 |
| Liquidate | Negativo que liquida uma entrada anterior ao avançar o estágio: a requisição empenhada que vira pedido, o pedido que vira nota | Commitment · Obligation | RN-O30 |
| Reverse | Estorno por propagação. A entrada caiu porque outra que a liquidava foi estornada — nada aconteceu com ela no mundo | todos | RN-O29 |
| DocumentCancelled | O documento foi cancelado. É a causa direta do estorno, e é o que o Reverse propaga cadeia acima | todos | RN-O29 |
| Return | Devolução de mercadoria. Distinguir "devolveram" de "cancelaram o documento" é o que torna a devolução parcial legível | Actual · Obligation | RN-O29 |
| ReceiptReversal | Recebimento desfeito sem que o pedido morra | Actual | RN-O29 |
| QuantityChange | Quantidade do documento mudou — par de movimentos | Commitment · Obligation | Orçamento |
| DateChange | Data de necessidade mudou — par de movimentos entre baldes | todos | RN-O11 |
| PriceRevision | Revisã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 é InvoicePriceVariance | Obligation | RN-O60 |
| ResidueRelease | Encerramento explícito do documento libera o saldo aberto | Commitment · Obligation | RN-O05 · RN-O35 |
| YearEndRollover | Virada 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ágio | Commitment · Obligation | RN-O13 |
| InvoicePriceVariance | Diferenç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 aceita | Actual | RN-O31 |
| ExchangeVariance | Variação cambial da nota. Actual sem alvo, e sem motivo de pessoa: ninguém decide uma variação de câmbio, ela acontece | Actual | RN-O31 |
| RateCorrection | Correçã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 · Obligation | RN-O53 |
| Reconversion | Reconversã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 novo | Commitment · Obligation | RN-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.
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.
| 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 | PersonalDataErase | exige |
| ERRO_DE_DIGITACAO | Erro de digitação | ExchangeRateSupersede | — |
| RETIFICACAO_DO_BOLETIM | Retificação do boletim | ExchangeRateSupersede | — |
| PRAZO_DE_ENTREGA | Prazo de entrega | SupplierAwardOverride | — |
| DEMANDA_CANCELADA | Demanda cancelada | DocumentCancel | — |
| TITULARIDADE_DIVERGENTE | Titularidade divergente | BankAccountChangeReject | exige |
| CANAL_ANTERIOR_NAO_CONFIRMOU | Canal anterior não confirmou | BankAccountChangeReject | — |
| SOLICITACAO_NAO_RECONHECIDA | Solicitação não reconhecida pelo fornecedor | BankAccountChangeReject | exige |
| PREMISSA_INCORRETA | Premissa incorreta | BudgetReject · BudgetChangeReject | exige |
| VALOR_ACIMA_DO_TETO | Valor acima do teto da área | BudgetReject · BudgetChangeReject | — |
| SEM_VERBA_NA_ORIGEM | Sem verba no nó de origem | BudgetChangeReject | — |
| REVISAR_NO_PROXIMO_CICLO | Revisar no próximo ciclo | BudgetReject · BudgetChangeReject | — |
| DEMANDA_NAO_PREVISTA | Demanda não prevista no planejamento | BudgetChangeJustify | exige |
| REALOCACAO_DE_PRIORIDADE | Realocação de prioridade | BudgetChangeJustify | exige |
| ECONOMIA_EM_OUTRA_LINHA | Economia em outra linha | BudgetChangeJustify | — |
| ERRO_DE_PLANEJAMENTO | Erro de planejamento | BudgetChangeJustify | exige |
| VARIACAO_CAMBIAL | Variação cambial | PriceVarianceAccept | — |
| FRETE_NAO_PREVISTO | Frete não previsto no pedido | PriceVarianceAccept | — |
| REAJUSTE_CONTRATUAL | Reajuste contratual | PriceVarianceAccept | — |
| ERRO_NO_PEDIDO | Erro no pedido | PriceVarianceAccept | exige |
DUPLICIDADE em três, SANCAO_PUBLICA e FORA_DO_ORCAMENTO em dois. São eles que provam a decisão 2: com um catálogo por ação, cada um seria duas ou três linhas independentes, com rótulos que divergem na primeira edição e um relatório que precisa saber que DUPLICIDADE_ITEM e DUPLICIDADE_REQ são a mesma coisa.
Ela vai aparecer na implantação, e a resposta não é ceder nem recusar: é olhar qual motivo real está faltando. Se o cliente precisa de "Outro", o catálogo dele está incompleto, e a tela de administração existe para consertar isso em trinta segundos. Um "Outro" com complemento obrigatório é aceitável como paliativo do tenant; de fábrica, ele vira o caminho de menor resistência antes de o produto aprender qualquer coisa.
Operação
Desativar, renomear e histórico
Não existe apagar
Um código já usado tem reasonCodeId apontando para ele em atos que não podem perder sentido. A rota de exclusão não existe — como não existe DELETE /files/{id} no GrydFiles, e pela mesma razão. O que existe é desativar.
Desativar tira do combo e não toca no passado
O código some das telas de escolha e continua aparecendo, com o rótulo congelado, em todo ato que o usou. Filtro de relatório continua oferecendo códigos inativos — senão o histórico ficaria incompleto exatamente na hora de comparar antes e depois.
Não se desativa o último código ativo de um uso obrigatório
Se ApprovalReject exige motivo e alguém desativa o último código que vale ali, a rejeição fica impossível — e o cliente descobre isso na sexta-feira à tarde. A operação é recusada com REASON_CODE_LAST_ACTIVE_CONFLICT, dizendo qual uso ficaria vazio. É a única regra desta spec que existe para proteger o cliente dele mesmo, e ela cabe aqui porque a consequência é operação travada.
Tirar um uso de appliesTo é permitido e não é retroativo — 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ão | Semeada em | Observação |
|---|---|---|
| lookup:reason-codes | todos os papéis operacionais | Abre 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-codes | administradores e supervisores | A 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-codes | Administrador | Criar, editar, ativar e desativar códigos do tenant, sobreposições incluídas. Não alcança a linha global — nenhuma permissão alcança; escrita sobre id global materializa a sobreposição do tenant |
Trilha no GrydAudit, na convenção 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
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.
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/lookup | lookup:reason-codes | ReasonCodeLookupDto: 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 vazia | 400 |
| GET /reason-codes | read:reason-codes | Os códigos do tenant e os globais que ele não sobrepôs, já resolvidos numa lista só — para cada code, a linha do tenant quando existe, senão a global. Filtro por usage — que é como todo combo chama —, e includeInactive para tela de administração e filtro de relatório | 400 |
| POST /reason-codes | create:reason-codes | ReasonCodeDto. Nasce ativo, do tenant. Recusa code repetido no tenant; igual ao de um global cria a sobreposição, e a resposta a marca como tal | 400 · 409 · 422 |
| PUT /reason-codes/{id} | update:reason-codes | Altera name, description, appliesTo, requiresComment e sortOrder. code não é editável depois do primeiro uso, nem nunca numa sobreposição. Em id global, materializa a sobreposição do tenant com o corpo enviado e devolve o id dela — o global não muda | 400 · 404 · 409 · 422 |
| PATCH /reason-codes/{id}/deactivate | delete:reason-codes | Desativa código do tenant. Em id global, materializa a sobreposição do tenant com isActive = false — é o caminho do tenant para não usar o que o produto entregou, e o global continua ativo nos outros tenants. Recusa quando deixaria um uso obrigatório sem nenhum código ativo na lista resolvida | 404 · 409 |
| PATCH /reason-codes/{id}/activate | update:reason-codes | Reativa. Em id global, reativa a sobreposição do tenant se houver; sem ela, não há o que fazer. Idempotente | 404 |
| GET /reason-codes/usages | read:reason-codes | Os 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ó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 | O 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_UNPROCESSABLE | 422 | Sobreposiçã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_UNKNOWN | 400 | Valor fora do enum de dezoito. 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 seis 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 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.
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. 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.
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.
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.
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.
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.
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.
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:
- 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 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
- 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.
Registro
Histórico de revisões
| Versão | Data | O que mudou |
|---|---|---|
| v1.14 | 30/10/2026 | Decisõ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.13 | 21/09/2026 | Validaçã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.12 | 21/09/2026 | Validaçã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.11 | 20/09/2026 | Recuo 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.10 | 19/09/2026 | Decisã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.9 | 13/09/2026 | Correçã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.8 | 12/09/2026 | Correçã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.7 | 12/09/2026 | Validaçã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.6 | 09/09/2026 | Alinhamento 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.5 | 04–09/09/2026 | Motor 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.0 | 03/09/2026 | Primeira 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. |