Ponto de partida
Oito decisões
Fechadas antes de o documento existir. As três primeiras decidem o tamanho da spec; a quarta é a que impede uma regra de segurança já escrita em outra spec de virar um if espalhado por vinte disparos.
Usuário × slug × canal, e a linha só existe quando difere do padrão. NotificationPreference grava exceção, não estado. Ausência de linha significa "o que o catálogo declarou", então slug novo entra ligado ou desligado conforme o próprio catálogo — sem migração, sem linha órfã, e sem o caso que sempre morde: o usuário que configurou as preferências em março e deixa de receber, em silêncio, o aviso publicado em agosto.
isMandatory com canal e motivo. O slug não some da tela e o canal que carrega o controle não desliga; canais adicionais ficam a critério do usuário. O motivo vem do catálogo e aparece ao lado do controle desabilitado, porque controle que a pessoa não entende vira ruído — e ruído acaba desligado por outro caminho. Obrigatória não significa todos os canais: push às duas da manhã para um controle que o e-mail já cumpre é como se ensina alguém a ignorar.
Resolvedor por tipo de audiência. O disparo manda audience: {type, params}; quem vira lista de pessoas é um IAudienceResolver registrado por tipo, e ele mora no módulo dono do conceito — o rollup de donos do nó é do centro de custo, não do módulo de notificação. A alternativa era cada consumidor entregar a lista pronta, e o preço seria o mesmo rollup reimplementado em três módulos, divergindo no primeiro bug. Foi a decisão mais cara das oito, e é a que evita que "notificação" vire o lugar onde regra de negócio de outros módulos se acumula.
Notificação é ponteiro, não relatório. O corpo carrega identificação, contexto não sensível e link — nunca valor sob permissão. A RN-O22 do Orçamento já dizia que sem read:budget-amounts os valores não vão "nem na API, nem no CSV, nem na notificação"; aqui isso deixa de ser regra do orçamento e vira regra do produto. O catálogo declara as variáveis de cada slug, e nenhuma delas pode ser sensível: a checagem acontece na semeadura, não em tempo de execução.
nexio.<entidade>.<verbo-no-passado>, e o AuditLog fecha junto. O slug é literalmente a ação da trilha com o prefixo do produto: a ação attachment.revoked tem o aviso nexio.attachment.revoked. Fecha na mesma conversa a convenção de nomes de ação que a spec de Anexo propôs e não fechou. O prefixo não é enfeite: NotificationTemplate é tabela global do framework, e vai conviver com os slugs do próximo produto Gryd.
Onde a ação mora, já que o AuditLog não tem coluna para ela. A convenção vale só para evento de negócio — o que se registra por IAuditLogService.RecordAsync, como "baixou o anexo" ou "aprovou o passo". Esses gravam a ação em AdditionalData, sob a chave fixa action, e a consulta por ação é filtro sobre esse metadado; índice sobre ele é decisão de infraestrutura, para quando o banco estiver escolhido. A entrada automática do interceptor de diff não tem ação: ela tem EntityType, EntityId e ChangeType, e é outra coisa — quem quer "o que mudou na linha" lê o diff, quem quer "o que a pessoa fez" lê a ação. Coluna própria Action no AuditLog é o lar definitivo e é spec de plataforma, no molde do GrydFiles; até lá nada no Nexio depende dela, porque a chave do metadado é contrato do produto.
v1 imediata com throttle; agrupamento declarado e não construído. O catálogo já nasce com digestEligible e throttleWindow. A v1 implementa só o throttle — a mesma tupla usuário + slug + entidade não repete dentro da janela —, que resolve barato o ruído de lembrete de SLA. O agrupador entra depois sem migração, porque os campos que ele precisa já existem e já estão preenchidos.
Usuário, depois tenant, depois o template global. Cascata de três, o mesmo desenho da resolução de taxa em Moeda e Câmbio e da herança de política em Orçamento. O campo Locale já existe no NotificationTemplate; o que faltava era dizer quem escolhe.
O documento espera, e ninguém submete no lugar da pessoa. Fecha o que a spec de Anexo deixou declarado em aberto, e fecha pelo lado curto: submeter documento com anexo em varredura é recusado, pelo 409 que o Anexo já devolve; a tela acompanha a liberação e reenvia; veredito sujo faz a pessoa trocar o anexo ou pedir varredura nova. Não existe fila que submeta sozinha. Submeter dispara aprovação e movimenta orçamento, e um job que fizesse isso precisaria executar um comando com a permissão e o escopo de quem pediu — contexto que a plataforma não oferece. Execução delegada é requisito de v2, escrito na demanda de plataforma.
O que não foi decidido aqui, porque já estava decidido. A fila do aprovador não é notificação: é ApprovalAssignment, especificada no Fluxo de Aprovação. A notificação de tarefa nova é um empurrão para uma fila que existe sem ela — e é por isso que ela pode ser desligada, agrupada em resumo e perdida sem que nada se perca. Confundir as duas é como se constrói um sistema em que a decisão mora na caixa de entrada.
Camadas
A fronteira com o GrydNotifications
O módulo da plataforma foi lido no código em 02/09/2026 e entrega mais do que o documento de arquitetura descreve. Nada do que está na coluna da esquerda é remodelado, reembrulhado ou reimplementado — esta spec consome.
| Assunto | Quem resolve | Detalhe |
|---|---|---|
| Entrega, retry e canal | GrydNotifications | Notification, NotificationRecipient, NotificationDeliveryAttempt, UserNotification, DeviceToken; in-app por IInAppNotificationSender, push por Firebase, e-mail por SendGrid, cada provedor num projeto de infraestrutura próprio |
| Template e renderização | GrydNotifications | NotificationTemplate com Slug, SubjectTemplate, HtmlBodyTemplate, TextBodyTemplate, Locale, Version, RequiredVariables (uma string anulável, não um vetor — a lista é desmembrada na conferência) e TenantId? — nulo é template do produto, preenchido é override do tenant. Mesmo padrão que o AttachmentType já pratica |
| Fila e agendamento | GrydNotifications · GrydJobs | INotificationQueue, INotificationScheduler e a política de retry. O digest da v2 se apoia neste agendador, e é por isso que ele não precisa de infraestrutura nova |
| Quais avisos existem | Nexio | Catálogo de slugs. A plataforma sabe entregar um template; ela não tem como saber que "orçamento estourou dentro da tolerância" é um aviso deste produto, que ele vai para os donos do nó por rollup e que ele nunca carrega valor |
| Quem recebe | Nexio | Resolvedores de audiência. Rollup de centro de custo, aprovadores pendentes e contato financeiro do fornecedor são conceitos do domínio de compras |
| Quem quer receber | Nexio | NotificationPreference. O GrydAuth tem UserPreference, e ela guarda preferência de conta — idioma, entre outras coisas; o que não existe no framework é preferência por aviso e por canal, que é uma matriz usuário × slug × canal e depende de um catálogo que só o produto tem. É essa a lacuna, e é a razão de esta spec ter uma tabela em vez de nenhuma |
| Alerta de operador | Plataforma | Fila de scan alta e base de assinaturas velha, do GrydFiles, avisam o operador da plataforma — não um usuário do tenant. Slug de operador não é nexio.*, não aparece na tela de preferências e não passa por NotificationPreference. Ver § Escopo |
A dívida que sai junto. NotificationAttachment guarda byte[] Content numa coluna do banco. É exatamente o que o GrydFiles corrige, e é a mesma troca que Item e Supplier fizeram em 02/09: o campo vira storedFileId, com perfil próprio gryd.notification-attachment declarado no appsettings — retenção curta, porque anexo de e-mail enviado não é prova de nada depois que o e-mail chegou. Ninguém no Nexio depende dessa coluna hoje, então a troca é barata agora e cara depois de o primeiro cliente ter dois anos de e-mails.
Modelo
Mapa de entidades
UserPreference, não preferência por aviso e por canal. Uma linha por exceção: usuário, slug, canal, ligado ou desligado. Ausência de linha é o padrão do catálogo, e é isso que faz slug novo funcionar sem migração.
NotificationDefinition por slug: audiência, canais, padrão, obrigatoriedade, variáveis, throttle. É do produto e muda com release, não com o tenant. Mesmo precedente do AttachmentOwnerRegistry.
IAudienceResolver por tipo de audiência, implementado pelo módulo dono do conceito. Tipo sem resolvedor derruba a inicialização, pela mesma razão que ownerType sem descritor derruba.
Slug, Locale, Version, RequiredVariables e TenantId?. O Nexio semeia um template por slug e por locale suportado, e nunca cria a tabela.
Convenção de nomes: o documento é escrito em português e todo identificador é em inglês — entidade, atributo, valor de enum, slug, rota, permissão e código de erro.
Produto
O catálogo de slugs
Sessenta e quatro avisos, e nenhum deles foi inventado aqui: cada linha é um disparo já escrito em uma das specs publicadas, com o documento de origem na última coluna, e as que entraram nas rodadas de validação têm pedido registrado da trilha que precisava delas — as demais têm a origem declarada, que é o documento onde o disparo já estava escrito. O catálogo é a varredura, e a varredura é o que impede o vício clássico — o time inventar avisos que ninguém pediu e não escrever os que a regra de negócio exige.
Ele mais que dobrou, e o que entrou diz o que o produto virou. Eram vinte e sete; as rodadas de validação da base acrescentaram quarenta e tiraram três — saíram os dois da fila de submissão e o aviso de liberação de varredura, que só existiam por causa dela. Quase tudo o que entrou é controle: política publicada, configuração crítica efetivada, taxa sobrescrita, passo parado, empresa sem administrador, canal de aviso trocado, eliminação a pedido do titular. É a mesma história contada por outro ângulo — cada segunda pessoa que o desenho passou a exigir precisa de uma terceira que fique sabendo.
Legenda de canais: E e-mail · I in-app · P push — os três que a plataforma entrega, e os três que existem na v1. Maiúscula é ligada por padrão, minúscula é disponível e desligada. obrig marca o que a preferência não desliga.
| Slug | Dispara quando | Audiência | Canais | Ritmo | Origem |
|---|---|---|---|---|---|
| nexio.approval-assignment.assigned | Uma tarefa entra na fila de um aprovador — de qualquer processo: cadastro e homologação de fornecedor, alteração bancária e curadoria de item também são instâncias do motor, e os avisos de tarefa, lembrete e decisão os cobrem | User | E I p | digest ✓ | Fluxo |
| nexio.approval-assignment.reminded | SLA a vencer ou vencido, com EscalationBehavior = Notify. Vai ao aprovador e a quem submeteu | User | E I p | digest ✓ · 24 h | Fluxo |
| nexio.approval-request.escalated | Escalate. A tarefa original continua viva, e o aviso diz isso. Não há NotifyManager no Fluxo — ele dependia da hierarquia de pessoas, que é fase 2 | User | E I P | 24 h | Fluxo |
| nexio.approval-request.returned | Devolução para ajuste. Gera evento e avisa quem submeteu | User | E I P | imediato | Fluxo |
| nexio.approval-request.decided | Instância terminou em Approved ou Rejected. Variável outcome, um template só | User | E I P | imediato | Fluxo |
| nexio.approval-step.authority-exceeded | Um passo ativou nos Owners da raiz sem ninguém na cadeia com alçada para o valor — a aprovação vai acontecer no topo (decisão 7 do Fluxo) e a controladoria fica sabendo com a tarefa ainda aberta, a tempo de corrigir o vínculo ou reatribuir. Sem valor no corpo (RN-NOT-09): quantia e limite ficam do outro lado do link. Era o slug que faltava ao gate 9 da contra-análise | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.approval-step.resolved-out-of-scope | Um nível resolveu para quem não alcança a empresa do documento e o passo caiu no aprovador de reserva. É o mesmo tipo de fato do authority-exceeded — uma cadeia que não saiu como o desenho previa —, e o caminho normal é não acontecer: a RN-CC-30 recusa o vínculo fora de escopo na criação, e a RN-AP-40 recusa o alvo fora de escopo na publicação da política. Sobra o escopo revogado depois, com o vínculo dormente, e a controladoria descobre por aviso em vez de pela pergunta "por que este pedido foi parar no reserva?" | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.approval-step.resolved-empty-on-activation | Um passo ativou e resolveu zero pessoas por passagem do tempo — vigência de membro de grupo vencida, papel sem detentor, Owner que deixou de ser vigente —, o passo caiu no aprovador de reserva e ficou marcado. É o irmão do resolved-out-of-scope: lá o vínculo existe e não alcança a empresa; aqui o vínculo deixou de existir sozinho, sem ninguém decidir nada. O destinatário é o mesmo, e pelo mesmo motivo — o dono do nó é justamente quem se beneficiaria do silêncio | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.approval-request.reopened | Uma instância saiu de Approved por revaluate e abriu rodada nova. Vai aos aprovadores da rodada anterior: eles perderam uma assinatura que pensavam ter dado, e o documento que assinaram não é mais o documento. O requisitante não recebe — foi ele quem editou. O SLA não é o da rodada velha, e o texto diz isso | User | E I p | imediato | Fluxo |
| nexio.approval-assignment.info-requested | Um aprovador perguntou algo ao requisitante (POST /approvals/{id}/assignments/{aid}/request-info) e o SLA do passo pausou. Vai ao requisitante: sem ele a pergunta existe e ninguém sabe — o documento fica parado esperando uma resposta que não foi pedida a ninguém, e o tempo conta contra o requisitante. É o par do info-answered | User | E I P | imediato | Fluxo |
| nexio.approval-assignment.info-answered | O requisitante respondeu à pergunta de um aprovador (POST /approvals/{id}/answer-info). Vai a quem perguntou: sem ele o relógio do passo volta a correr em silêncio, e o SLA corre contra um aprovador que não sabe que a resposta chegou | User | E I p | imediato | Fluxo |
| nexio.approval-policy.published | Uma versão de política de aprovação foi publicada. Vai à controladoria com o diff — o que mudou de nível, alvo, limiar e comportamento. É o par do autor ≠ publicador: a segunda pessoa assina, e a terceira fica sabendo. Vale para os dez processos, não só para os de controle — política de alçada de dinheiro também é controle obrig | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.approval-settings-change.applied | Configuração crítica do motor efetivada: materialidade, reserva e grupo de reserva, authorityExceededBehavior, dedupe, delegable, e os membros de todo grupo ou papel usado por nível de controle em política ativa — inclusive por cadeia congelada de instância em aprovação. Vai à controladoria com o diff, que nomeia quem foi incluído ou prorrogado e quem efetivou, com o CPF verificado mascarado. Muda como a política muda, e por isso avisa como ela obrig | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.control-role-member.granted | Alguém passou a deter um papel usado por nível de controle em política ativa — a controladoria e qualquer outro. Depende do evento de concessão de papel, que é do GrydAuth e está na demanda de plataforma: enquanto ele não existir, a mudança de papel é invisível para o produto, e é essa ausência que o aviso torna explícita em vez de silenciosa | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.control-role-member.revoked | O contrário do anterior, e o mais importante dos dois: quem saiu de um papel de controle. Mesma dependência de plataforma | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.approval-delegation.created-by-other | Uma delegação foi criada por um terceiro — não pelo delegante. Vai ao delegante, e é o aviso que impede a delegação silenciosa: quem recebeu a alçada de outra pessoa sem saber não pode reclamar do que for aprovado em seu nome obrig | User | E I P | imediato | Fluxo |
| nexio.approval-delegation.expiring | Delegação a vencer. Vai ao delegante, que é quem decide renovar ou deixar cair | User | E i | digest ✓ · 24 h | Fluxo |
| nexio.approval-group-member.expiring | Vigência de membro de grupo de aprovação a vencer. Vai ao administrador do tenant, porque grupo vazio na ativação é o caminho do resolved-empty-on-activation — e este aviso existe para o outro chegar antes de precisar | TenantAdmin | E i | digest ✓ · 24 h | Fluxo |
| nexio.approval-step.controller-required | Um passo caiu no complementar da controladoria por authorityExceededBehavior = RequireControllerApproval: ninguém na cadeia tinha alçada para o valor e o desenho manda uma segunda leitura em vez de aprovar no topo | TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.approval-step.resolved-by-fallback | Um passo foi resolvido pelo aprovador de reserva (origin = ByFallback), qualquer que tenha sido o caminho até ele. É o aviso guarda-chuva dos dois anteriores: eles dizem por que caiu, este diz que caiu, e é o que a controladoria filtra para ver o padrão | TenantRole(Controladoria) | E I p | digest ✓ · 12 h | Fluxo |
| nexio.approval-step.stalled | Um passo parou: resolveu zero pessoas e não há reserva elegível, ou a instância ficou sem ninguém que possa decidir — o caso declarado da troca bancária em tenant com menos de duas pessoas elegíveis, e o da correção de identidade que ligou todo mundo ao fornecedor. A regra é não afrouxar: a instância fica parada com alerta, nunca cai no reserva por conveniência obrig | TenantAdmin · TenantRole(Controladoria) | E I P | imediato | Fluxo |
| nexio.approval-request.decision-refused | A decisão que concluiria a instância foi recusada pelo contrato de desfecho — Refused(code): verba estourada na decisão final, remanejamento cuja origem não cobre mais o valor, callback sem confirmação, anexo comprobatório ausente, pessoa ligada a uma correção de identidade. A decisão não foi gravada e a tarefa continua ativa. Também sai quando o desfecho recusa o ajuste de um documento aprovado: a alteração não foi gravada e o documento continua na versão aprovada (Fluxo de Aprovação, RN-AP-71). Vai a quem submeteu e ao dono do impedimento, resolvido pelo módulo que recusou — para verba, os donos do nó com rollup. Carrega o código da recusa, nunca o valor: quanto falta está do outro lado do link obrig | User · CostCenterOwners | E I P | imediato | Fluxo · Orçamento |
| nexio.approval-request.cancelled | A instância foi cancelada antes de decidir — pelo requisitante, pela contestação do fornecedor, ou pelo módulo dono. Vai a quem submeteu, e é o nome único do cancelamento: o decided cobre Approved e Rejected, e cancelamento não é decisão | User | E I P | imediato | Fluxo |
| nexio.approval-request.exchange-rate-overridden | Documento submetido com taxa informada ou tipo de taxa trocado por exchangeRateOverride: a flag nasce de o override existir, com ou sem taxa e mesmo com o tipo igual ao configurado. Sai sempre que a flag nascer — na submissão e na reavaliação, porque a taxa pode trocar depois e antes disso a controladoria recebia a tarefa sem o aviso. Quem sobrescreveu e o autor da taxa usada ficam fora da decisão. A taxa decide a alçada, e quem pede não baixa a própria alçada sem segundo olho obrig | TenantRole(Controladoria) | E I P | imediato | Fluxo · Moeda |
| nexio.budget.warning-signaled | Avaliação devolveu Warning — estoura dentro da tolerância | CostCenterOwners | E i | digest ✓ · 12 h | Orçamento |
| nexio.budget.approval-requirement-unmet | Um nó em RequireApproval estourou e a cadeia resolvida não tem nenhum passo vindo de política com critério de orçamento — a exigência foi declarada e não foi cumprida, quase sempre porque a política complementar foi aposentada ou expirou. Vai à controladoria com o documento ainda em aprovação, a tempo de reatribuir ou republicar a política. Não se confunde com o overrun-recorded: aquele diz "estourou" e vai aos donos do nó; este diz "o controle não rodou" e vai a quem responde por ele (RN-AP-42) | TenantRole(Controladoria) | E I P | imediato | Orçamento |
| nexio.budget.overrun-recorded | Política Warn deixou passar e marcou o documento como fora do orçamento. Cobre também a correção de taxa, que é movimento de sistema isento de Block: ela passa marcada, e os donos do nó da linha estourada recebem o mesmo aviso, com o mesmo conteúdo. Cobre ainda o Actual gravado sob Block ou sem linha que o cubra (Uncovered), que nunca é recusado, e o positivo isento da virada — o da rotina e o da virada sob demanda disparada por liquidação. O fato é o mesmo, gravou fora do orçamento, e a audiência também (Orçamento, RN-O33 e RN-O13) | CostCenterOwners | E I p | imediato | Orçamento |
| nexio.budget.year-end-pending | A virada de exercício não fechou: o contrato de desfecho devolveu Failed em documentos abertos e o exercício não é arquivado enquanto houver pendência. Vai ao administrador do tenant com a lista dos documentos. Sem ele, um exercício fica meio virado e ninguém sabe — que é o modo de falha mais caro do razão obrig | TenantAdmin | E I P | imediato | Orçamento |
| nexio.budget-residue.aged | A rotina semanal do Orçamento achou resíduo aberto — Commitment ou Obligation sem movimento — mais velho que residueAlertAfterDays no nó. Um aviso por dono e por nó, com o número de cada documento, a idade em dias e o link para a consulta de resíduos. Sem valor (RN-NOT-09): o saldo preso fica do outro lado do link. Sem Watcher, porque o aviso pede ação de quem responde pelo ramo. É o que faz o resíduo sair por ação humana antes de comer o disponível do ano | CostCenterOwners | E I p | 7 d | Orçamento |
| nexio.currency-settings-change.applied | Configuração de câmbio crítica efetivada: tipo de taxa por processo, tipo padrão e a janela de idade máxima da cotação. Vai à controladoria com o diff — campo, processo, antes, depois, quem propôs e quem efetivou. Alargar a janela para trás é escolher taxa mais velha sem tocar em tipo nenhum, e a diferença cai direto na alçada obrig | TenantRole(Controladoria) | E I P | imediato | Moeda |
| nexio.exchange-rate.capture-failed | consecutiveFailures cruzou notifyOnFailureAfter — padrão 1, porque câmbio quebrado em silêncio é o modo de falha que aquela spec existe para evitar | User → TenantRole(Controladoria) | E I P | 24 h por par | Moeda |
| nexio.item-compliance.expiring | Job diário, 30, 15 e 5 dias antes. Quando blocksPurchaseWhenExpired, o texto diz que vai bloquear | TenantRole(Catalogo) | E i | digest ✓ · 24 h | Catálogo |
| nexio.item.rejected | Item recusado na curadoria — a instância ITEM terminou em Rejected. Carrega o rótulo do motivo codificado — reasonLabel, que vem de lista fechada — e avisa o criador. O reasonComment não vai junto: é texto livre de uma pessoa, e fica do outro lado do link (RN-NOT-25). Convive com approval-request.decided: um avisa a decisão, o outro diz o que ela foi | User | E I p | imediato | Catálogo |
| nexio.item-import.completed | Importação assíncrona terminou. Link para o relatório — o exemplo mais limpo de ponteiro em vez de conteúdo | User | E I p | imediato | Catálogo |
| nexio.company.administrator-missing | Rotina diária: uma empresa — ativa ou inativa — ficou sem nenhuma linha de escopo vigente e sem fim, de pessoa ativa que tenha a permissão de administrar escopo. A pessoa saiu da organização, foi bloqueada, ou perdeu a permissão. Vai a quem tem linha vigente e a permissão naquela empresa, com destaque quando todas essas linhas terminam em até sete dias; sem ninguém assim, ao administrador do tenant, que é exatamente o caso em que quem saiu era o único administrador dela. Enquanto houver linha dependente pendente de decisão, a lista delas se repete neste aviso obrig | CompanyScopeAdmins · TenantAdmin | E I | digest ✓ · 24 h | Empresa |
| nexio.user-company-scope.revoked | A linha de escopo de alguém foi encerrada ou encurtada por corte em cadeia: quem concedeu teve a própria vigência reduzida, e a concessão não passa da vigência de quem concede — no tempo, não só no ato. Vai a quem perdeu ou teve a linha encurtada, com cópia a quem a concedeu, se ainda estiver na organização. Carrega a empresa, o novo fim e a linha de origem do corte. Sem ele, a pessoa descobre pelo 403 no meio de um pedido | User | E I p | imediato | Empresa |
| nexio.user-company-scope.holder-removed | O sistema encerrou as linhas de quem saiu da organização — pelo evento da plataforma ou pela rotina diária. Vai a quem administra escopo na empresa, com a pessoa removida, as empresas, as linhas que caíram em cadeia e, em destaque, as que ficaram esperando decisão. Sem ninguém com linha e permissão na empresa, ao administrador do tenant: é justamente quando quem saiu era o único administrador que as linhas pendentes ficariam órfãs, sem ninguém para decidir obrig | CompanyScopeAdmins · TenantAdmin | E I P | imediato | Empresa |
| nexio.attachment.revoked | Anexo revogado em documento já submetido e ainda em aprovação. O approvalRequestId que a audiência exige vem do descritor do dono (OpenApprovalOf(ownerId)), resolvido no momento do disparo — e este slug tem audienceMustResolve: descritor que não devolve instância aberta é falha, não silêncio — aviso obrigatório de audiência calculada não entrega a zero pessoas | PendingApprovers | E I P | imediato | Anexo |
| nexio.attachment.infected | Veredito do antivírus deu Infected. Avisa quem anexou — e, quando uploadedByUserId é nulo, o ResponsibleOf(ownerId) do descritor, que nunca devolve vazio. O nulo não é exceção teórica: é o caminho do portal, o único em que um terceiro sobe arquivo, e sem o fallback a proposta infectada de um fornecedor não avisava ninguém. É fallback de destinatário, não audiência nova obrig | User | E I P | imediato | Anexo |
| nexio.attachment.scan-failed | O arquivo ficou em Failed depois das tentativas automáticas — o antivírus não conseguiu dar veredito. Vai a quem anexou, ou ao responsável pelo documento quando o arquivo veio pelo portal. Uma vez por episódio, com as duas ações que resolvem: verificar de novo, ou remover | User | E I P | imediato | Anexo |
| nexio.attachment.rejected | O arquivo foi reprovado — tipo, tamanho ou assinatura fora do que o descritor aceita —, com o código da recusa. Mesma audiência do infectado, e pela mesma razão: quem anexou é quem consegue trocar o arquivo | User | E I P | imediato | Anexo |
| nexio.attachment.infected-after-release | Arquivo já liberado que a varredura nova achou infectado. A audiência segue o estado do documento dono: em rascunho, quem anexou; em andamento, isso mais os aprovadores pendentes; fechado, obrigatoriamente o responsável e quem anexou, com recurso a quem administra guarda legal se nenhum dos dois estiver ativo. É o caso em que o arquivo já foi visto, já foi baixado e já serviu de prova obrig | User · PendingApprovers | E I P | imediato | Anexo |
| nexio.attachment-erasure-request.registered | Registrou-se um pedido de eliminação a pedido do titular que pede segunda pessoa — prova de controle, dono ainda dentro da guarda, ou anexo com retenção Permanent — e nasceu livre. O que nasce travado sai em attachment-erasure-request.blocked: o registro publica uma das duas ações, nunca as duas. O que não pede segunda pessoa executa no próprio registro e não tem este aviso. Vai a quem pode executar: permissão de eliminar, escopo, e leitura em todos os donos dos anexos daquele arquivo — menos quem registrou. Sem ninguém elegível, ao administrador do tenant: o pedido fica parado, nunca volta à mesma pessoa. Carrega o número legível do dono, a referência do pedido, quem registrou e quando — nunca o nome do arquivo nem o comentário obrig | PermissionHolders(erase:attachments) · TenantAdmin | E I P | imediato | Anexo |
| nexio.attachment-erasure-request.blocked | O pedido do titular foi recebido e está travado. O blockReason diz por quê, e tem três valores: aprovação em curso no dono, decisão de controle pendente, ou ScopeIncomplete — quem registrou não alcança todos os donos dos anexos daquele arquivo. Vai ao titular, pelo endereço externo do próprio pedido — e, sem ele, a quem registrou, que responde pelo canal por onde o pedido chegou. Diz que o pedido foi recebido, e o prazo. Quem pode executar não é avisado: ninguém executa enquanto o bloqueio não cai, e o pedido espera na fila do Anexo, com o prazo vencido escalando ao administrador. Nunca nome de arquivo, número de documento, tipo de decisão pendente, motivo do bloqueio ou comentário: quem lê está fora do tenant. Com ScopeIncomplete, e só nele, o aviso vai também ao administrador do tenant, com o alcance que falta — as empresas fora do escopo e os tipos de dono cuja leitura foi negada —, sem dono, número legível nem nome de arquivo: é o único dos três bloqueios que ninguém de dentro do pedido derruba, porque quem concede escopo é ele. Um slug, e não dois: ao titular a frase é a mesma nos três casos, e a audiência por condição é a mesma forma do attachment-erasure-request.purge-blocked obrig | User · TenantAdmin | E I p | imediato | Anexo |
| nexio.attachment-erasure-request.purge-blocked | A eliminação foi pedida e não concluída: o ato está gravado, o nome já foi neutralizado, e o expurgo foi recusado — guarda de outro detentor, ou referência de outro escopo criada nesse meio-tempo. O evento é attachment-erasure-request.purge-blocked, nome próprio: o bloqueio do registro é outro momento e sai em attachment-erasure-request.blocked, e os dois nunca compartilham nome. Vale também para o pedido que ficou Executed com o expurgo não gravado e o prazo vencido — falha transitória que ninguém reparou; aí não houve recusa, e o aviso sai sem código. Vai a quem registrou e ao titular; e, só quando o prazo já venceu e o pedido continua travado, ao administrador do tenant. O código da recusa vai para os dois de dentro e nunca para o titular — a ele se diz que o pedido está em curso e em que prazo terá resposta obrig | User · TenantAdmin | E I P | imediato | Anexo |
| nexio.attachment.erased | Os bytes saíram — e é isso, e só isso, que o slug significa. O gatilho é o expurgo gravado em todas as cópias do escopo, não o ato de revogar: publicar a eliminação no ato e o expurgo ser recusado depois era como um dado pessoal ficava para sempre num arquivo que todo mundo achava apagado. Mesma audiência do aviso de expurgo travado obrig | User · TenantAdmin | E I p | imediato | Anexo |
| nexio.attachment-erasure-request.cancelled | O evento é attachment-erasure-request.cancelled, nome próprio, como no Anexo. Todo cancelamento de pedido de eliminação, e não só a repetição: cancelar devolve o arquivo à propagação e o anexo ao acervo, e o pedido do titular deixa de existir — isso não é rotina. Vai ao titular, pelo endereço externo do pedido, e, sem e-mail, a quem registrou, que responde pelo canal por onde o pedido chegou; leva a referência, quando foi pedido, quando foi cancelado e quem cancelou, e nunca nome de arquivo, número de documento ou comentário. Do segundo cancelamento em diante sobre o mesmo arquivo, vai também ao administrador do tenant: a repetição é o padrão de quem cancela para não executar obrig | User · TenantAdmin | E I p | imediato | Anexo |
| nexio.supplier-bank-account.change-requested | Proposta de alteração bancária, avisando no canal antigo — o contato Finance vigente antes da proposta —, só por e-mail. O corpo leva o link de contestação de uso único e a instrução de ligar para o comprador por um número que o fornecedor já conhece; nunca mostra a conta proposta. A redundância contra a caixa comprometida não está aqui: está no callback de saída para telefone confiável, obrigatório sempre, que é evidência da tarefa e não aviso obrig | SupplierFinanceContact | E | imediato | Fornecedor |
| nexio.supplier-bank-account.change-approved | Proposta aprovada — a instância BANK_ACCOUNT_CHANGE terminou em Approved e o Fornecedor aplicou. Vai também ao canal anterior do fornecedor — o mesmo contato que recebeu o change-requested, pelo mesmo resolvedor e pelo mesmo parâmetro de canal, por e-mail —, com o usableFrom e a data e a hora em que a carência de 24 a 72 h termina, e com link de contestação novo, que invalida o anterior. É de lá que sai o "não fui eu" dentro da carência, e é por isso que este aviso não se desliga. Nunca mostra a conta. A controladoria recebe em cópia, nunca no lugar do fornecedor (RN-NOT-29). Convive com approval-request.decided, que avisa só a decisão obrig | SupplierFinanceContact · TenantRole(Controladoria) | E I p | imediato | Fornecedor |
| nexio.supplier-document.expiring | Job diário, 30, 15 e 5 dias antes de expiresOn de certidão ou documento do fornecedor. Existe porque o compliance-changed dispara depois do vencimento — e vencimento é data conhecida de antemão: quando o aviso chega, a política de documento vencido já travou o pedido, e o comprador descobre pela recusa. Antecipação é o ponto inteiro | TenantRole(Compras) | E i | digest ✓ · 24 h | Fornecedor |
| nexio.supplier.compliance-changed | Abriu-se um alerta de conformidade do fornecedor: sanção nova, situação cadastral alterada, divergência com o ERP ou verificação vencida. Alerta, nunca bloqueio automático. Certidão vencida saiu daqui — é data conhecida de antemão e tem aviso próprio, que chega antes | TenantRole(Compras) | E I p | digest ✓ · 24 h | Fornecedor |
| nexio.supplier-bank-change-proposal.contested | Alguém disse "não fui eu" — pelo link de uso único, por dentro do Nexio, ou pelo callback que voltou Denied. Carrega o changeKind, porque o efeito difere: em troca de conta a proposta morre e trocar exige proposta nova; em troca de contato já aplicada o valor volta ao anterior, o novo fica não confiável e as propostas de conta do fornecedor ficam travadas; e quando a proposta trazia canal contestado ou o anexo do pedido de troca, a contestação suspende e manda o caso ao complementar da controladoria, que decide com a evidência — não reverte sozinha obrig | PermissionHolders(approve:supplier-banks) · TenantAdmin | E I P | imediato | Fornecedor |
| nexio.supplier-bank-change-proposal.notice-bounced | O aviso ao canal anterior voltou e não há próximo e-mail na cadeia: a instância foi cancelada. Vale para conta e para contato da cadeia de reserva. É o aviso que impede a carência de correr sobre um e-mail que nunca chegou — e-mail devolvido não inicia a janela obrig | PermissionHolders(approve:supplier-banks) · TenantAdmin | E I P | imediato | Fornecedor |
| nexio.supplier-bank-account.erp-divergence-detected | A conferência periódica achou, no ERP, conta diferente da que o Nexio guarda — o Nexio é a fonte da verdade, e o golpe não pode migrar para o outro lado. Repete a cada conferência enquanto durar, e também depois de três dias seguidos de resposta indisponível: silêncio de integração não é ausência de divergência obrig | PermissionHolders(approve:supplier-banks) · TenantAdmin | E I P | imediato | Fornecedor |
| nexio.supplier-bank-accounts.initial-loaded | A carga inicial de contas bancárias do onboarding foi confirmada — janela única por tenant, dupla custódia, evento próprio, ato de controle. É a única porta que cria conta sem proposta, e por isso a única que precisa ser vista por quem não a abriu obrig | PermissionHolders(approve:supplier-banks) · TenantAdmin | E I P | imediato | Fornecedor |
| nexio.supplier-protected-fields.changed | Configuração de campo protegido do fornecedor fora do núcleo bancário foi alterada. O núcleo — ISPB, agência, conta, PIX, IBAN, titular, e-mail e telefone do contato Finance — não é configurável e não chega aqui; o que é configurável avisa obrig | PermissionHolders(approve:supplier-banks) | E I p | imediato | Fornecedor |
| nexio.supplier-bank-permission.granted | Alguém passou a deter approve, propose ou read-unmasked:supplier-banks — por concessão direta, ou porque um papel já atribuído passou a conter a permissão. Vai a quem já aprova contas, ao administrador, e ao contato contratual do tenant — quem assinou o contrato, que é destinatário externo e o único que percebe o administrador comprometido se concedendo poder de pagamento. Quem recebe essas permissões precisa de identidade verificada com CPF único obrig | PermissionHolders(approve:supplier-banks) · TenantAdmin · TenantContractContact | E I P | imediato | Fornecedor |
| nexio.supplier-contact.change-requested | Pediu-se alteração protegida de contato do aviso — NoticeContact ou o contato designado da cadeia de reserva. Vai ao endereço anterior, só por e-mail, com link de contestação; nunca mostra o valor novo. O texto diz que a troca só é aplicada depois de uma ligação de confirmação para número que o fornecedor já tinha. Na variante sem telefone confiável ele diz mais: a troca vai à aprovação com a controladoria, exige o pedido de troca anexado — que o sistema não confere, apenas exige e guarda com quem o anexou, quem decidiu, data e hora — e só vale sete dias depois de aplicada obrig | SupplierFinanceContact | E | imediato | Fornecedor |
| nexio.supplier-contact.change-applied | A alteração de contato do aviso foi aplicada. Vai ao endereço anterior, só por e-mail, com usableFrom e um token novo de contestação que invalida o anterior — válido até trinta dias depois de usableFrom. Continua sem mostrar o valor novo: o aviso serve para o fornecedor legítimo dizer "não fui eu", não para confirmar ao fraudador que deu certo obrig | SupplierFinanceContact | E | imediato | Fornecedor |
| nexio.supplier.identity-corrected | Desfecho de uma correção de identidade de fornecedor aprovado — cpf, ownerCpf ou tipo de entidade sem trocar a existência do CNPJ. Três variantes: aplicada, rejeitada e cancelada, as duas últimas dizendo que a identidade não mudou. Todas nomeiam quem pediu e quem decidiu, e dizem que essas pessoas ficam ligadas àquele fornecedor enquanto ele existir: não propõem, não confirmam e não decidem conta bancária nem canal de aviso dele. Sem o aviso nas duas últimas variantes, uma ligação que nasceu de uma correção que não aconteceu ficaria invisível. Carrega o campo corrigido, nunca o CPF em claro obrig | PermissionHolders(approve:supplier-banks) · TenantAdmin | E I P | imediato | Fornecedor |
| nexio.supplier-control-evidence.lost | O anexo que sustentava um controle já aplicado caiu: o pedido assinado de troca de canal, ou o comprovante da correção de identidade, revogado, eliminado, reprovado ou infectado depois da aplicação. Abre alerta de conformidade do fornecedor com esse motivo. Não é cosmético — era esse anexo o rastro que sustentava a escolha de não conferir o documento, e anexo que some sem alerta desfaz a mitigação inteira. Slug próprio, e não uma variante do compliance-changed: a audiência é outra, e nesta página a audiência é do slug obrig | PermissionHolders(approve:supplier-banks) · PermissionHolders(verify:suppliers) · TenantAdmin | E I P | imediato | Fornecedor |
| nexio.portal-invitation.sent | Convite de acesso ao portal enviado ao contato convidado. Audiência, canal e conteúdo são do Portal do Fornecedor; o slug fica reservado aqui e só é registrado quando o resolvedor do convidado existir | a definir pelo Portal | E | imediato | Portal |
| nexio.portal-access-link.reissued | Link de acesso reemitido. Cada emissão invalida o anterior, e é isso que o texto diz — senão o fornecedor guarda o primeiro e-mail e reclama do link morto. Audiência, canal e conteúdo são do Portal do Fornecedor; o slug fica reservado aqui e só é registrado quando o resolvedor do convidado existir | a definir pelo Portal | E | imediato | Portal |
| nexio.portal-round.closing | Rodada de cotação a encerrar no portal. Único aviso de prazo que fala com quem está fora do tenant, e por isso o único do portal com ritmo próprio. Audiência, canal e conteúdo são do Portal do Fornecedor; o slug fica reservado aqui e só é registrado quando o resolvedor do convidado existir | a definir pelo Portal | E | 24 h | Portal |
Em lugar nenhum do catálogo — e é isso que ele prova. AssignmentRole = Watcher do Centro de Custo existe só para receber notificação e nunca aprova. Ele não é um slug, é um parâmetro do resolvedor: CostCenterOwners aceita includeWatchers, e os dois avisos de estouro de orçamento o ligam. O de resíduo não o liga, porque pede ação, e ação é do dono. Destinatário e decisor são eixos diferentes, e confundi-los é como se acaba mandando aviso só para quem tem alçada.
Nenhum slug de "documento criado", "campo alterado" ou "status mudou". Aviso de mudança de estado sem consequência para quem recebe é exatamente o que faz um usuário desligar a categoria inteira — e junto com ela o aviso que importava. A trilha do GrydAudit já responde "o que aconteceu com este documento"; notificação responde "você precisa fazer alguma coisa, ou alguém precisa saber agora".
Nomes
Convenção de slug e de ação
Duas dívidas fecham com uma regra. A spec de Anexo propôs <entidade>.<verbo-no-passado> para as ações do GrydAudit e deixou a convenção sem dono. O slug de notificação adota a mesma cauda e acrescenta o prefixo do produto.
A forma
nexio.<entidade>.<verbo-no-passado> — entidade e verbo os dois em kebab-case, a entidade no singular e com o nome da classe do modelo, o verbo no particípio, descrevendo o que aconteceu, nunca o que se quer que a pessoa faça. nexio.attachment.revoked e nexio.approval-step.authority-exceeded, não nexio.attachment.please-review nem nexio.approval-step.authority_exceeded.
Uma grafia só, e é esta. Duas convenções fariam a conferência por máquina da RN-NOT-03 não ter o que conferir, e deixariam dois slugs para o mesmo fato sobreviver lado a lado. A cauda do slug é a mesma do nome da ação na trilha, e ela também é kebab.
Por que o prefixo
NotificationTemplate é tabela do framework, com TenantId anulável e sem coluna de produto. O segundo produto Gryd que semear supplier.approved colide com o nosso em silêncio, e o efeito é um cliente recebendo o texto do outro produto. Três segmentos custam nada agora.
A ação de auditoria
É o mesmo string sem o prefixo: attachment.revoked. Onde um aviso e uma ação de trilha descrevem o mesmo fato, os dois nomes são o mesmo nome — e a busca por "o que aconteceu com este documento" atravessa os dois módulos sem tabela de-para.
Nem toda ação vira aviso
A recíproca não vale e não deve valer. A trilha registra attachment.downloaded; ninguém precisa ser notificado disso. O catálogo é um subconjunto das ações, escolhido pela pergunta "alguém precisa agir ou saber agora?".
Domínio
Campos · NotificationDefinition
Não é tabela. É um registro em código, uma definição por slug, no mesmo padrão do AttachmentOwnerRegistry: o que é do produto e muda com release não vira linha que o tenant edita. O tenant personaliza o texto, pelo override de NotificationTemplate que o framework já oferece — não o comportamento.
Legenda: obrig sempre · cond conforme o caso · opc opcional · gerado derivado.
| Campo | Tipo | Regra | |
|---|---|---|---|
| slug | string | obrig | Chave. nexio.<entidade>.<verbo-no-passado>, ver § Convenção. Imutável depois de publicado: renomear slug é publicar outro e migrar as preferências, e a v1 não faz isso |
| category | enum | obrig | Approval · Budget · Currency · Catalog · Company · Supplier · Attachment · System. Só agrupa a tela. A preferência é por slug, não por categoria — a categoria existe para a lista de sessenta e quatro itens ser legível, não para ser desligada em bloco. Currency e Company entraram com os avisos de câmbio e de escopo por empresa: oito grupos para sessenta e quatro itens é uma tela que se lê |
| audienceType | enum | obrig | Um dos nove de § Resolução de destinatário, ou uma combinação deles — vários avisos de controle pedem mais de uma audiência, e a união é feita antes da preferência. Valor sem resolvedor registrado derruba a inicialização |
| audienceParams | objeto | cond | Os parâmetros do resolvedor escolhido, declarados no catálogo e não no disparo: role para TenantRole, includeWatchers e rollup para CostCenterOwners, channel para SupplierFinanceContact, permission para PermissionHolders, companyId para CompanyScopeAdmins. Obrigatório quando o resolvedor declara parâmetro sem padrão; parâmetro faltando ou desconhecido derruba a inicialização, na mesma varredura da § Semeadura. userIds e approvalRequestId não moram aqui — são do disparo, que é quem sabe de quem se trata |
| audienceMustResolve | bool | obrig | Padrão false. Audiência vazia nos slugs marcados cai no TenantAdmin e é registrada como falha de configuração, nunca como entrega de zero. Deixou de ser lista escrita à mão e passou a ser regra conferida na semeadura: é verdadeiro em todo aviso isMandatory cuja audiência interna for calculada — por papel, permissão, escopo ou descritor —, e falso quando ela é um User nomeado pelo próprio disparo. Audiência externa fica fora da derivação — cadeia do fornecedor, contato contratual, endereço do titular do pedido —, porque nela não existe recurso ao administrador: quem trata o vazio é a RN-NOT-29, recusando o ato na origem. No slug que tem as duas, a marca vale sobre a parte interna. Definição que viole a regra derruba a inicialização, na mesma varredura da § Semeadura. Uma lista escrita à mão já errou uma vez, e errou exatamente nos dois avisos que existiam para dizer que um controle não rodou; com o catálogo em sessenta e quatro slugs, erraria de novo em silêncio. |
| allowedChannels | enum[] | obrig | Email · InApp · Push — os três que o GrydNotifications entrega. Canal fora desta lista não aparece na tela e é recusado na API: não existe "ligar push" para um aviso que não faz sentido no celular. Sms saiu do enum: SMS e WhatsApp estão fora da v1 por decisão de produto, não por falta de implementação — são os canais mais atacados, por clonagem de número e por troca de chip, e o único slug que os usaria é justamente o controle antifraude bancário. O aviso bancário sai só por e-mail, e a redundância que ele perdeu é reposta pelo callback telefônico, que é obrigatório sempre e é evidência da tarefa — não canal de notificação. Ver Fornecedor. |
| defaultChannels | enum[] | obrig | Subconjunto de allowedChannels que vem ligado. É o valor que a ausência de linha em NotificationPreference significa, e por isso mudar este campo num release muda o comportamento de quem nunca configurou nada — de propósito, e é a razão de o campo ser do produto |
| isMandatory | bool | obrig | Padrão false. Verdadeiro nas quatro famílias descritas em § Notificação que não se desliga, cada uma com o motivo escrito. A conta cresceu com o catálogo: quase tudo o que as rodadas de validação acrescentaram é aviso de controle |
| mandatoryChannels | enum[] | cond | Não vazio quando isMandatory; vazio caso contrário. Normalmente um canal só — o que carrega o controle —, e os demais canais permitidos continuam desligáveis. É plural, por um caso e por uma razão: o aviso bancário no canal antigo vale contra uma caixa de e-mail comprometida, e um controle cuja redundância não cabe no campo que a declara é um controle que a semeadura não sabe conferir |
| mandatoryReason | string? | cond | Obrigatório quando isMandatory. Frase curta, em pt-BR, exibida ao lado do controle desabilitado. Não é texto de sistema: é a explicação que evita o usuário procurar outro jeito de desligar |
| variables | string[] | obrig | As variáveis que o disparo entrega ao template, cada uma com a sua classe — Identifier · Label · DateOrCount · Link. Confrontadas com RequiredVariables do template semeado, na inicialização. Classe ausente ou desconhecida derruba a inicialização, e texto livre digitado por uma pessoa não tem classe — ver § Payload e permissão |
| entityKey | string? | opc | Qual variável identifica a entidade para efeito de throttle e de agrupamento futuro. Nulo desliga o throttle por entidade e mantém só o de usuário + slug |
| throttleWindow | interval | obrig | Zero significa imediato e sem memória. Diferente de zero, a mesma tupla usuário + slug + entidade não repete dentro da janela. Ver § Ritmo |
| digestEligible | bool | obrig | Declarado na v1, consumido na v2. Está aqui porque acrescentar o campo depois obrigaria a revisar o catálogo inteiro sob pressão de release — e o catálogo já passou de vinte e sete para sessenta e quatro, que é a melhor prova de que a decisão estava certa |
| audit | string? | opc | A ação correspondente na trilha, quando existe: attachment.revoked. É o slug sem o prefixo, e serve de conferência automática da convenção |
Domínio
Campos · NotificationPreference
Herda de TenantScopedEntity. Id e o bloco de auditoria e soft delete vêm do BaseEntity e não estão repetidos. A tabela guarda exceção, não estado — quem nunca mexeu em nada não tem uma linha sequer.
| Campo | Tipo | Regra | |
|---|---|---|---|
| tenantId | Guid | gerado | Do token, nunca aceito do cliente |
| userId | Guid | obrig | Sempre o do próprio requisitante. Não existe editar a preferência de outra pessoa — administrador que pudesse fazer isso contornaria a decisão 2 por outro caminho |
| slug | string | obrig | Precisa existir no catálogo. Slug desconhecido é 422, não 404: o cliente mandou algo que este release não conhece |
| channel | enum | obrig | Precisa estar em allowedChannels da definição |
| enabled | bool | obrig | Só se grava quando difere de defaultChannels. Gravar um valor igual ao padrão apaga a linha em vez de criá-la — é o que faz a preferência acompanhar a mudança de padrão do produto |
Índice único por (tenantId, userId, slug, channel). A leitura é uma varredura do catálogo contra as poucas linhas do usuário, resolvida em memória — não há join, e a tela carrega com uma consulta.
NotificationThrottle
| Campo | Tipo | Regra | |
|---|---|---|---|
| tenantId · userId · slug | Guid · Guid · string | obrig | Quem recebeu o quê |
| entityId | string | obrig | O valor de entityKey naquele disparo, ou a string vazia quando a definição não tem entityKey |
| lastSentAt | timestamptz | gerado | Upsert na entrega. Índice único nas quatro colunas anteriores |
Tabela de estado, não de negócio: um job diário apaga o que passou da maior janela do catálogo, e perder a tabela inteira reenvia avisos — não corrompe nada. É a única coisa neste documento que poderia ter ficado em cache; ficou em tabela porque um reinício de aplicação no meio de uma varredura de vencimentos mandaria o lote inteiro de novo.
Decisão 3
Resolução de destinatário
O disparo não conhece pessoas. Ele diz que audiência quer, e o resolvedor daquele tipo devolve a lista — com o motivo de cada um estar nela, que vai para a trilha e responde depois a pergunta "por que eu recebi isto?".
| Tipo | Parâmetros | Quem implementa | Devolve |
|---|---|---|---|
| User | userIds[] | o próprio módulo | As pessoas nomeadas pelo consumidor, filtradas por tenant e por usuário ativo. É o caso trivial, e cobre "quem submeteu", "quem criou" e "quem importou" — o consumidor sabe quem foi |
| TenantRole | role | Auth · gryd | Todos os usuários ativos com o papel no tenant. É aqui que "responsáveis pelo catálogo" mora: o levantamento contava seis audiências, e essa era papel com outro nome. O role é declarado por slug em audienceParams — ver o card ao lado |
| CostCenterOwners | nodeId · rollup · includeWatchers | Centro de Custo | Os CostCenterAssignment vigentes no nó — Owner, Deputy quando não há Owner, e Watcher quando pedido. Com rollup, sobe o path até a raiz. Este resolvedor mora no módulo de centro de custo, e é a razão inteira da decisão 3 |
| PendingApprovers | approvalRequestId | Fluxo de Aprovação | Os ApprovalAssignment em Pending ou Active da instância. Nunca os já decididos, nunca os obsoletos |
| SupplierFinanceContact | supplierId · channel | Fornecedor | Não devolve usuário — devolve endereços de e-mail, e só isso. Consentimento não é condição: o aviso é prevenção a fraude, não comunicação de marketing. A cadeia é a do Fornecedor, nesta ordem: contato Finance → PortalAdmin → o contato Commercial designado → o contato designado do estabelecimento principal. Outro contato Commercial e o e-mail do endereço do fornecedor nunca entram. Audiência vazia não cai no administrador do tenant — o administrador recebe cópia, nunca substitui o fornecedor —, e sem nenhum endereço a proposta não abre, com recusa do Fornecedor. channel = Previous lê o contato Finance vigente antes da alteração proposta, qualquer que seja o changeKind dela: sem isso, reescreve-se o contato num PUT e abre-se a proposta bancária em seguida, e o "canal anterior" já é o do atacante. Ver § Obrigatória |
| TenantAdmin | — | Auth · gryd | Os administradores do tenant no GrydAuth. É o destinatário de último recurso de todo aviso de governança — o que a marca audienceMustResolve exige que exista — e o destinatário próprio de dezessete avisos em que não há dono de negócio: virada de exercício travada, passo parado, vigência de membro de grupo a vencer, pedido de eliminação sem executor elegível. Depende de consulta que só a plataforma pode responder — "quem administra este tenant" —, pedida na demanda de plataforma. Enquanto ela não existir, o recurso não tem para onde ir, e é por isso que o pedido está escrito em vez de contornado |
| PermissionHolders | permission | Auth · gryd | Quem resolve a permissão hoje no tenant — por atribuição direta ou por papel —, e não quem tem um papel com aquele nome. É a audiência de quase todo aviso bancário (approve:supplier-banks) e do pedido de eliminação (erase:attachments). A diferença para TenantRole é a que importa em controle: papel é rótulo do cliente e muda de conteúdo sem avisar; permissão é o que de fato abre a porta. Mesma dependência de plataforma do TenantAdmin, e a mesma do evento de concessão e retirada de papel que os dois avisos de papel de controle consomem |
| CompanyScopeAdmins | companyId | Empresa | Quem tem linha de escopo vigente naquela empresa e a permissão de administrar escopo — as duas coisas ao mesmo tempo. Não é PermissionHolders filtrado depois: a linha vigente é o que faz a pessoa enxergar a empresa, e resolver a permissão sem ela devolveria gente que não pode agir. Mora na Empresa, pela mesma razão que o rollup mora no centro de custo. Devolve vazio exatamente no caso que os dois avisos dela existem para tratar — e aí o recurso é o TenantAdmin |
| TenantContractContact | — | Auth · gryd | Não devolve usuário — devolve o endereço de quem assinou o contrato com a R&G, que está fora do tenant e não tem tela. Um consumidor só, e ele justifica o tipo: é o único destinatário que um administrador comprometido não consegue silenciar, e o aviso que ele recebe é o de alguém ganhar poder de aprovar conta bancária. Depende da plataforma — o Nexio não guarda quem assinou o contrato — e está na demanda. Sem o resolvedor, esse aviso não teria como sair do tenant, que é o ponto inteiro dele |
Papel é do tenant: quem cria, renomeia e povoa é o cliente, e o produto não tem como saber que a controladoria dele se chama "Governança & Compliance". Por isso o Nexio semeia três papéis funcionais no TenantCreated, com chave estável e rótulo editável — Controladoria, Compras e Catalogo —, e é a chave que o catálogo de slugs cita. O cliente muda o rótulo, escolhe quem está em cada um e acrescenta os papéis que quiser; o que ele não faz é apagar a chave. É a mesma mecânica de semente com sobreposição já usada em NotificationTemplate, AttachmentType e ReasonCode — a quarta repetição dela no acervo
Sem o campo que diz qual papel, um slug com TenantRole entrega a quem o resolvedor encontrar — e pode entregar a ninguém. Foi o que a validação mediu em approval-step.authority-exceeded e budget.approval-requirement-unmet, os dois avisos criados exatamente para dizer "o controle não rodou": zero pessoas, sem erro. Um aviso de governança que não chega é pior do que não existir, porque alguém acredita que ele existe.
O resolvedor mora no dono do conceito
O módulo de notificação declara a interface e o registro; quem implementa CostCenterOwners é o centro de custo, PendingApprovers é o fluxo, CompanyScopeAdmins é a empresa. Sem isso, "notificação" vira o depósito onde regra de negócio de outros módulos se acumula — e o rollup do path passa a ter duas implementações que divergem no primeiro nó com vigência vencida.
E quando o dono do conceito é a plataforma, o resolvedor não se improvisa aqui. Três dos nove — administrador do tenant, detentores de uma permissão e contato contratual — respondem a perguntas que só o GrydAuth sabe responder. O Nexio não deduz nenhuma delas por conta própria: não varre papéis procurando quem parece administrador, não infere permissão por nome de papel, e não guarda uma segunda cópia de quem assinou o contrato. O que ele faz é declarar o requisito, com critério de aceite, na demanda de plataforma — e deixar o aviso registrado como não entregue por falta de resolvedor até ela existir, em vez de entregar a alguém parecido.
Tipo sem resolvedor derruba a inicialização
Mesma disciplina do AttachmentOwnerRegistry: a validação acontece no startup, não em produção às três da manhã com a fila cheia. Acrescentar um tipo de audiência custa exatamente o que custa implementar o resolvedor dele.
Lista vazia não é erro
Nó de centro de custo sem Owner vigente, instância sem aprovador pendente, papel sem ninguém: o disparo registra recipients: 0 com o motivo e não lança exceção. A exceção é uma marca por slug — audienceMustResolve. Todo aviso de governança a leva, e ela obriga a duas coisas: fallback declarado para o papel de administrador do tenant, e registro do esvaziamento como falha de configuração na trilha, não como entrega de zero. O contrário — a lista fixa — foi o que deixou authority-exceeded e approval-requirement-unmet de fora, que eram justamente os dois avisos que existem para dizer que um controle não rodou.
Deduplicação é do resolvedor combinado
Um disparo pode pedir mais de uma audiência — o lembrete de SLA vai ao aprovador e a quem submeteu. A união é feita por userId antes da preferência ser consultada, então a mesma pessoa nas duas listas recebe uma vez, e o motivo registrado traz as duas razões.
Decisão 4
Payload e permissão
Uma notificação sai do servidor para uma caixa de entrada que ninguém controla, para uma pessoa cuja permissão não foi checada no momento em que o texto foi montado. A tela sabe esconder o valor; o e-mail já saiu. O Orçamento escreveu a regra primeiro, para o caso dele. Aqui ela vira regra do produto.
A regra, e é uma só. O corpo de uma notificação carrega identificação — número do documento, nome do centro de custo, nome do fornecedor, data —, contexto não sensível — o que aconteceu, o que se espera de quem lê — e um link. Nunca carrega valor sob permissão: quantia, saldo, preço unitário, total do documento, limite de alçada. "O pedido PC-2026-0341 ficou fora do orçamento no CC Engenharia · abrir" é a notificação inteira. O número está do outro lado do link, onde a permissão é checada uma vez, no lugar em que ela já era checada.
E a regra vale sobre o conteúdo, não sobre o nome da variável. Uma lista de nomes proibidos (amount, total, balance…) só pega o que ninguém erraria. O vazamento real tem outro nome: reasonComment — o texto livre que a spec de Motivo torna obrigatório em toda rejeição, e cujo exemplo de venda é "Fora do orçamento — a verba de março já foi para o contrato de manutenção". Uma frase escrita por uma pessoa não tem como ser classificada por nome. Por isso a lista deixa de ser negativa e passa a ser positiva: uma variável de notificação só pode ser identificador, rótulo vindo de lista fechada (o reasonLabel, o nome do estado, o nome do processo), data ou contagem, ou link. Texto digitado por uma pessoa nunca é variável de notificação — nem comentário, nem justificativa, nem descrição, nem observação, qualquer que seja o nome do campo.
A alternativa era marcar variáveis como sensíveis e montar um corpo por pessoa, cortando o que ela não pode ver. Custa renderizar N vezes por notificação, resolver permissão fora do contexto de uma requisição — onde o token não existe — e ainda assim não resolve nada: o e-mail correto, entregue à pessoa certa, é encaminhado no dia seguinte para quem não podia ver. Um controle que depende de o destinatário não repassar não é controle.
Cada variável da definição declara a sua classe — Identifier, Label, DateOrCount, Link —, e o confronto com o RequiredVariables do template semeado acontece na inicialização. Variável sem classe, ou de classe fora das quatro, não sobe; a lista de nomes proibidos continua, agora como segunda malha e não como a única. É a diferença entre "não deixe passar total" e "só deixe passar o que se sabe classificar" — a primeira depende de alguém prever o nome errado, a segunda não. Assim a regra é verificada uma vez por deploy, e não vinte vezes por dia em tempo de execução.
Consequência aceita: o e-mail é menos útil. Um gestor que recebe "estourou" e quer saber quanto precisa abrir o sistema. É o preço, foi escolhido de olhos abertos, e vale porque o erro contrário — o número no e-mail de quem não devia vê-lo — não tem correção depois de acontecer.
Decisão 2
Notificação que não se desliga
Vinte e nove dos sessenta e quatro, em quatro famílias — e a proporção subiu de propósito. Quase tudo o que as rodadas de validação acrescentaram é aviso de controle, e aviso de controle desligável é controle que não existe. A lista slug a slug está no catálogo, na coluna de obrigatoriedade; aqui ficam as famílias, porque é o motivo que decide, não o nome.
| Slug | Canais obrigatórios | Motivo exibido na tela |
|---|---|---|
| 1 · O aviso é o controle | Troca de conta e de canal de aviso, contestação, permissão bancária concedida, carga inicial, divergência com o ERP, correção de identidade. Motivo exibido: "Este aviso vai para o contato cadastrado antes da alteração. É o que detecta uma troca feita por quem tomou a caixa de e-mail nova." | |
| 2 · O ato que não aconteceu | InApp | Anexo infectado, antes ou depois de liberado. Motivo exibido: "Um arquivo que você anexou foi recusado pelo antivírus. Sem este aviso, o documento fica parado sem ninguém saber por quê." |
| 3 · O controle não rodou | Email · InApp | Política publicada, configuração crítica efetivada, taxa sobrescrita, desfecho recusado, passo parado, delegação criada por terceiro, empresa sem administrador, virada travada. Motivo exibido: "Este aviso diz que um controle deste sistema não funcionou como desenhado. Quem responde por ele precisa ver." |
| 4 · O prazo é de lei | Pedido de eliminação a pedido do titular: registrado, travado, expurgo recusado, concluído, cancelado de novo. Motivo exibido: "Alguém pediu a eliminação dos próprios dados e há um prazo para responder. Este aviso não se desliga." |
Família 1 — o aviso é o controle
O aviso bancário no canal antigo não acompanha um controle: ele é o controle. Desligável, o controle simplesmente não existe, e o cliente descobre isso depois do primeiro pagamento desviado. Vale a pena notar que este destinatário não é usuário do tenant, e por isso nem chega à preferência — ver abaixo.
E ele sai por um canal só, de propósito. A objeção é justa: quem consegue abrir a proposta fraudulenta é, quase por definição, quem já tem a caixa de e-mail. A resposta da v1 não é um segundo canal de aviso — SMS e WhatsApp foram cortados porque são o que o mesmo fraudador toma em seguida, e um segundo canal fraco dá confiança sem dar controle. A resposta é o callback de saída: uma ligação para telefone confiável, obrigatória, registrada como evidência da tarefa no Fornecedor, que nenhum aviso substitui. Redundância fora de banda de verdade, e não mais um e-mail com outro nome.
Família 2 — o ato que não aconteceu
Anexo recusado pelo antivírus conta uma história só: a pessoa acredita que fez alguma coisa, e não fez. Silêncio aqui não é economia de ruído, é um documento parado em rascunho que ninguém procura. Por isso o canal obrigatório é InApp — o mais barato de todos, o que não invade e o que aparece quando a pessoa volta.
Família 3 — o controle não rodou
Um passo que parou, uma política publicada, uma configuração crítica efetivada, uma taxa sobrescrita, um desfecho recusado, uma empresa que ficou sem administrador. O conteúdo destes avisos é sempre o mesmo: o desenho previa uma segunda pessoa, e a segunda pessoa não apareceu — ou apareceu por um caminho que ninguém escolheu. São os avisos cujo destinatário é, por construção, quem não se beneficia do silêncio; e é por isso que eles são também os que mais precisam da marca audienceMustResolve: entregar a zero pessoas aqui é o modo de falha que a validação encontrou duas vezes.
Família 4 — o prazo é de lei
A eliminação a pedido do titular tem prazo, e quem espera está fora do tenant: não tem conta, não tem tela e não tem como perguntar. Os avisos deste caminho contam as quatro coisas que podem acontecer — o pedido foi recebido, está travado por uma decisão em curso, o expurgo foi recusado, os bytes saíram. O que eles nunca carregam é nome de arquivo, número de documento, código de recusa ou comentário: o destinatário é externo, e o aviso que protege um dado pessoal não pode vazar outro.
Obrigatória não é em todos os canais
mandatoryChannels tem um canal só em todos eles: o que carrega o controle. Os demais canais permitidos continuam sendo escolha do usuário: quem quiser o aviso de arquivo infectado também por e-mail liga, e quem não quiser push desliga. Obrigar os três seria o caminho mais curto para a pessoa criar uma regra de caixa de entrada e nunca mais ver nenhum.
Quem não é usuário não tem preferência
O contato financeiro do fornecedor não tem conta, não tem tela e não tem linha em NotificationPreference. Para essa audiência o pipeline pula a consulta de preferência inteira — não "consulta e não acha", pula, porque perguntar a preferência de quem não pode ter uma é como se cria um caminho para o controle falhar silenciosamente quando alguém, um dia, criar a linha.
Decisões 6 e 7
Throttle, digest e locale
Throttle — o que a v1 constrói
Antes de entregar, o pipeline consulta NotificationThrottle pela tupla usuário + slug + entidade. Dentro da janela declarada, a entrega é descartada — não adiada, não enfileirada, não somada. É o comportamento certo para um lembrete: a informação da segunda cópia é a mesma da primeira, e ela ainda está lá.
O caso que o justifica é o lembrete de SLA. Um aprovador com quarenta tarefas vencendo recebe, sem throttle, quarenta e-mails; recebe, com janela de 24 horas por instância, um por instância por dia — que ainda é muito, e é exatamente por isso que digestEligible já está marcado neste slug.
Digest — o que a v1 declara e não constrói
digestEligible e entityKey em todas as definições do catálogo, NotificationThrottle registrando o que já saiu, e o INotificationScheduler do framework. O que falta da v2 é a janela por usuário, o template de resumo e a decisão de o que fazer com um aviso obrigatório dentro de um agrupado — e essa última é a razão principal de não fazer agora.
Agrupamento é mais spec do que este documento inteiro, e o produto ainda não tem um cliente para dizer qual é a janela certa. O throttle resolve a maior parte do ruído por um custo de uma tabela de quatro colunas. Fazer o digest antes de existir volume é desenhar a solução com números inventados.
Locale
Cascata de três, resolvida no momento da renderização, por destinatário: preferência do usuário, depois padrão do tenant, depois o template global — que é o NotificationTemplate com TenantId nulo e o Locale de fábrica. O desenho é o mesmo da resolução de taxa em Moeda e Câmbio e da herança de política de orçamento: uma cascata declarada, sem exceção por chamador.
Para o contato financeiro do fornecedor, que não é usuário, a cascata começa no tenant. Fornecedor estrangeiro com aviso em português é um problema real e conhecido — o Supplier tem SupplierType = Foreign e poderia carregar um locale. Fica declarado como gancho e não implementado na v1, porque só o portal do fornecedor torna isso frequente.
Decisão 8
Scanning: o documento espera
A spec de Anexo decidiu que documento com anexo em Scanning não submete — ATTACHMENT_SCAN_PENDING_CONFLICT —, escreveu que prender o usuário na tela é solução ruim, e deixou a saída declarada em aberto para ser fechada aqui. Está fechada, e sem fila: a recusa continua, a tela acompanha a liberação pela consulta em lote do Anexo e reenvia, e é a pessoa que reenvia.
| Passo | O que o sistema faz | O que a pessoa vê |
|---|---|---|
| 1 · Recusa | Devolve ATTACHMENT_SCAN_PENDING_CONFLICT com a lista de anexos que travam | "Dois anexos ainda estão em verificação." Nada a escolher: a tela mostra o que falta e o botão de enviar volta a funcionar quando faltar nada |
| 2 · Espera | Nada é criado. O estado que interessa já existe e é do Anexo: o veredito de cada arquivo, lido pela consulta em lote | A lista dos anexos que faltam, atualizada na própria tela do documento |
| 3 · Veredito limpo | Os anexos ficam Available e a submissão normal passa — pela pessoa, com a permissão dela, no contexto de uma requisição | O botão de enviar volta a funcionar. Nenhum aviso: quem está na tela vê, e quem saiu volta e reenvia |
| 4 · Veredito sujo | Infected, Failed ou recusado: o anexo não libera, e a submissão continua recusada pelo mesmo 409 | nexio.attachment.infected, nexio.attachment.scan-failed ou nexio.attachment.rejected a quem anexou, com a ação que resolve — remover o anexo ou pedir varredura nova |
| 5 · Silêncio | Sem veredito, o documento fica em rascunho, intacto, e ninguém decidiu nada por ninguém | O operador da plataforma já foi alertado pela fila do GrydFiles, que é onde o problema realmente está |
Quem avisa é o anexo, não a submissão
Os avisos deste caminho são todos do Anexo e vão a quem anexou — ou ao responsável pelo documento, quando o arquivo veio pelo portal e uploadedByUserId é nulo. Não há aviso para "quem tentou submeter", porque não há mais nada registrando quem tentou: a tentativa é uma requisição recusada, não um pedido guardado.
Nenhum comando roda em nome de uma pessoa fora de uma requisição
É a razão inteira de a fila ter saído. Autorizar no pedido e executar horas depois, num job sem identidade, é como fila assíncrona vira furo de alçada — e a plataforma hoje não restaura sequer o tenant fora de requisição, quanto mais a permissão e o escopo por empresa de quem pediu. Enquanto isso não existir, submeter é ato de gente. O requisito de execução delegada está na página de Mensageria e na demanda de plataforma, para a v2.
O descritor não ganhou pergunta nova
A fila teria obrigado o AttachmentOwnerRegistry a saber "como se submete este documento, e quem pode". Sem ela, o descritor fica com as oito perguntas que já tinha, e o Anexo não carrega uma política de submissão que ninguém mais consome — o que é também a regra de contar o segundo consumidor antes de desenhar.
O que se perde, e por que vale
Perde-se a conveniência de sair da tela e o documento seguir sozinho. Ganha-se que nenhuma aprovação e nenhum empenho nascem de um processo sem gente: o pior caso da fila era um worker submetendo, aprovando por política e empenhando verba com a identidade de alguém que já tinha perdido a permissão. Conveniência contra alçada não é uma troca difícil.
Operação
Semeadura e versão de template
O Nexio semeia, no startup e de forma idempotente, um NotificationTemplate global — TenantId nulo — por slug e por locale suportado. A semeadura é o momento em que seis coisas são verificadas de uma vez.
1 · Todo slug tem template
Definição sem template semeado para o locale padrão derruba a inicialização. O modo de falha que isso evita é o pior de todos: o disparo acontece, a fila aceita, o renderizador não acha o template e o aviso desaparece sem erro visível.
2 · As variáveis batem
variables da definição é comparado com RequiredVariables do template. Variável exigida pelo template e não declarada pela definição derruba; declarada e não usada apenas registra aviso, porque texto encurtado é normal.
3 · Nenhuma variável é sensível
A lista de nomes proibidos de § Payload é aplicada aqui. É onde a decisão 4 deixa de depender de alguém lembrar dela.
4 · A convenção é conferida
Slug fora do formato nexio.<entidade>.<verbo-no-passado>, ou audit que não seja o slug sem o prefixo, derruba. Convenção conferida por máquina é convenção; conferida por revisão é intenção.
5 · O parâmetro de audiência está lá
Resolvedor com parâmetro sem padrão exige audienceParams na definição, e role só aceita chave de papel semeada. Parâmetro faltando, sobrando ou desconhecido derruba (RN-NOT-24) — e tipo de audiência sem resolvedor registrado, também (RN-NOT-12).
6 · A marca de audiência bate com a regra
audienceMustResolve é derivado, não escrito à mão: definição cuja marca não bate com a regra da RN-NOT-28 derruba a inicialização. É o que impede a lista escrita à mão de errar de novo, em silêncio, num catálogo deste tamanho.
O tenant personaliza o texto, não o comportamento. O override por tenant já existe no framework: uma linha de NotificationTemplate com TenantId preenchido para o mesmo slug ganha do template global, e o Nexio não precisa escrever uma linha de código para isso funcionar. O que o tenant não muda é audiência, canal padrão, obrigatoriedade, janela ou variáveis — esses são do produto, e é a diferença entre um catálogo e uma tela de configuração que ninguém consegue suportar.
Segurança
Escopo, permissões e trilha
Escopo por empresa
A audiência é resolvida dentro do escopo do documento. Um aviso sobre um documento da empresa B não alcança quem tem UserCompanyScope só na empresa A, mesmo que a pessoa tenha o papel certo — o filtro é aplicado depois do resolvedor e antes da preferência. É a mesma regra que faz o documento responder 404 fora do escopo, e não uma regra nova.
Permissões
| Permissão | Semeada em | Observação |
|---|---|---|
| update:notification-preferences | todos os papéis operacionais | Editar as próprias preferências (PUT /notifications/preferences). Não existe editar a de outra pessoa, e essa ausência é deliberada: seria o caminho de contorno da decisão 2 |
| delete:notification-preferences | todos os papéis operacionais | Apagar todas as exceções das próprias preferências e voltar ao padrão do produto (DELETE /notifications/preferences). Também nunca vale para outra pessoa |
| read:notification-preferences | todos os papéis operacionais | Ler as próprias preferências já resolvidas contra o catálogo. Separada da de escrita porque um endpoint declara exatamente uma permissão, e nada é implicado por outra |
| read:notification-catalogs | Administrador | A tela de catálogo — slug, audiência, canais, obrigatoriedade, variáveis e janela. Leitura pura: o catálogo é código |
Trilha
Uma ação no GrydAudit, na convenção fechada em § Convenção: notification-preference.changed, com slug, canal, valor anterior e novo. O GrydAudit está em correção pelo time do Gryd.IO — isolamento por tenant, permissão e gravação na mesma transação do ato —, e o Nexio não o mapeia até a correção da plataforma: nenhuma tela de ato de controle entra em produção antes dela. A entrega em si não é auditada aqui: NotificationDeliveryAttempt do framework já guarda tentativa, canal, momento e falha, e duplicar isso na trilha seria escrever duas versões da mesma verdade.
Alerta de operador não é notificação de tenant. A fila de scan alta e a base de assinaturas com mais de 48 horas do GrydFiles, e qualquer alerta de infraestrutura, vão para o operador da plataforma. Eles não têm slug nexio.*, não aparecem na tela de preferências de ninguém, não passam por NotificationPreference e não são resolvidos por audiência de tenant. A fronteira precisa estar escrita porque os dois usam o mesmo GrydNotifications e a tentação de reaproveitar o catálogo é imediata — e o resultado seria um usuário de compras podendo desligar o alerta que mantém o antivírus vivo.
Interface
Telas
Uma linha por slug do catálogo — sessenta e quatro hoje —, agrupadas pelas oito categorias, com três colunas de canal: e-mail, in-app e push, que são os três que existem. O que a categoria faz é agrupar, não desligar em bloco — não há interruptor de categoria, porque ele juntaria a sanção de fornecedor com o lembrete de cadastro. Vinte e nove dessas linhas têm pelo menos um canal travado, e a tela mostra o cadeado e o motivo em cada um.
Linha obrigatória mostra cada canal travado com cadeado e o mandatoryReason ao lado, em texto normal — não em tooltip. Os demais canais dessa linha continuam clicáveis.
Nada de "salvar": cada toggle é um PUT. E o rodapé diz a frase que evita o chamado mais comum: "avisos novos entram ligados ou desligados conforme o padrão do produto; o que você mudou aqui continua valendo."
Somente leitura, com read:notification-catalogs. Uma linha por slug com audiência, canais, janela, obrigatoriedade e as variáveis declaradas. Existe para responder, sem abrir código, as duas perguntas que o suporte recebe: "por que o fulano recebeu isso?" e "por que ninguém recebeu?".
A segunda pergunta é a que a coluna de audiência responde: nó sem Owner vigente entrega a zero pessoas, e isso é configuração do cliente, não defeito.
Não existe tela de composição, de envio manual, de reenvio nem de caixa de saída. A caixa in-app é do UserNotification, que é do framework e já tem a sua. Escrever uma segunda seria o começo de um cliente de e-mail dentro de um sistema de compras.
API
Endpoints
| Rota | Permissão | Devolve | Erros |
|---|---|---|---|
| GET /notifications/preferences | read:notification-preferences | O catálogo já resolvido contra as linhas do usuário: slug, categoria, rótulo, canais permitidos, estado atual de cada um, isMandatory e mandatoryReason. A tela não precisa saber o que é exceção e o que é padrão | — |
| PUT /notifications/preferences | update:notification-preferences | Lote de {slug, channel, enabled}. Grava, apaga ou ignora cada item conforme ele difira do padrão — o cliente manda o estado desejado e não pensa nisso. Sempre do próprio usuário | 400 · 422 |
| DELETE /notifications/preferences | delete:notification-preferences | Apaga todas as exceções do usuário: volta tudo ao padrão do produto. Uma linha na tela, e o caminho de saída de quem se perdeu nos toggles | — |
| GET /notifications/catalog | read:notification-catalogs | O catálogo inteiro como o código o declara, incluindo variables e audienceType. Não é paginado: são dezenas de itens e o número muda por release | — |
Não há rota de disparo. Notificação é emitida em processo, por INotificationDispatcher, pelo módulo que sabe que o fato aconteceu — nunca por chamada HTTP de fora. Uma rota de envio seria, no primeiro dia, o caminho para mandar e-mail em nome do produto a partir de qualquer lugar.
Contrato
Contrato de erros
O status HTTP é derivado do sufixo do código, nunca da mensagem, e o código exato vence o sufixo: o mapeador testa primeiro os códigos canônicos e só então os 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.
| Código | HTTP | Quando |
|---|---|---|
| NOTIFICATION_SLUG_UNKNOWN_UNPROCESSABLE | 422 | Slug que este release não conhece. É 422 e não 404 de propósito: o recurso não some, o cliente é que está desatualizado |
| NOTIFICATION_CHANNEL_NOT_ALLOWED_UNPROCESSABLE | 422 | Canal fora de allowedChannels daquele slug |
| NOTIFICATION_CHANNEL_MANDATORY_UNPROCESSABLE | 422 | Tentativa de desligar um canal de mandatoryChannels. A resposta traz o mandatoryReason, para a tela não ter que carregar o texto duas vezes |
| NOTIFICATION_PREFERENCE_FORBIDDEN | 403 | userId diferente do requisitante. Não é 404: negar a existência aqui não protege nada e confunde o cliente |
Invariantes
Regras de negócio
Nenhuma decisão, prazo ou estado depende de um aviso ter sido entregue. A fila do aprovador é ApprovalAssignment; o extrato do orçamento é BudgetEntry; o vencimento é ItemCompliance. Um tenant com o e-mail quebrado por uma semana perde avisos e não perde nada mais.
Definição sem template para o locale padrão, ou template sem definição, derruba a inicialização. O modo de falha evitado é o silencioso: disparo aceito, template não encontrado, aviso que desaparece sem erro.
nexio.<entidade>.<verbo-no-passado>, conferido por máquina na semeadura. A recíproca não vale: nem toda ação auditada tem aviso.
Nunca "desligado" e nunca "ligado" por convenção do código. É o que faz um slug publicado depois funcionar para quem configurou antes — sem migração e sem linha órfã.
Gravar um valor igual ao padrão apaga a linha. Uma tabela que guardasse o estado inteiro congelaria cada usuário no catálogo do dia em que ele mexeu.
Não há rota, não há permissão e não há tela. Um administrador que pudesse fazê-lo teria, por consequência, o poder de desligar um aviso obrigatório de outra pessoa.
Um canal por slug obrigatório, com motivo escrito e exibido. Obrigar todos os canais é o caminho mais curto para a regra de caixa de entrada que apaga tudo.
O contato financeiro do fornecedor não tem conta nem linha. O pipeline pula a consulta — não consulta e ignora o resultado.
Quantia, saldo, preço, total e limite de alçada ficam do outro lado do link. Generaliza a RN-O22 do Orçamento para o produto inteiro.
Toda variável declara Identifier, Label, DateOrCount ou Link, e a lista de nomes proibidos continua como segunda malha — lista de nomes não vê conteúdo. A regra 09 não depende de alguém lembrar dela ao escrever o vigésimo disparo.
Rollup de centro de custo é do centro de custo; aprovador pendente é do fluxo. O módulo de notificação declara o contrato e nunca a regra.
Mesma disciplina do ownerType sem descritor no Anexo: falha no startup, não em produção.
E antes da preferência. Papel certo em empresa errada não recebe, pela mesma regra que faz o documento responder 404.
Registra recipients: 0 com o motivo. Nos slugs com audienceMustResolve, "ninguém" é falha de configuração: cai no papel de administrador do tenant e é registrado como tal. A marca alcança só a audiência interna: audiência externa vazia não cai no administrador, e o ato que dependia do aviso é recusado na origem (RN-NOT-29).
União por userId antes da preferência, com os motivos somados. O lembrete de SLA para quem é aprovador e requisitante do mesmo documento é um e-mail, não dois.
Dentro da janela, a segunda cópia é jogada fora. Adiar acumularia uma fila que entrega tudo junto no fim da janela, que é o pior dos dois mundos.
A emissão é em processo, pelo módulo que sabe que o fato aconteceu. Rota de envio é, no primeiro dia, o caminho para mandar e-mail em nome do produto de qualquer lugar.
Fila de scan, base de assinaturas velha e falha de infraestrutura são da plataforma. Não têm slug nexio.* e não aparecem em tela nenhuma do cliente.
NotificationDeliveryAttempt já guarda tentativa, canal, momento e falha. A trilha do Nexio registra só o que é decisão do usuário: mudança de preferência e pedido ou abandono de fila.
Todo resolvedor com parâmetro sem padrão exige audienceParams na definição do slug, e role só aceita chave de papel semeada. Parâmetro faltando, sobrando ou desconhecido derruba a inicialização — mesma disciplina do tipo sem resolvedor (RN-NOT-12) e da variável sensível (RN-NOT-10).
Por quê: seis slugs declaravam TenantRole sem dizer qual papel, e a definição não tinha onde dizer. O modo de falha não era um erro: era a entrega silenciosa a zero pessoas, num aviso que a spec descreve como obrigatório.
Comentário, justificativa, descrição e observação — inclusive o reasonComment obrigatório em toda rejeição — nunca são variável de aviso. O aviso leva o ponteiro para o documento e o rótulo do motivo, que vem de lista fechada. Vale qualquer que seja o nome do campo, e é por isso que a classificação é por classe declarada e não por lista de nomes proibidos (RN-NOT-10).
Por quê: a RN-NOT-09 era verdadeira para amount e total, que ninguém erraria, e falsa exatamente onde o vazamento acontece. "Fora do orçamento — a verba de março já foi para o contrato de manutenção" é o exemplo que o acervo usa para vender o campo, e ele sai por e-mail para quem não pode ver nem a verba nem o contrato.
Administrador do tenant, detentores de uma permissão e contato contratual são perguntas do GrydAuth. O Nexio não varre papéis procurando quem parece administrador, não infere permissão por nome de papel e não guarda segunda cópia de quem assinou o contrato. Sem a consulta, o aviso é registrado como não entregue por falta de resolvedor — nunca entregue a alguém parecido.
Por quê: a aproximação erraria para os dois lados. Quem parece administrador e não é recebe aviso de poder de pagamento; quem é e não parece fica sem. Os dois erros são silenciosos, e o segundo é o que a fraude procura.
audienceMustResolve é derivado do catálogo, não escrito à mãoÉ verdadeiro em todo aviso isMandatory cuja audiência interna seja calculada — por papel, permissão, escopo ou descritor — e falso quando ela é um User nomeado pelo disparo. Audiência externa não entra na derivação: marcar um slug que só fala com o fornecedor ou com o titular mandaria ao administrador do tenant justamente o aviso que a RN-NOT-29 proíbe desviar. Num slug com as duas, a marca vale sobre a parte interna. A semeadura confere a regra e derruba a inicialização quando uma definição a viola.
Por quê: a lista escrita à mão já errou uma vez, e errou exatamente nos dois avisos que existiam para dizer que um controle não rodou. Com o catálogo em sessenta e quatro slugs, uma lista à mão erra de novo — e erra em silêncio, que é o modo de falha que esta marca existe para não ter.
Quando o destinatário é o fornecedor ou o titular de um pedido de eliminação, audiência vazia não é substituída por gente de dentro: o administrador recebe cópia, nunca no lugar. Sem nenhum endereço externo, o ato que dependia do aviso é recusado na origem pelo módulo dono — a proposta de conta não abre.
Por quê: o aviso ao canal anterior existe para o fornecedor legítimo dizer "não fui eu". Trocar esse destinatário por um usuário do tenant mantém a aparência do controle e remove a única pessoa que sabe que a conta não é dela.
Nome de arquivo, número de documento, código de recusa, tipo de decisão pendente e o valor novo de um contato em troca nunca saem para fora do tenant. O aviso externo leva a referência do próprio pedido, a data, o prazo e o link.
Por quê: é a RN-NOT-09 aplicada ao destinatário, e não só à variável. Quem está fora não tem permissão nenhuma a checar do outro lado do link — e no caminho da eliminação o aviso que protege um dado pessoal não pode vazar outro.
O aviso ao canal anterior do fornecedor sai por e-mail, e só — é audiência externa, e o endereço é a única coisa que o resolvedor devolve. Os avisos bancários internos continuam nos três canais da v1, porque quem os recebe tem conta e tela. A redundância fora de banda do BANK_ACCOUNT_CHANGE é a ligação de saída para telefone confiável, registrada como evidência da tarefa no Fornecedor — não é canal desta spec, não tem slug e não se desliga por preferência.
Por quê: SMS e WhatsApp saíram da v1 porque são o que o mesmo fraudador toma em seguida, por clonagem de número e por troca de chip. Um segundo canal fraco dá confiança sem dar controle, e a confiança é o que faz alguém parar de ligar.
Não há fila que submeta, aprove ou empenhe. Todo ato com consequência acontece dentro de uma requisição, com a identidade, a permissão e o escopo de quem o faz. Enquanto a plataforma não restaurar esses três fora de requisição, execução delegada é v2, escrita como requisito na página de Mensageria e na demanda de plataforma.
Por quê: o pior caso da fila que saiu era um worker submetendo, aprovando por política e empenhando verba com a identidade de alguém que já tinha perdido a permissão — e sem sequer restaurar o tenant. Conveniência contra alçada não é uma troca difícil.
As RN-NOT-18 a 21 e a RN-NOT-26 eram da fila de submissão diferida e saíram com ela. Os números ficam vagos de propósito: regra não se renumera, porque o número é o endereço por onde outra página a cita.
Aferição
Referência de mercado
Nenhuma das decisões acima é original, e é bom que não seja. O que o mercado mostra, quando se olha o que os sistemas grandes fazem de notificação em compras e em fluxo de trabalho:
ServiceNow, Jira e Oracle Fusion tratam a notificação como um registro de produto — evento, destinatário e template declarados —, não como uma chamada de e-mail espalhada pelo código. É a diferença entre conseguir responder "quais avisos este sistema emite?" abrindo uma tela e ter que rodar uma busca no repositório.
O notification scheme do Jira amarra evento a papel, e é excelente — até o cliente ter oito esquemas parecidos e ninguém saber qual projeto usa qual. A lição aplicada aqui: a audiência é do slug, não de um esquema configurável por tenant. Flexibilidade de destinatário é a origem mais comum de "não sei por que fulano recebeu".
Ariba e Coupa mandam lembrete de aprovação pendente e escalonam por tempo — e é justamente aí que aparece a fadiga de e-mail que faz o comprador criar uma regra de caixa de entrada. Daí vir o throttle na v1 e o digest declarado: o problema é conhecido antes de o produto ter o primeiro cliente.
GitHub, Slack e as suítes de RH convergiram no mesmo desenho: o padrão vem do produto, o usuário ajusta o que incomoda, e uma minoria mexe. É a razão de a tabela guardar exceção — e de a tela ser uma lista agrupada em vez de um assistente de configuração.
O que não se copia: a caixa de "notificações de sistema" que alguns ERPs usam como segundo canal de mensagens internas, com resposta e encaminhamento. É um cliente de e-mail dentro do ERP, e ninguém o usa depois do primeiro mês.
Limites
Fora de escopo
- Canal, entrega, retry e caixa in-app. Tudo do
GrydNotifications: e-mail, push, in-app, tentativas, agendador e provedores. - Template e renderização. Incluindo o override por tenant, que já funciona por
TenantIdnoNotificationTemplate. - Alerta de operador. Infraestrutura, fila de scan, base de assinaturas: da plataforma, e não passa por este catálogo.
- Editor de template. O texto se altera por semeadura ou por override, não por WYSIWYG dentro do produto.
- Digest.
digestEligible,entityKeye o agendador já existem; falta janela por usuário, template de resumo e o tratamento do obrigatório dentro do agrupado. Entra sem migração. - Horário de silêncio. "Nada de push entre 20h e 7h" é preferência de canal com janela, e cabe na mesma tabela — depois de existir push em uso real.
- SMS e WhatsApp. Cortados da v1 — são os canais mais atacados, por clonagem e por troca de chip, e um controle antifraude que se apoia neles apoia-se no que o fraudador toma primeiro. Canal novo é do framework; entrar aqui custaria uma linha em
allowedChannels, e é isso que o enum está preparado para receber quando a decisão mudar. - Notificação ao fornecedor pelo portal. Hoje só o controle bancário fala com quem está de fora. Os três avisos do portal estão reservados no catálogo pelo nome, com audiência, canal e conteúdo definidos pelo Portal do Fornecedor; o resolvedor do convidado entra aqui quando aquela proposta for validada.
- Locale do fornecedor estrangeiro.
SupplierType = Foreignpoderia carregar o dele; a cascata já tem onde encaixar. - Submissão em nome de outra pessoa. A fila que submetia sozinha saiu da v1. Volta quando a plataforma souber executar um comando com a identidade, a permissão e o escopo de quem pediu — requisito escrito na página de Mensageria e na demanda de plataforma.
O que esta spec deixa amarrado em outro documento. Duas coisas, e as duas são pequenas de propósito. A primeira é o contrato de audiência: quem implementa cada resolvedor é o módulo dono do conceito, e três deles — administrador do tenant, detentores de uma permissão e contato contratual — dependem de consulta que só o GrydAuth pode responder, pedida na demanda de plataforma. A segunda é o Motivo: os códigos que esta spec cita em texto de aviso são sempre o rótulo de lista fechada, nunca o comentário — e é a mesma fronteira entre motivo de sistema e motivo de pessoa que as duas páginas declaram com as mesmas palavras. Nenhuma das duas muda tabela, contrato de erro ou slug.
Registro
Histórico de revisões
| Versão | Data | O que mudou |
|---|---|---|
| v1.17 | 30/10/2026 | Permissões padronizadas em read/lookup/create/update/delete (decisão de 30/10/2026). manage:notification-preferences vira, por ação: PUT /notifications/preferences → update:notification-preferences; DELETE /notifications/preferences → delete:notification-preferences. A tabela de permissões ganha uma linha. |
| v1.16 | 21/09/2026 | Validação de 18/09, achado 25. A família da eliminação a pedido do titular volta à RN-NOT-03: o slug é a ação da trilha com o prefixo. nexio.attachment.erasure-requested avisava no registro, e a ação de mesmo nome saía na execução; passa a nexio.attachment-erasure-request.registered. Pela mesma regra, erasure-blocked, erasure-purge-blocked e erasure-cancelled passam a nexio.attachment-erasure-request.blocked, .purge-blocked e .cancelled, os nomes dos eventos que o Anexo já publicava; nexio.attachment.erased fica. Decisão do Roger: o registro publica uma de duas ações, nunca as duas — .registered quando o pedido nasce livre e .blocked quando nasce travado; o travado não avisa quem executa. Texto, audiência e canais dos cinco avisos não mudam. Contagens intactas: 64 slugs, 29 obrigatórios. |
| v1.15 | 21/09/2026 | Validação de 18/09, achado 22 (decisão do Roger: o aviso de resíduo do Orçamento fica na v1). Slug novo nexio.budget-residue.aged: CostCenterOwners sem Watcher, E I p, janela de 7 dias, um aviso por dono e por nó, com a lista dos documentos e sem valor (RN-NOT-09). A idade é o BudgetPolicy.residueAlertAfterDays. nexio.budget.overrun-recorded ampliado: cobre também o Actual sob Block ou Uncovered e o positivo isento da virada, que o Orçamento mandava notificar sem slug. Contagens recontadas: 64 slugs (quarenta acrescentados às vinte e sete originais, menos três), vinte e nove obrigatórios, nove resolvedores. |
| v1.14 | 20/09/2026 | Recuo da validação de 18/09 (achado 11). Os três slugs do portal — nexio.portal-invitation.sent, nexio.portal-access-link.reissued e nexio.portal-round.closing — deixam de apontar para SupplierFinanceContact, que é a cadeia do aviso bancário e não chega ao convidado: a audiência, o canal e o conteúdo passam a ser do Portal do Fornecedor, e os três ficam reservados pelo nome até o resolvedor do convidado existir. Sai daqui o detalhe do token no fragmento, que é regra do Portal. Contagens inalteradas: 63 slugs, nove resolvedores. |
| v1.13 | 19/09/2026 | Decisão D4 do Fluxo de Aprovação (RN-AP-71): a descrição de nexio.approval-request.decision-refused passa a cobrir também o ajuste de documento aprovado recusado pelo desfecho. Nenhum slug novo. |
| v1.12 | 13/09/2026 | Correção da validação R7. Contagem do cabeçalho (BX-01): a linha de resumo dizia 62 slugs contra os sessenta e três contados no catálogo — as três contagens vizinhas (1 agregado, 1 entidade de estado, 2 registros em código) foram reconferidas na página e batem. Ata B11: o cabeçalho e a § Trilha passam a carregar a ressalva que o resto do acervo carrega — o GrydAudit está em correção pelo time do Gryd.IO e o Nexio não o mapeia até a correção da plataforma; nenhuma tela de ato de controle entra em produção antes dela. RN-NOT-31: o e-mail exclusivo é do canal anterior do fornecedor, que é audiência externa — os oito avisos bancários internos seguem nos três canais. erasure-cancelled: o evento passa a ser nomeado na linha do slug, attachment-erasure-request.cancelled, igual ao Anexo. |
| v1.11 | 12/09/2026 | Correção da validação R6. Aviso bancário: supplier-bank-account.change-approved passa a ir também ao canal anterior do fornecedor, por e-mail, com o fim da carência e link de contestação novo, e passa a ser obrigatório — a carência de 24 a 72 h corria sem o fornecedor legítimo saber que tinha começado (Ata B8). Audiência: a derivação de audienceMustResolve exclui audiência externa, que é o que a RN-NOT-29 já mandava tratar na origem; o campo, a RN-NOT-14 e a RN-NOT-28 dizem a mesma frase. Catálogo: 62 → 63 slugs — entra supplier-control-evidence.lost, o anexo que sustentava um controle já aplicado e caiu depois dele; obrigatórios 27 → 29. Eliminação a pedido do titular: erasure-blocked ganha o blockReason com ScopeIncomplete e o administrador do tenant nesse caso; erasure-purge-blocked ganha nome de evento próprio e o caso do expurgo não gravado com prazo vencido; erasure-cancelled avisa o titular em todo cancelamento. Semeadura: as conferências que a página já declarava passam a estar listadas — seis, não quatro. Contagens recontadas na página: sessenta e três slugs, vinte e nove obrigatórios, TenantAdmin em dezessete e PermissionHolders em nove. Forma (Ata B18): as dezesseis notas de auto-história saíram do corpo normativo. |
| v1.10 | 12/09/2026 | Validação de 10/09 e as cinco rodadas de validação da onda 1. Catálogo: 27 → 62 slugs — trinta e cinco entraram com pedido registrado das trilhas (política publicada, configuração crítica do motor e do câmbio efetivada, papel de controle concedido e retirado, delegação criada por terceiro e a vencer, membro de grupo a vencer, passo parado, complementar da controladoria, resolução por reserva, desfecho recusado, instância cancelada, taxa sobrescrita, virada travada, empresa sem administrador, escopo cortado em cadeia e encerrado por remoção, seis do caminho de eliminação a pedido do titular, varredura falha e arquivo reprovado, infecção depois da liberação, nove do controle bancário e do canal de aviso, três do portal) e três saíram com a fila. Fora da v1: submissão diferida — o agregado DeferredSubmission, os campos, as três rotas, os cinco erros, os dois slugs e o aviso de liberação de varredura; §Scanning reescrita sem fila, e nenhum comando roda em nome de pessoa fora de requisição (RN-NOT-32). Fora da v1 também: Sms sai de allowedChannels — três canais, não quatro —, o aviso bancário sai só por e-mail e a redundância fora de banda é o callback, que não é canal desta spec (RN-NOT-31). Audiência: quatro resolvedores novos (5 → 9) — TenantAdmin, PermissionHolders(permission), CompanyScopeAdmins(companyId) e TenantContractContact, os três de plataforma declarados como requisito e nunca deduzidos (RN-NOT-27); a cadeia do SupplierFinanceContact passa a ser Finance → PortalAdmin → Commercial designado → contato designado do estabelecimento principal, só e-mail, sem condição de consentimento, e audiência externa vazia não cai no administrador (RN-NOT-29). Obrigatoriedade: 3 → 27 slugs, em quatro famílias (o aviso é o controle · o ato que não aconteceu · o controle não rodou · o prazo é de lei), e audienceMustResolve deixa de ser lista escrita à mão para ser regra conferida na semeadura (RN-NOT-28). Payload: aviso a destinatário externo não carrega identificador interno (RN-NOT-30). Convenção: uma grafia só — entidade e verbo em kebab-case; treze slugs em snake_case renomeados. Menores: dez sufixos de erro com _UNAVAILABLE → 503 vigente e código exato vencendo o sufixo; categorias Currency e Company (oito); read:notification-preferences na tabela de permissões; a trilha volta a uma ação; RequiredVariables do template é string anulável; o framework tem UserPreference — a lacuna é preferência por aviso e por canal, não preferência de conta. |
| v1.9 | 09/09/2026 | Três slugs novos (24 → 27): ativação que resolve vazia, instância reaberta, certidão de fornecedor a vencer. audienceParams na definição, com três papéis semeados — a audiência TenantRole não dizia qual papel, e dois avisos de governança entregavam a zero pessoas sem erro (RN-NOT-24). A guarda de payload deixa de ser lista de nomes proibidos e passa a ser classe declarada por variável, com texto de pessoa fora de qualquer aviso (RN-NOT-10, RN-NOT-25). mandatoryChannel → mandatoryChannels. Resolvedor Previous lê o contato vigente antes da alteração, qualquer que seja o tipo dela. |
| v1.1 – v1.8 | 03–09/09/2026 | Motor universal de aprovação e os slugs 20 a 24; alçada excedida, pergunta e resposta ao aprovador, resolução fora de escopo, exigência de aprovação não cumprida. Modo da fila de submissão diferida, com a volta a Waiting na recusa do dono. |
| v1.0 | 03/09/2026 | Primeira versão: as oito decisões, preferência como exceção por usuário × slug × canal, obrigatoriedade com canal e motivo, resolvedor por tipo de audiência no módulo dono do conceito, notificação como ponteiro e nunca relatório, slug igual à ação da trilha com o prefixo do produto, throttle na v1 e digest declarado. |