Especificação · Transversal

Motivo

Todo ato negativo do sistema — rejeitar, devolver, cancelar, bloquear, revogar, substituir — hoje pede um motivo, e cinco specs publicadas resolveram isso de cinco 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. Esta spec escreve a sexta e apaga as outras cinco. Uma entidade, um enum de doze 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 último transversal do gate da Requisition 1 entidade de configuração · 1 enum de 12 usos · 0 regras de comportamento v1.3 · 08/09/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 doze ações — mesma disciplina do ownerType do anexo: ação fora da lista não existe. Isso mantém DUPLICIDADE como uma linha válida na rejeição de item e na rejeição de requisição, e faz "por que rejeitamos ou devolvemos" ser uma consulta, não a união de dez.

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 cinco specs que já pedem motivo são corrigidas na mesma leva. Fluxo de Aprovação, Anexo, Cadastro de Item, Fornecedor e Moeda e Câmbio. Nenhuma tem código escrito, então o custo é edição de documento. Corrigir três e deixar duas seria, literalmente, criar a sexta implementação divergente de motivo enquanto se escreve a spec que existe para acabar com elas.

A frase que este documento existe para cumprir está no mapa de domínio desde a primeira versão: "campo-texto livre aqui é dado que nunca vira relatório". O teste da spec inteira é simples — depois dela, "quais foram os cinco motivos mais frequentes de devolução no trimestre" tem que ser uma consulta com GROUP BY, e não um pedido para alguém ler quatrocentos comentários.

Levantamento

Onde o motivo é exigido

Doze usos, e nenhum foi inventado aqui: dez saem de atos já escritos nas specs publicadas, com o documento de origem na última coluna; dois são declarados agora para agregados que ainda não existem, pela mesma razão que o AttachmentOwnerType já lista Requisition — acrescentar valor a enum fechado depois de haver dado é migração, e agora é uma linha.

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
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 fornecedorobrigtexto livreFornecedor
SupplierUnblockDesbloqueio — o ato de volta também pede motivo, e é o que a auditoria procuraobrigtexto livreFornecedor
SupplierManualPromotionProspectSpendAuthorized na mão, sem homologação completaobrigjustificativa livreFornecedor
AttachmentRevokeRevogar anexo de documento fora de rascunhoobrigreasonCodeId? apontando para o vazioAnexo
ExchangeRateSupersedeSubstituir cotação vigente por retificaçãoobrigvarchar(200) livreMoeda
DocumentCancelCancelar requisição, pedido ou cotaçãoa definirdeclarado
SupplierAwardOverrideEscolher fornecedor que não é o de menor preço na equalizaçãoa definirdeclarado
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.

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 · 12 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 — desde 04/09/2026 isso vale para toda rejeição do Nexio, porque toda aprovação é uma instância do motor; o processo declara qual uso a rejeição grava.
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 seis atos da tabela de usos.

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 de doze itens em que oito 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 cinco 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.

Editar o rótulo é permitido; editar o código, não

name muda porque texto de tela sempre muda. code é imutável depois do primeiro uso: ele é a chave estável do relatório e do ExternalRef quando a conciliação com o ERP existir. Quem quer outro código cria outro e desativa o primeiro.

O ato guarda; o catálogo não sabe de nada

ReasonCode não tem contador de uso, não tem "última vez usado" e não tem coleção de ocorrências. Quem responde isso é a consulta sobre os próprios atos, e o GrydAudit guarda quem fez, quando e a partir de qual estado. Duas fontes para o mesmo fato é como se descobre que elas divergem.

Limite

Motivo de usuário × motivo de sistema

Nem tudo que se chama "motivo" no acervo entra aqui, e a linha já estava traçada em duas specs antes desta existir.

O quêFica 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
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.

Decisão 7

Semente do produto e catálogo do tenant

O produto semeia códigos globais — tenantId nulo — por uso. O tenant não edita o global: sobrepõe o que quer diferente com uma linha própria de mesmo code, desativa o que não serve pelo mesmo caminho, e cria os seus. Mesma mecânica do NotificationTemplate e do AttachmentType, e é a terceira repetição dela no acervo.

Como a sobreposição funciona. Para cada code, vale a linha do tenant quando existe; senão, a global. Sobrepor é POST /reason-codes com o code do global — a tela pré-preenche com ele — ou qualquer escrita sobre o id global (PUT, deactivate, activate), que materializa a linha do tenant com o corpo enviado. A partir daí as duas linhas são independentes: release que mude o global não alcança quem o sobrepôs, e a sobreposição é uma linha do tenant como outra qualquer — quem quer o texto do produto de volta edita a linha. Desativar um global é uma sobreposição com isActive = false: ele some do combo daquele tenant e continua ativo em todos os outros. Foi assim que o NotificationTemplate resolveu o mesmo problema, e é o que fecha o achado A6 da contra-análise de 03/09/2026.

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 indevidoAttachmentRevokeexige
ERRO_DE_DIGITACAOErro de digitaçãoExchangeRateSupersede
RETIFICACAO_DO_BOLETIMRetificação do boletimExchangeRateSupersede
PRAZO_DE_ENTREGAPrazo de entregaSupplierAwardOverride
DEMANDA_CANCELADADemanda canceladaDocumentCancel
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

Atos já gravados continuam com o motivo que tinham. O que muda é o combo daqui para a frente. A alternativa — recusar a edição enquanto houver uso — congelaria o catálogo no primeiro mês.

Segurança

Escopo, permissões e trilha

O catálogo é do tenant, não da empresa. Não há recorte por UserCompanyScope: um motivo de rejeição não muda porque a compra é da filial de Manaus, e criar o recorte agora significaria manter dois catálogos quase iguais em todo cliente multiempresa. O gancho fica declarado no fora de escopo, sem campo.

PermissãoSemeada emObservação
read:reason-codetodos os papéis operacionaisSem ela nenhum combo de motivo carrega — e como o motivo é obrigatório em cinco atos, sem ela metade do sistema para. É a permissão mais banal e a mais indispensável do produto
manage:reason-codeAdministradorCriar, editar, ativar e desativar códigos do tenant, sobreposições incluídas. Não alcança a linha global — nenhuma permissão alcança; escrita sobre id global materializa a sobreposição do tenant

Trilha no GrydAudit, na convenção fechada em 03/09: reason-code.created — marcada como sobreposição quando o code é de um global —, reason-code.updated — com valor anterior e novo de name e appliesTo —, reason-code.deactivated e reason-code.activated. O uso do motivo não é auditado aqui: ele é parte do ato que o produziu, e a trilha daquele ato já o registra.

Por que a trilha de edição de rótulo importa mais do que parece. É a única forma de responder "o que estava escrito neste combo em março", e essa pergunta aparece toda vez que alguém compara o relatório de dois trimestres e acha uma variação que não bate com a operação. O snapshot no ato resolve a leitura de um caso; a trilha do catálogo resolve a leitura de uma tendência.

Interface

Telas

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 "sobrepõe o produto"; desativar faz o mesmo com isActive desligado — o mesmo desenho da tela de tipos de anexo.

Ao desativar, a tela diz quantos atos já usaram aquele código e que eles não mudam. Ao tentar desativar o último de um uso obrigatório, diz qual operação ficaria impossível.

O combo, nos cinco 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-codesread:reason-codeOs 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-codesmanage:reason-codeReasonCodeDto. 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}manage:reason-codeAltera 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}/deactivatemanage:reason-codeDesativa 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}/activatemanage:reason-codeReativa. 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-codeOs doze usos com o rótulo em pt-BR, o piso do produto e quantos códigos ativos cada um tem. É o que a tela de administração usa para avisar antes de o cliente se trancar do lado de fora

Não existe DELETE. E não existe rota de "registrar motivo": o motivo é gravado pelo ato, na rota do consumidor — POST /approvals/{id}/reject, POST /attachments/{id}/revoke. Uma rota própria criaria um segundo caminho para o mesmo dado, e os dois divergiriam na primeira validação nova.

Contrato

Contrato de erros

O status HTTP é derivado do sufixo do código, nunca da mensagem: o mapeador do Core reconhece nove sufixos — _NOT_FOUND → 404 · _ALREADY_EXISTS, _ALREADY_ASSIGNED e _CONFLICT → 409 · _FORBIDDEN, _ACCESS_DENIED e _BLOCKED → 403 · _UNAUTHORIZED → 401 · _UNPROCESSABLE → 422 · sem sufixo → 400. _UNAVAILABLE → 503 seria o décimo: é extensão pedida ao Core pelo épico do GrydFiles, ainda não vigente — conferido no código da plataforma em 08/09/2026.

Có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_CONFLICT409Desativar deixaria um uso obrigatório sem nenhum código ativo. Nomeia o uso
REASON_CODE_USAGE_UNKNOWN400Valor fora do enum de doze. 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 cinco 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 dez usos com piso obrigatório continuam obrigatórios em todo cliente. A configuração do tenant só acrescenta exigência onde o produto não exige — nunca remove.

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. Desde 04/09/2026 toda aprovação humana é uma instância do motor, e cada processo declara o uso da sua rejeição (ItemReject em ITEM, SupplierReject em SUPPLIER_REGISTRATION e SUPPLIER, ApprovalReject nos demais) — logo o comentário passou a ser obrigatório também na recusa de item e de fornecedor, que antes exigiam só o código. requiresComment do código continua valendo por cima.

O porquê

O que isto responde

Um catálogo de motivos não é um cadastro de apoio — é a dimensão que faltava para o sistema dizer alguma coisa sobre o processo do cliente. Estas perguntas passam de "alguém lê os comentários" para GROUP BY:

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 três a seis por uso, e a description existe para desempatar os parecidos. Crescer é fácil; podar depois é conversa política.

Limites

Fora de escopo

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.