Especificação · Transversal

Anexo

O GrydFiles especifica armazenamento, hash, veredito, referências e retenção; sua implementação está parcial. O Nexio define de qual documento o arquivo é evidência, quem pode consultá-lo e como preservar essa prova durante revisões, propagação e revogação.

consome GrydFiles · perfil nexio.attachment desbloqueia Requisition · QuotationRequest · PurchaseOrder · Receipt · Invoice · Contract 1 agregado · 1 entidade de configuração · 1 registro em código v1.9 · 09/09/2026

Ponto de partida

Onze decisões

Fechadas antes de o documento existir. Cada uma responde a uma pergunta que, sem resposta escrita, vira campo improvisado no primeiro agregado que precisar de anexo — e depois vira seis campos improvisados diferentes.

Decisão 1 · Camadas

Duas camadas, e a regra de corte é o metadado. StoredFile é do GrydFiles e responde por bytes; Attachment é do Nexio e responde por vínculo. Arquivo com metadado próprio — número, órgão emissor, validade, status derivado da data — é entidade do domínio (ItemDocument, SupplierDocument); arquivo que é só evidência é Attachment. O precedente é o par FND_DOCUMENTS + FND_ATTACHED_DOCUMENTS da Oracle, que tem trinta anos de produção.

Decisão 2 · Dono

Amarração polimórfica: ownerType fechado mais ownerId, sem chave estrangeira. A arquitetura proíbe FK cruzando bounded contexts, e o AuditLog do framework já resolve assim. O preço é integridade referencial, e ele é pago com três mitigações obrigatórias — registro de tipos de dono, evento de domínio no descarte de rascunho e varredura diária. Nenhum agregado navega para Attachment.

Decisão 3 · Visibilidade

visibility = Internal | Supplier desde a v1. Decidir depois obrigaria a reclassificar arquivo já anexado, e o modo de falha dessa migração é vazamento — a planilha de custo interna aparecendo no portal do fornecedor. É o To Buyer / To Supplier do Oracle iProcurement, e o GrydFiles não avalia nada disso: read:file não é autorização de negócio.

Decisão 4 · Propagação

Propagar é copiar o vínculo, nunca o arquivo — e é sempre explícito. Requisição → pedido → recebimento cria um Attachment novo apontando para o mesmo storedFileId, com copiedFromAttachmentId, herdando a visibilidade e criando a sua própria FileReference. Propagação silenciosa é como anexo interno chega ao fornecedor sem ninguém ter decidido.

Decisão 5 · Revogação

Revogar em rascunho libera o arquivo; fora de rascunho, não. As duas regras existem pelo mesmo motivo: liberar a referência de um documento fechado faria o blob morrer no fim da carência e a auditoria receber FILE_PURGED_CONFLICT — guardaríamos a lápide da prova e perderíamos a prova. Fora de rascunho quem decide a morte do arquivo passa a ser a retenção, não a revogação.

Decisão 6 · Configuração

Duas camadas, e está escrito quem faz o quê. O perfil nexio.attachment é da plataforma, em appsettings: tamanho máximo, tipos permitidos, TTL das URLs. O AttachmentType é do tenant, em tabela, com IOptionalTenant — e é ele que declara o modo de retenção que o Nexio envia ao GrydFiles no upload. A exigência de anexo mora no consumidor, nunca aqui.

Decisão 7 · Escopo e trilha

companyId derivado do dono, fora de escopo é 404, e a trilha não é repetida. O recorte por empresa usa UserCompanyScope e responde igual para "não existe" e "não é seu" — regra do DeliveryLocation. O GrydFiles já registra a emissão de cada URL de download; o Nexio registra o que a plataforma não sabe: qual documento e qual anexo, porque lá só existe um fileId.

Decisão 8 · Scanning

Anexar durante a verificação é permitido; submeter não. Um aprovador não pode receber na fila um documento cujo anexo ainda pode virar Infected. E prender o usuário na tela esperando o antivírus é a solução ruim. A saída ficou declarada em aberto na v1.0 e foi fechada em 03/09/2026 pela spec de Notificação: aviso quando o arquivo liberar, mais uma fila de submissão diferida que a pessoa pede a cada tentativa e pode cancelar. Ver § Scanning.

Decisão 9 · Portal

Anexo de origem portal se amarra ao cadastro de fornecedor. supplierId é obrigatório e gravado denormalizado; portalUserId é opcional, porque o fornecedor pode responder por link de uso único. Autoria em uploadedByKind = User | PortalUser | System. E nasce Internal — é isso que sustenta o isolamento entre concorrentes numa cotação.

Decisão 10 · Espelho

Espelhar o imutável, consultar só o que muda. originalFileName, sizeBytes e contentType nunca mudam depois da confirmação, então são denormalizados. status muda, então é consultado — em lote. Listar vinte anexos não pode virar vinte chamadas ao módulo de arquivo.

Decisão 11 · Liberação

O vínculo nasce cedo; a referência de arquivo espera o veredito. Nova em 09/09/2026. POST /attachments cria o Attachment e declara ao GrydFiles a quem o arquivo vai pertencer — não cria FileReference. A liberação é que cria a referência e devolve o veredito ao Nexio pelo contrato registrado, na mesma transação. Até o veredito, o anexo aparece e mostra o estado, e não é referenciável, baixável nem propagável. É o que faz a invariante do módulo de arquivo continuar verdadeira no passo normal do único consumidor que ele tem.

Escopo

A fronteira com o GrydFiles

O GrydFiles é fonte da verdade da camada de arquivo. Nada do que está escrito lá é redecidido aqui: esta spec consome e cita. A pergunta que separa os dois lados é a mesma que o módulo usa para se definir — isso continuaria verdade num produto que não é o Nexio?

O que já está garantido lá, e por isso não aparece nos campos daqui
AssuntoQuem resolveO que o Nexio faz com isso
Bytes, bucket, chave do objetoGrydFilesNunca vê objectKey nem nome de bucket. O binário não passa pela API do Nexio em nenhum sentido
URL pré-assinada de upload e de downloadGrydFilesPede, repassa ao navegador e esquece. TTL vem do perfil
sha256, tamanho real, tipo verificadoGrydFiles, no servidorDeclara o esperado e recebe o verificado. Não recalcula nada
Antivírus, quarentena, vereditoGrydFiles · ClamAVLê status e decide o que a tela e o fluxo fazem com ele
Dedupe por conteúdoGrydFiles, por tenantIgnora. Dois anexos podem compartilhar blob sem que nada no domínio mude
Contagem de referências e expurgoGrydFilesCria e libera FileReference. Nunca apaga arquivo — não existe rota de exclusão do outro lado
Quanto tempo guardarNexio decide, GrydFiles executaDeclara retentionMode, retainUntil e legalHold no upload, resolvidos do AttachmentType
Quem pode ver este documentoNexio, antes de pedir a URLread:file é permissão de módulo, não autorização de negócio. A decisão é visibility + escopo + permissão do dono
O que o arquivo éNexioAttachment · AttachmentType · ownerType. O GrydFiles guarda só ownerScope e ownerKey em texto opaco

Como o Nexio fica sabendo do veredito. Novo em 09/09/2026 — até 08/09 nada declarava isto, e três comportamentos dependiam dele. O GrydFiles não publica evento, por decisão dele: o despacho de evento pós-commit não existe na plataforma e o DomainEventDispatcher publica dentro da transação. O mecanismo é o contrato de veredito: o Nexio registra uma implementação de IFileOutcomeHandler para o escopo nexio.attachment, e o job de scan a chama ao gravar o veredito, na mesma unidade de trabalho. É a mesma regra que vale no desfecho de aprovação — efeito na transação, evento só para notificar, depois do commit. Se o efeito do Nexio falha, o veredito não é gravado e a tentativa se repete; nada fica meio aplicado.

O que o handler faz, e é a lista inteira: em liberado, marca o anexo como utilizável e destrava quem esperava por ele; em infectado, revoga com revocationSource = FileInfected (RN-ATT-20) e dispara o aviso obrigatório; em reprovado por tipo, descarta o anexo com o rejectionCode que veio do módulo. Os três eram, até 08/09, reações a uma transição que acontece dentro de outro módulo, sem nenhum caminho escrito para chegar até aqui.

Consumo em processo, não por HTTP. O GrydFiles roda no mesmo processo e expõe IFileService; o Nexio chama a interface diretamente, como o GrydReports já faz. As rotas /files/* existem para a SPA do próprio módulo, e a SPA do Nexio não as chama — ela fala com /attachments, que é onde a autorização de negócio acontece. Expor /files/{id}/download-url para a tela de requisição seria entregar ao navegador a única rota que não sabe o que é uma requisição.

O upload, ponta a ponta, em cinco passos

1. A tela chama POST /attachments/upload-intent com ownerType, ownerId, attachmentTypeId, nome, tipo e tamanho declarados. O Nexio resolve a empresa do dono, confere permissão, confere que o tipo é aceito naquele dono, resolve a retenção e só então chama o POST /files do GrydFiles com o perfil nexio.attachment. A recusa cara acontece antes de gastar rede.

2. O navegador faz o PUT direto no storage, com a URL e os cabeçalhos devolvidos. Nenhum byte passa pela API.

3. A tela chama POST /attachments com o fileId. O Nexio cria o Attachment e confirma no GrydFiles (POST /files/{id}/complete) declarando o par pendente ownerScope = "nexio.attachment" · ownerKey = attachment.id. Nenhuma FileReference é criada aqui — revisto em 09/09/2026. O anexo existe, aparece na lista e mostra o estado real; o que ele ainda não é, é referenciável, baixável ou propagável. A resposta volta com o arquivo em Scanning.

4. O antivírus roda no GrydFiles. O anexo já existe, já aparece na lista, e a lista mostra o estado real — não um spinner que some quando o usuário troca de aba.

5. Available é a liberação: o GrydFiles cria a FileReference do par pendente e chama o contrato de veredito do Nexio na mesma transação. Só então o anexo libera download, envio ao fornecedor, propagação e submissão para aprovação. Infected e a reprovação por tipo (Purged) chegam pelo mesmo contrato, sem referência nenhuma para desfazer: o anexo é descartado — revogado com motivo próprio e sem direito a arquivo —, e a tela e o aviso dizem o que aconteceu.

O que a inversão comprou. Até 08/09/2026 este passo 3 criava a referência sobre um arquivo em Scanning, e isso tornava falsa a invariante central do módulo de arquivo — a que diz que nada é referenciável antes do veredito. Pior: as duas saídas negativas ficavam inconsistentes por construção. Reprovação por tipo apagava um objeto com referência ativa apontando para ele; Infected deixava uma referência viva apontando para blob em quarentena. Nenhuma das duas tinha regra escrita, porque nenhuma das duas deveria ser possível.

Por que o Attachment nasce antes do veredito, e a referência não

A alternativa seria criar o vínculo só quando o arquivo ficasse Available. Ela parece mais limpa e é pior em três frentes: o usuário não vê o que acabou de anexar; a falha do antivírus vira um arquivo órfão que ninguém consegue nomear na tela; e a segunda tentativa de subir o mesmo arquivo não tem como ser reconhecida como repetida. Criar cedo e deixar o estado à mostra é o que torna a espera honesta — e é o que permite a decisão 8 ser sobre submeter, não sobre anexar.

A referência é outra coisa, e ela espera. Revisto em 09/09/2026. Criar o vínculo cedo é decisão de produto; criar a FileReference cedo era violação da regra de quem guarda o arquivo. As duas foram separadas: o Attachment continua nascendo no passo 3, e a referência nasce sozinha na liberação. Para o usuário nada muda — ele vê o anexo e vê "verificando". O que muda é que o estado intermediário deixou de ser uma promessa sobre um arquivo que ainda podia ser recusado.

Modelo

Mapa de entidades

Attachment aggregate root · Nexio O vínculo. Aponta para um StoredFile e para um dono polimórfico, carrega visibilidade, autoria de negócio, o rastro da cópia e a revogação. Espelha o que é imutável no arquivo.
AttachmentType configuração · por tenant O catálogo. Rótulo, visibilidade padrão, donos aceitos, se propaga e — o que só ele decide — o modo de retenção declarado ao GrydFiles. IOptionalTenant: nulo é tipo global do produto.
AttachmentOwnerRegistry registro em código · não é tabela Um descritor por ownerType, implementado pelo agregado dono. Resolve empresa, estado, limites e o número legível do documento. Tipo sem descritor derruba a inicialização. Ver § O dono polimórfico.
StoredFile GrydFiles · plataforma Bytes, sha256, status, retenção e lápide. Nada aqui é remodelado, e o Nexio nunca escreve neste registro a não ser pelas rotas de retenção do próprio módulo.
FileReference GrydFiles · uma por anexo (storedFileId, "nexio.attachment", attachmentId). É o que impede o expurgo enquanto alguém precisa do arquivo — e o que cada cópia cria para si.
ItemDocument · SupplierDocument entidades do domínio · não são anexo Têm número, órgão, vigência e status derivado da data. Continuam existindo, e passam a guardar storedFileId em vez de URL — a dívida que sai junto com esta spec.
ReasonCode transversal · especificado O motivo da revogação, no uso AttachmentRevoke. Especificado em 03/09/2026: o anexo grava reasonCodeId mais o código e o rótulo congelados. O motivo de sistema continua viajando em revocationSource — são eixos diferentes. Ver Motivo.

Convenção de nomes. O documento é escrito em português e todo identificador é em inglês — entidade, atributo, valor de enum, rota, permissão e código de erro. A exceção são os dados de negócio brasileiros, que ficam como são no mundo real.

Duas tabelas: core.attachments e core.attachment_types. Índices: um em (tenantId, ownerType, ownerId) filtrando isDeleted = false, que é a consulta que a tela faz o tempo todo; um em (tenantId, storedFileId), para responder "quem mais aponta para este arquivo" sem consultar o outro módulo; um em (tenantId, supplierId) onde supplierId IS NOT NULL, que é o isolamento da cotação e a promoção a SupplierDocument; e, no tipo, unicidade em duas camadas — code único entre os globais (tenantId nulo) e (tenantId, code) único dentro de cada tenant: dois índices parciais, ou um único que trate nulos como iguais; depende do banco, e a verificação vale na aplicação. A mesma chave nas duas camadas não colide: é a sobreposição do tenant sobre o tipo global, ver § AttachmentType.

Domínio

Campos · Attachment

Herda de TenantScopedAggregateRoot. Id, CreatedAt, CreatedBy, UpdatedAt, UpdatedBy e o bloco de soft delete vêm do BaseEntity e não estão repetidos aqui.

Legenda: obrig sempre · cond conforme a operação · opc opcional · gerado calculado pelo sistema ou pelo banco.

Vínculo

CampoTipoRegra
idGuidgeradoAtribuído pelo agregado no construtor. É o ownerKey da FileReference do outro lado, então precisa existir antes de a referência ser criada.
tenantIdGuidgeradoDo token, nunca aceito do cliente.
storedFileIdGuidobrigSem chave estrangeira — é outro bounded context e outro DbContext. Imutável: trocar o arquivo de um anexo é criar outro anexo, porque o primeiro já pode ter sido lido, propagado e aprovado.
attachmentTypeIdGuidobrig→ AttachmentType. Decide visibilidade padrão, retenção e se propaga. Editável enquanto o dono é rascunho, e só entre tipos com a mesma retenção — mudar de tipo depois mudaria a data de expurgo de um arquivo já sob guarda.
ownerTypeenumobrigEnum fechado, ver § O dono polimórfico. Imutável. Valor sem descritor registrado não compila conceitualmente: a inicialização falha.
ownerIdGuidobrigA chave do dono no mundo dele. Sem FK, imutável. Reamarrar anexo a outro documento é copiar, ver § Propagação.
lastKnownOwnerStateenumgeradoNovo em 09/09/2026. Draft · InProgress · Closed — o último estado do dono que o Nexio viu. Gravado pelo próprio dono, na transição, no mesmo método que muda o estado dele; nunca por leitura preguiçosa e nunca pelo módulo de anexo. Existe porque a varredura de órfãos precisa decidir se libera a referência de um documento que não existe mais — e a essa altura não há a quem perguntar. Sem a coluna, a terceira mitigação do polimorfismo não era executável: ou a varredura nunca libera (e o blob fica preso e pago para sempre), ou libera sempre (e mata prova de documento fechado).
companyIdGuid?geradoDerivado do dono pelo descritor e denormalizado. Existe para o escopo por empresa caber no filtro de linha sem join entre contextos — mesmo padrão do DeliveryLocation. Anulável desde 09/09/2026: nulo é a resposta do CompanyOf dos donos que são do tenant e não de uma empresa — item, fornecedor, proposta de alteração bancária e a instância de aprovação desses dois. Nulo significa anexo do tenant, e o filtro o deixa passar; não significa empresa desconhecida.

Espelho do arquivo · o que não muda depois da confirmação

CampoTipoRegra
originalFileNamestring(255)geradoCopiado do StoredFile na confirmação. É o que a lista mostra e o nome sugerido no download. Nunca compõe chave de nada, nem aqui nem lá.
contentTypestring(120)geradoO tipo verificado pelo GrydFiles no scan, não o declarado pelo cliente. É o que decide o ícone e se a tela oferece pré-visualização do navegador.
sizeBytesbigintgeradoO tamanho real. Espelhado para a lista somar o peso do documento sem consultar o outro módulo — e é a soma que a regra de limite agregado usa.
status—geradoNão é coluna. Muda com o tempo e por isso é consultado, sempre em lote, por GET /attachments/status. Espelhar estado que muda é como se cria a divergência que ninguém consegue explicar seis meses depois. Ver § Endpoints.

Classificação e negócio

CampoTipoRegra
visibilityenumobrigInternal · Supplier. Nasce do defaultVisibility do tipo. Supplier exige que o tipo permita, que o descritor do dono permita e que o usuário tenha share-with-supplier:attachments. Ver § Visibilidade.
descriptionstring(300)?opcPor que este arquivo está aqui. É o campo que transforma doc3_final_v2.pdf em informação. Editável enquanto o anexo não está revogado.
copiedFromAttachmentIdGuid?geradoO anexo de origem quando este nasceu de propagação. Nulo quando é original. Aponta para o anexo, não para o arquivo — o arquivo os dois já compartilham.
rootAttachmentIdGuid?geradoA origem da cadeia inteira, resolvida na criação. Existe para "onde este documento entrou no sistema" ser uma consulta, e não uma recursão de cinco níveis na tela de auditoria.

Autoria de negócio · quem, do ponto de vista de compras

CampoTipoRegra
uploadedByKindenumobrigUser · PortalUser · System. O uploadedBy do StoredFile continua sendo a identidade do tenant no GrydAuth — o portal não tem GrydAuth. A autoria de negócio é do anexo.
uploadedByUserIdGuid?condObrigatório com User. Nulo nos outros dois.
portalUserIdGuid?condicionalObrigatório em uploadedByKind=PortalUser, derivado da sessão. Nulo para autor interno. Registra identidade convidada, sem presumir prova da pessoa física.
supplierIdGuid?condObrigatório quando uploadedByKind = PortalUser, resolvido do token da RFQ e gravado denormalizado. Opcional quando o comprador anexa algo que recebeu do fornecedor por fora. Congelado: contato muda, é desligado ou apagado, e a autoria fica.
uploadedAtDateTimegeradoQuando o vínculo foi confirmado, não quando a URL foi emitida. É a data que a retenção de RetainUntil usa como base.

Revogação

CampoTipoRegra
revokedAtDateTime?geradoPreenchido é revogado, e revogado é terminal — não existe "desrevogar". O caminho de volta é anexar de novo, o que cria outro registro e outra referência.
revokedByGuid?geradoQuem revogou. Nulo quando revocationSource não é User.
reasonCodeIdGuid?cond→ ReasonCode, uso AttachmentRevoke. Obrigatório quando a revogação é de usuário e o dono não é rascunho — remover prova de documento em curso precisa de motivo nomeado. Anulável porque revogação de rascunho e revogação de sistema não pedem motivo. Grava também reasonCode, reasonLabel e reasonComment?, ver Motivo.
revocationSourceenum?geradoUser · OwnerDiscarded · OrphanSweep · FileInfected. Separa o que uma pessoa decidiu do que o sistema concluiu — e é o que a tela usa para escrever a frase certa.
fileReferenceReleasedboolgeradoSe a FileReference foi liberada no GrydFiles. Verdadeiro só quando a revogação aconteceu com o dono em rascunho. É a coluna que responde, sem consultar o outro módulo, se este anexo ainda segura o blob. Ver § Revogação.

O que não está aqui, de propósito. Não existe fileUrl — URL é emitida na hora e vence. Não existe origin: uploadedByKind mais supplierId já respondem de onde veio, e um terceiro campo dizendo a mesma coisa é o começo de duas verdades. Não existe isRequired nem requiredAt: a exigência de anexo é regra do consumidor, e escrevê-la aqui criaria um motor de regra paralelo ao de aprovação. Não existe version: versionamento de documento é do Contract. E não existe expiresAt: arquivo com validade é ItemDocument ou SupplierDocument, pela regra de corte da decisão 1.

Configuração

Campos · AttachmentType

Herda de BaseEntity e implementa IOptionalTenant: tenantId nulo é tipo global do produto, o mesmo padrão que o NotificationTemplate do framework já pratica. O tenant não edita o tipo global — ele o sobrepõe: uma linha própria com o mesmo code vale no lugar da global só naquele tenant, e é assim que ele renomeia, muda a retenção dos anexos novos ou desativa o que o produto entregou. Para cada code, vale a linha do tenant quando existe; senão, a global. A linha global nunca muda para ninguém, e o release que a altere não alcança quem a sobrepôs. Anexo já gravado continua apontando para a linha que usou; a sobreposição vale para o seletor e para os anexos novos. Mesma mecânica do NotificationTemplate e do Motivo, fechada em 04/09/2026 pelo achado A6 da contra-análise.

Não existe allowedContentTypes nem maxSizeBytes no tipo. Tipo de arquivo e tamanho são do perfil nexio.attachment, em appsettings, porque quem valida é o GrydFiles e ele valida por perfil. Repetir aqui seria criar um segundo teto que diverge do primeiro no dia em que alguém mexer em um só.
CampoTipoRegra
tenantIdGuid?geradoNulo é tipo do produto, disponível para todos e mantido pela Gryd. Preenchido é do cliente — e, com o mesmo code de um global, é a sobreposição dele naquele tenant. Unicidade em duas camadas, ver a nota de índices em § Mapa.
codestring(40)obrigÚnico entre os globais e único dentro de cada tenant; igual ao de um global é sobreposição, não colisão. Imutável — na sobreposição, desde a criação —, normalizado em maiúsculas. É o que a integração e a semente usam; o rótulo é para gente.
namestring(120)obrigComo o tipo aparece no seletor de anexo.
descriptionstring(300)?opcExplica quando usar. É o texto que evita todo mundo escolher "Geral".
allowedOwnerTypesenum[]obrigOnde este tipo pode ser usado. Vazio é recusado — tipo que serve para tudo não classifica nada. A validação cruza com o que o descritor do dono aceita, e vale a interseção.
defaultVisibilityenumobrigInternal · Supplier. O padrão da tela. Anexo do portal ignora este campo e nasce Internal sempre, ver § Portal.
allowsSupplierVisibilityboolobrigSe o tipo pode ir ao fornecedor. Falso trava a promoção mesmo para quem tem a permissão — é como se escreve "planilha de custo interno nunca sai" uma vez, em vez de confiar em quem clica.
propagatesboolobrigSe o tipo entra na sugestão de propagação. Sugestão, não automação — a cópia continua sendo ato explícito. Ver § Propagação.
retentionModeenumobrigUntilReleased · RetainUntil · Permanent. É o campo que só existe aqui. O GrydFiles executa o que recebe no upload, e quem preenche é este tipo. Ver § Retenção.
retentionYearsint?CondicionalObrigatório com RetainUntil. Define o piso inicial da política, não um prazo legal universal. Registrar policyVersion, fundamento, marco de contagem e retainUntil resolvido; fechamento, obrigação contratual ou legalHold podem estender a guarda. Tipos fiscais exigem política validada pelo responsável fiscal da empresa.
legalHoldboolobrigLiga o legalHold do StoredFile, que trava até o expurgo manual e o pedido de apagamento por titular. Ligar é decisão de tipo, e desligar depois exige permissão de plataforma, com trilha.
maxCountPerOwnerint?opcTeto por documento para este tipo. Nulo herda o teto do descritor do dono. Existe porque "20 notas de entrega" e "1 contrato assinado" não são o mesmo problema.
isActive · sortOrderbool · intobrigInativo some do seletor e não invalida anexo existente. Ordem é a do seletor, porque a lista alfabética coloca "Ata" antes de "Nota fiscal" em todo cliente.

Semente global · tenantId nulo

Oito tipos, e nenhum deles é "Outros". A semente é global; o cliente que precisar de "Laudo de Vibração" cria o seu, e não migra nada.
CódigoRetençãoVisibilidadePor que existe
GENERALUntilReleasedInternalO anexo solto. Morre quando o documento morre — é o caso mais comum e o mais barato
TECHNICAL_SPECUntilReleasedSupplierDesenho, memorial, especificação. Nasce visível ao fornecedor porque é o que a cotação precisa mandar
SUPPLIER_PROPOSALRetainUntil · 5 anosInternalA proposta que o fornecedor enviou. É prova do que foi combinado e sustenta contestação de preço
QUOTE_COMPARISONRetainUntil · 5 anosInternalO mapa comparativo. allowsSupplierVisibility = false — é o arquivo que nunca pode sair
DELIVERY_PROOFRetainUntil · 5 anosInternalCanhoto, foto da entrega, romaneio. Prova de recebimento em divergência
FISCAL_DOCUMENTRetainUntil · 5 anos + legalHoldInternalDANFE, boleto, comprovante. Cinco anos é o prazo de decadência do CTN, e o legal hold é o que impede o expurgo de acidente
SIGNED_CONTRACTPermanentSupplierSó expurgo manual, com motivo e permissão própria. Contrato assinado não tem data de morte automática
APPROVAL_EVIDENCERetainUntil · 5 anosInternalO que sustentou uma aprovação de exceção. É o primeiro arquivo que a auditoria interna pede

A escolha estrutural

O dono polimórfico e o registro dos tipos de dono

A alternativa que foi recusada, e por quê

A opção com integridade referencial seria uma tabela de ligação por dono — requisition_attachments, purchase_order_attachments, e assim por diante. Ela dá FK de verdade e recusa a decisão de arquitetura do produto: "cada bounded context tem suas tabelas, sem chaves estrangeiras cruzando contextos". Também multiplica por seis o número de tabelas, de repositórios e de telas, e faz "liste todos os anexos deste fornecedor" virar uma união de seis consultas que ninguém lembra de atualizar quando a sétima aparece.

O precedente interno decide: o módulo mais transversal do framework, o AuditLog, amarra em EntityType + EntityId sem FK. E o GrydFiles faz o mesmo em FileReference. Fazer diferente aqui seria o Nexio contrariar duas camadas abaixo dele.

O preço, dito com todas as letras

Sem FK, o banco não impede um anexo apontar para um documento que não existe mais. É um preço real, e ele é pago com três mitigações obrigatórias — nenhuma delas opcional, nenhuma delas "a fazer depois". Sem as três, esta decisão não se sustenta e o desenho vira dívida.

Mitigação 1 · o registro dos tipos de dono

Cada ownerType tem um descritor de onze perguntas. As respostas são declaradas aqui, nesta página — é o que faz dela um registro e não uma lista; a implementação vive no módulo dono, que é quem sabe consultar o próprio agregado. As duas coisas são separadas de propósito: onze respostas escritas por seis mãos em semanas diferentes é como um registro fica incoerente, e a resposta declarada é o contrato que a implementação tem de cumprir.

O guard confere a implementação, não a declaração: todo valor do enum tem de ter um descritor registrado na inicialização, e o que falta derruba a aplicação no startup — não em produção às três da manhã. É a diferença entre um erro de programação e um incidente, e é verificável: percorre-se o enum e pergunta-se ao contêiner. Revisto em 09/09/2026: a formulação anterior falava em "o módulo dono publicou o seu descritor", que descreve uma pessoa cumprindo um combinado e não tem como ser conferida por código.

As onze perguntas. As respostas de cada tipo estão declaradas abaixo; a implementação vive no módulo dono. O módulo de anexo não conhece requisição, pedido nem contrato — ele conhece a interface.
O que o descritor respondePara quê
CompanyOf(ownerId)Resolve o companyId denormalizado. É o que faz o escopo por empresa funcionar sem join entre contextos
StateOf(ownerId)Draft · InProgress · Closed · Missing. Decide se pode anexar, se pode revogar, e — a pergunta central — se a revogação libera a referência
DisplayNumberOf(ownerId)Traduz o Guid em "REQ-2026-00412". É o que torna a trilha legível e a tela de auditoria útil
AllowedTypesOs AttachmentType aceitos naquele dono. Cruza com allowedOwnerTypes do tipo, e vale a interseção
AllowsSupplierVisibilitySe aquele dono pode ter anexo visível ao fornecedor. Requisição interna, por exemplo, não pode
PropagatesToPara quais ownerType a propagação é oferecida. Requisição → pedido; pedido → recebimento; cotação → pedido
MaxCount · MaxTotalBytesTeto por documento, em quantidade e em peso somado. O peso vem do sizeBytes espelhado, sem consultar o GrydFiles
CanWrite(user, ownerId)Delega a autorização de escrita ao dono. Quem pode editar a requisição pode anexar nela — o anexo não inventa uma segunda regra de quem manda no documento
SubmitPolicyOf(user, ownerId)A nona pergunta, acrescentada em v1.1. Se aquele usuário pode submeter o documento agora, um carimbo de versão do documento, e como submetê-lo. Existe porque a fila de submissão diferida da spec de Notificação precisa submeter sem conhecer requisição, pedido nem contrato — e porque a permissão tem que ser reavaliada na execução, não no pedido
OpenApprovalOf(ownerId)A décima, acrescentada em 09/09/2026. O approvalRequestId da instância de aprovação aberta daquele documento, ou nulo. Existe porque o aviso de anexo revogado vai para os aprovadores pendentes daquela instância — e nenhuma das nove perguntas anteriores sabia dizer qual instância é. Sem ela, o aviso obrigatório de uma revogação no meio da aprovação não tinha destinatário resolvível e falhava em silêncio
ResponsibleOf(ownerId)A décima-primeira, acrescentada em 09/09/2026. O usuário interno que responde pelo documento — solicitante da requisição, comprador da cotação e do pedido, conferente do recebimento. Nunca vazio. Existe para o caminho do portal: quando o arquivo veio de fora, uploadedByUserId é nulo, e "avisar quem anexou" não avisa ninguém. A proposta infectada de um fornecedor precisa chegar a alguém dentro da empresa, e é esta pergunta que diz a quem

Mitigação 2 · evento de domínio no descarte

Todo agregado que aceita anexo publica OwnerDiscarded(ownerType, ownerId) quando um rascunho é descartado. O handler revoga os anexos daquele dono com revocationSource = OwnerDiscarded e libera as FileReference, porque descarte de rascunho é exatamente o caso em que o arquivo não precisa sobreviver. É o caminho normal; a varredura é a rede de segurança.

Mitigação 3 · varredura diária

Um job diário (attachment-orphan-sweep, sobre o GrydJobs) percorre os anexos ativos em lotes por ownerType e pergunta ao descritor se o dono existe. Ausente, o anexo é revogado com revocationSource = OrphanSweep; a referência é liberada apenas se lastKnownOwnerState = Draft — a coluna que o próprio dono grava na transição, porque a essa altura não há mais a quem perguntar. Revisto em 09/09/2026: até 08/09 a guarda citava um dado que não existia em lugar nenhum, e a varredura não tinha como decidir sem ou liberar sempre, ou nunca. E o job reporta: contagem por tipo de dono no log e na tela de administração. Órfão não é ocorrência normal — é sintoma de um consumidor que esqueceu de publicar o evento, e o número precisa aparecer para alguém consertar a causa.

Os valores de ownerType na v1

Quinze valores, enum fechado. Acrescentar um é escrever o descritor — e é por isso que o enum é fechado: o custo de entrar na lista é exatamente o custo de responder às onze perguntas do registro.
ValorPropaga paraObservação
RequisitionPurchaseOrder · QuotationRequestO caso de entrada. Orçamento prévio, foto do que quebrou, especificação do solicitante
RequisitionItemQuotationRequestItem · PurchaseOrderItemAnexo de linha existe porque a requisição de dez itens tem um desenho por item, não um por documento. A propagação passa pela cotação desde 09/09/2026 — antes ela pulava direto para o pedido, e o desenho não chegava a quem precisava cotá-lo
QuotationRequestPurchaseOrderEdital, planilha modelo, condições. Tipicamente Supplier
QuotationResponsePurchaseOrderOnde o anexo do portal cai. supplierId obrigatório, nasce Internal
PurchaseOrderReceipt · InvoicePedido assinado, aceite do fornecedor, alteração negociada
PurchaseOrderItem—Herda o desenho da linha de requisição
ReceiptInvoiceCanhoto, foto da carga, laudo de conferência. É o tipo com mais anexo por documento no mundo real
Invoice—DANFE, boleto, divergência. Retenção fiscal com legal hold
ContractPurchaseOrderO contrato assinado é Permanent. Versionamento é do Contract, não daqui
QuotationRequestItemPurchaseOrderItemNovo em 09/09/2026. A linha da cotação, que é onde o desenho por item de fato serve — o fornecedor cota aquele item
ApprovalRequest—Novo em 09/09/2026. A evidência que o aprovador junta à decisão: parecer, cotação de terceiro, autorização por fora. O documento em aprovação não aceita mais alteração — a instância aceita, e é ela o dono. É onde o tipo semeado APPROVAL_EVIDENCE passa a ter uso; até 08/09/2026 ele não tinha onde ser usado
Supplier—Novo em 09/09/2026. Evidência avulsa do cadastro: ata de visita técnica, correspondência, e-mail de negociação, parecer que não tem número nem órgão emissor. Certidão, contrato social e cartão de CNPJ não entram aqui — são SupplierDocument, ver a regra abaixo
Item—Novo em 09/09/2026. Evidência avulsa do cadastro: laudo, catálogo do fabricante, e-mail de especificação. Foto e ficha técnica do item continuam sendo entidade do catálogo, com perfil próprio no GrydFiles — a fronteira da decisão 1 não muda
Budget—Novo em 09/09/2026. Planilha de premissas, ata que aprovou a verba, ofício de suplementação. A revisão orçamentária é decisão cara e sem prova nenhuma até aqui
BankAccountChangeProposal—Novo em 09/09/2026. Comprovante de titularidade da conta. É o anexo do controle antifraude bancária — quem aprova a troca de conta precisa olhar alguma coisa, e essa coisa precisa ter existido antes da aprovação

Os seis últimos entraram em 09/09/2026, e entraram agora de propósito. Mexer em enum fechado depois de haver dado gravado é migração, e as seis instâncias que faltavam — aprovação, fornecedor, item, orçamento, alteração bancária e linha de cotação — são justamente as que mais precisam de prova. Quatro delas passam por aprovação e nenhuma aceitava um único arquivo. As onze respostas dos seis estão declaradas logo abaixo, escritas de uma vez e em um lugar só — acrescentar o valor sem as respostas apenas trocaria o buraco de "APPROVAL_EVIDENCE não tem onde ser usado" por "seis valores que não sobem".

Renomeados em 06/09/2026: RequisitionLine → RequisitionItem e PurchaseOrderLine → PurchaseOrderItem, pela Decisão 5 do Cadastro de Item — linha de documento é <Documento>Item. Os descritores e as respostas não mudam.

As onze respostas dos seis donos que entraram em 09/09/2026

Declaradas aqui, de uma vez. Cada módulo dono implementa a sua coluna; nenhum deles reescreve a resposta. Onde a resposta é decisão e não preenchimento, ela está escrita como regra logo abaixo da tabela.

Seis donos, onze respostas cada. As nove primeiras perguntas já existiam; OpenApprovalOf e ResponsibleOf entraram em 09/09/2026 pelo mesmo achado.
PerguntaQuotationRequestItemApprovalRequestSupplierItemBudgetBankAccountChangeProposal
CompanyOfA empresa da cotação paiCondicional: a empresa do documento alvo quando ele tem uma (requisição, pedido, orçamento); nula quando o alvo é fornecedor ou itemNula — cadastro do tenantNula — vocabulário do tenantA empresa do orçamentoNula — a proposta é do fornecedor, e fornecedor é do tenant
StateOfSegue a cotação pai: Draft enquanto a RFQ é montada, InProgress depois de enviada, Closed na adjudicação ou no cancelamentoInProgress com a instância aberta, Closed quando conclui. Nunca DraftInProgress enquanto ativo, Closed quando inativado. Nunca DraftDraft no item que nasceu em rascunho pela importação do catálogo, InProgress ativo, Closed inativadoDraft antes de publicado, InProgress no exercício corrente, Closed com o exercício arquivado — nunca MissingDraft enquanto é montada, InProgress pendente de aprovação, Closed aprovada ou recusada
DisplayNumberOfO número da cotação mais a posição da linha — COT-2026-00118 · item 4O número da instância mais o documento alvo — APR-2026-00931 · REQ-2026-00412Razão social mais CNPJ formatadoCódigo do item mais a descrição curtaCódigo do orçamento mais o exercícioO fornecedor mais os quatro últimos da conta proposta
AllowedTypesTECHNICAL_SPEC · GENERALAPPROVAL_EVIDENCE · GENERALGENERAL na sementeTECHNICAL_SPEC · GENERALAPPROVAL_EVIDENCE · GENERALAPPROVAL_EVIDENCE · GENERAL
AllowsSupplierVisibilitySim — é a linha que o fornecedor precisa ver para cotarNão — evidência de decisão é internaNão na v1NãoNão — orçamento não sai da empresaNão — é a prova que o antifraude olha
PropagatesToPurchaseOrderItem—————
MaxCount · MaxTotalBytes5 · 25 MB10 · 50 MB20 · 100 MB10 · 50 MB10 · 50 MB3 · 15 MB
CanWriteQuem pode editar a cotaçãoO aprovador pendente da rodada corrente, e mais ninguém — não é "quem enxerga a instância"Quem pode editar o cadastro do fornecedorQuem pode editar o itemQuem pode editar o orçamento daquele exercícioQuem pode editar a proposta enquanto ela está aberta; depois de submetida, ninguém
SubmitPolicyOfDelega à cotação pai — a linha não submete sozinhaNão submete. Ver a regra abaixoSubmete o cadastro para o processo de homologaçãoSubmete o item para aprovaçãoSubmete a revisão orçamentáriaSubmete a proposta de alteração bancária
OpenApprovalOfDelega à cotação paiReflexivo — é ele mesmo. Ver a regra abaixoA instância aberta do processo de fornecedor, ou nuloA instância aberta do processo de item, ou nuloA instância aberta da revisão, ou nuloA instância aberta da alteração bancária, ou nulo
ResponsibleOfO comprador da cotaçãoQuem abriu a instância, nunca os aprovadores — eles são audiência do aviso, não donos do documentoO comprador responsável pelo cadastroO responsável pelo catálogoO responsável pelo orçamento daquele exercícioO comprador que registrou a proposta; vinda do portal, o responsável pelo cadastro do fornecedor

Sobre o AllowedTypes: o tipo carrega a retenção, então escolher o tipo é escolher quanto tempo a prova dura. GENERAL é UntilReleased — o arquivo morre quando o documento morre —, e APPROVAL_EVIDENCE é RetainUntil de cinco anos. Por isso Budget e BankAccountChangeProposal aceitam os dois: o comprovante de titularidade que sustentou uma troca de conta e a ata que sustentou uma suplementação de verba são exatamente o primeiro arquivo que a auditoria pede, e deixá-los em GENERAL os faria morrer junto com o documento que eles justificam. É a mesma escolha que já valia para ApprovalRequest.

E onde a semente não chega, ela não deve chegar. Os oito tipos globais não têm um "documento do fornecedor" nem um "laudo do item", e isso é a decisão 1 funcionando: arquivo com metadado próprio — número, órgão, vigência — não é anexo. O que sobra para o tenant acrescentar por sobreposição (RN-ATT-25) é a etiqueta local, e ela varia por segmento e por cliente; um tipo global que serve metade dos tenants é pior do que não existir.

Duas evidências, dois donos: a que o aprovador junta e a que sustenta a proposta

Novo em 09/09/2026. As duas usam APPROVAL_EVIDENCE e não são a mesma coisa, e confundi-las é o jeito mais fácil de perder prova.

A que sustenta a proposta nasce antes da decisão, e o dono é o próprio agregado. O comprovante de titularidade da conta é parte da proposta de alteração bancária; a ata que aprovou a verba é parte da revisão orçamentária. Existiriam mesmo que ninguém precisasse aprovar nada — são o porquê que o autor apresenta. Ficam em ownerType = BankAccountChangeProposal e Budget, e entram na conta da decisão 8: enquanto o anexo não tiver veredito, o documento não é submetido, porque o aprovador não pode receber uma proposta cuja prova ele não consegue abrir.

A que o aprovador junta nasce depois da submissão, e o dono é a instância. Parecer, cotação de terceiro, autorização por fora: só existem porque alguém teve de decidir. Não cabem no documento — ele já não aceita alteração —, e é para isso que o ownerType = ApprovalRequest existe. Nenhuma regra bloqueia a decisão por causa delas; o que vale é a RN-ATT-26, e enquanto o arquivo não liberar o aprovador simplesmente não o abre.

O teste, quando a fronteira ficar dúbia: este arquivo existiria se não houvesse instância de aprovação? Se sim, o dono é o agregado. Se ele só nasceu porque alguém precisou decidir, o dono é a instância. As duas convivem na mesma aprovação, e é comum que convivam — a proposta traz o comprovante, o aprovador junta o parecer, e a auditoria de daqui a três anos precisa dos dois, com os mesmos cinco anos de guarda.

Certidão de fornecedor não é anexo — ela vive no SupplierDocument

Novo em 09/09/2026, e é correção: até a tabela acima ser escrita, a linha do Supplier dizia "certidão, contrato social, cartão de CNPJ", o que contraria a decisão 1 desta própria página e o que a spec de Fornecedor já dizia. Certidão tem número, órgão emissor, emissão e vencimento, e o status dela é derivado da data — alguém digita esses campos, alguém filtra por eles, e o sistema avisa antes de vencer. Isso é entidade de domínio, com ownerScope = "nexio.supplier-document", FileReference própria e liberação quando o documento é substituído.

O que sobra para o ownerType = Supplier é a evidência sem campo para digitar: ata de visita técnica, correspondência, e-mail de negociação, parecer solto. O mesmo corte vale para o Item — laudo com número e órgão é ItemDocument, e foto e ficha técnica são entidade do catálogo, com perfil próprio no GrydFiles.

O teste é o mesmo de sempre: alguém vai ter de digitar um campo sobre este arquivo e depois filtrar por ele? Se sim, é entidade e não anexo. E a passagem entre os dois lados já está escrita: a certidão que chega anexada numa cotação pode ser promovida a documento de cadastro, porque o supplierId do anexo do portal diz de quem ela é.

companyId nulo é resposta, não falta de resposta

Novo em 09/09/2026. Três dos seis donos novos não têm empresa: Item e Supplier são vocabulário e cadastro do tenant, não da empresa, e BankAccountChangeProposal pende de um fornecedor, que também é do tenant. O companyId denormalizado do anexo nasce nulo nesses casos, e nulo tem significado declarado: anexo do tenant, visível em qualquer empresa que o usuário enxergue — não anexo de empresa nenhuma.

É a mesma forma que GLAccountCompany e BudgetPolicy já usam (linha sem empresa vale para todas) e a mesma lição do escopo por empresa: ele restringe escrita, nunca apaga leitura. Sem esta linha escrita, o filtro por UserCompanyScope decidiria sozinho, e das duas maneiras possíveis: comparação estrita faz a ata de visita ao fornecedor sumir para todo mundo; comparação frouxa a faz aparecer para todo mundo, empresa alheia inclusive. As duas falham em silêncio, que é o pior modo de falhar de um filtro de linha.

E a resposta do ApprovalRequest é condicional, o que é o caso mais fácil de errar. A instância herda a empresa do documento que ela aprova: requisição, pedido e orçamento têm empresa; fornecedor e item não têm. O mesmo ownerType devolve Guid em três processos e nulo em dois, e isso é correto — o descritor pergunta ao alvo, não a uma tabela de de-para.

Instância de aprovação não é documento submetível

Novo em 09/09/2026. O SubmitPolicyOf do ApprovalRequest responde "não submete", e responde isso explicitamente — não é um método sem implementação, nem um que devolve nulo e deixa quem chama adivinhar. A instância é a submissão de outro documento; submetê-la seria submeter a submissão. A fila de submissão diferida chama esta pergunta sem saber o que tem do outro lado, e uma resposta ausente é exatamente o tipo de coisa que ela invocaria por engano no dia em que alguém anexasse um parecer a uma instância cujo anexo ainda está em Scanning.

O OpenApprovalOf da instância é a própria instância

Novo em 09/09/2026. Para ownerType = ApprovalRequest, o ownerId já é o approvalRequestId — a resposta é reflexiva e não custa consulta nenhuma: devolve o próprio ownerId se a instância estiver aberta, e nulo se estiver concluída. Está escrito porque a alternativa é alguém implementar "procure a instância aberta deste dono" com o dono sendo uma instância, e descobrir a recursão em produção, no dia em que um anexo de parecer for revogado no meio de uma rodada.

Nenhum agregado navega para Attachment. Não existe requisition.Attachments no modelo EF, e a consulta é sempre ownerType + ownerId pelo serviço de anexo. É o que impede o polimorfismo de virar uma dependência circular entre módulos — e é o que faz o dia em que o anexo mudar de forma não tocar em seis agregados.

Contrato vigente · revisão consolidada

Visibilidade e isolamento do fornecedor

Internal/Supplier indica publicação pelo comprador; supplierId indica o dono fornecedor da resposta. São dimensões distintas. Sempre exigir vínculo válido com a cotação/rodada, tenant correto, estado permitido e arquivo Available.

Pessoa / anexoInternalSupplierRevogado
Usuário interno autorizado no dono e na empresaSimSimHistórico, com permissão.
Usuário fora do escopo404404404
Fornecedor: próprio supplierId e própria respostaSim, inclusive para conferir o upload antes do envioSimNão
Fornecedor: outro supplierIdNãoNão, mesmo que outra resposta tenha visibility=SupplierNão
Fornecedor: anexo comum do QuotationRequestNãoSim, se publicado para sua rodada/audiênciaNão

Predicado do portal: vínculo válido AND (anexo da própria resposta/fornecedor OR anexo comum publicado da cotação). Não é um OR que dispense validar alvo/rodada. Anexo de proposta concorrente nunca vira anexo comum só por trocar visibility; eventual publicação exige cópia autorizada, revisão do conteúdo e novo dono comum.

Promover visibilidade exige tipo, dono, permissão share-with-supplier:attachments e estado editável. Depois de enviado, visibilidade trava; revogar registra exposição anterior. Download passa pelo descritor do negócio antes de GrydFiles e fixa a versão de bytes aprovada.

Ciclo do documento

Propagação entre documentos

A requisição vira pedido, o pedido vira recebimento, e o desenho técnico que o solicitante anexou precisa chegar até o fornecedor e até quem confere a carga. A pergunta é como.

O que a propagação faz

Cria um Attachment novo, com ownerType e ownerId do documento de destino, apontando para o mesmo storedFileId, com copiedFromAttachmentId preenchido, rootAttachmentId herdado, a mesma visibility, e uma FileReference própria no GrydFiles.

O que ela não faz

Não copia bytes — dedupe nem entra em cena, porque é o mesmo objeto. Não acontece sozinha. Não atravessa empresa. E não sobrevive à origem de forma dependente: revogar o anexo de origem não revoga as cópias, porque cada documento responde pelo que anexou.

Por que cada cópia cria a sua própria referência

Seria mais econômico manter uma referência só. Seria também errado: a FileReference é o que responde quem ainda precisa deste arquivo, e é o que segura o expurgo. Com uma referência só, descartar a requisição de origem liberaria o arquivo enquanto o pedido ainda o exibe — e o pedido descobriria isso trinta dias depois, recebendo FILE_PURGED_CONFLICT ao abrir um documento fechado. Uma referência por anexo é o que faz a contagem do GrydFiles significar o que ela diz significar.

Por que a herança de visibilidade é a regra, e não a redefinição

A cópia herda a visibility do original. A alternativa — recalcular do defaultVisibility do tipo no destino — trocaria uma decisão que uma pessoa tomou por um padrão de configuração, e o erro possível é justamente o pior: um anexo marcado Internal de propósito na requisição renascendo Supplier no pedido porque o tipo tem esse padrão. Herdar preserva a decisão; o destino pode rebaixar livremente e promover com as quatro portas da seção anterior.

Explícita, mas não trabalhosa

Explícito não significa um clique por arquivo. Ao criar o pedido a partir da requisição, a tela mostra os anexos elegíveis — os de tipo com propagates = true, não revogados, com arquivo Available — já marcados, com a visibilidade de cada um à vista, e o usuário desmarca o que não vai. É uma confirmação, não uma digitação. O que ela impede é o caso em que ninguém olhou.

Anexo em Scanning aparece na lista desmarcado e desabilitado, com o motivo escrito. Propagar um arquivo que ainda pode virar Infected multiplicaria o problema por dois documentos.

A cadeia é rastreável em um passo

copiedFromAttachmentId dá o pai; rootAttachmentId dá a origem sem recursão. A tela de auditoria responde "este PDF entrou no sistema onde?" com uma consulta, e "quem mais tem este arquivo?" com o índice em (tenantId, storedFileId) — sem perguntar nada ao GrydFiles, que aliás não saberia responder em termos de documento.

O fim do anexo

Revogação e retenção

Não existe exclusão de anexo. Existe revogação, que é sempre registro, e existe retenção, que é do arquivo e não do vínculo. A parte difícil é que revogar tem dois efeitos diferentes conforme o estado do dono — e a diferença não é detalhe de implementação, é a decisão 5.

O que a revogação faz, conforme o estado do dono
Estado do donoO anexoA FileReferenceO blob
Rascunho (Draft)Revogado, com revokedAt e revokedBy. Motivo é opcionalLiberadaEntra na fila de expurgo e some no fim da carência, se ninguém mais aponta
Em curso ou fechadoRevogado, com motivo obrigatório. Sai da tela, não vale como documento, não vai ao fornecedorMantidaContinua preso. Quem decide a morte passa a ser a retenção
Descartado (evento)Revogado com revocationSource = OwnerDiscardedLiberadaComo no rascunho — é o mesmo caso, chegando por evento
Ausente (varredura)Revogado com revocationSource = OrphanSweep e reportadoLiberada só se lastKnownOwnerState = DraftNa dúvida, preso. Guardar demais é barato; perder prova, não
Arquivo InfectedRevogado com revocationSource = FileInfected. A tela diz o que houveNunca existiu — ela nasceria na liberação, que não aconteceu. Nada a liberarNão é apagado pelo GrydFiles — vai para o prefixo de quarentena, e é decisão dele

Por que documento fechado não libera. Liberar a referência de um anexo de pedido fechado faria o blob morrer no fim da carência de trinta dias. Meses depois, a auditoria abre o pedido, pede o arquivo e recebe FILE_PURGED_CONFLICT — a lápide do GrydFiles, que informa corretamente que o conteúdo não existe mais. Teríamos guardado o registro da prova e perdido a prova. É uma troca entre custo de armazenamento e capacidade de responder a uma auditoria, e ela não é próxima: um PDF de dois megabytes por cinco anos custa menos que uma hora de alguém explicando por que o arquivo sumiu.

O que o aprovador vê depois de uma revogação

Anexo revogado não aparece para o aprovador na fila, porque não é mais evidência válida. Aparece na aba de histórico do documento, com quem revogou, quando, o motivo e a marca visual — e é lá que a auditoria olha. A distinção importa: esconder de quem decide agora, mostrar para quem confere depois.

Revogar evidência de documento submetido aciona explicitamente, na mesma transação, a reavaliação do conteúdo relevante pelo motor de aprovação. Se a evidência sustentava uma decisão, essa decisão não permanece válida sem reavaliação. A notificação durável informa os envolvidos e complementa o controle. A versão efetiva de um pedido emitido permanece preservada durante a aprovação da alteração.

Retenção é resolvida por política e pode ser estendida

O Nexio resolve retenção por tipo, empresa e política versionada no upload, e estende a data quando o marco de contagem ou a obrigação exigir. Pedido e nota são documentos distintos, ligados por referências; um não muda de natureza para virar o outro. O padrão de cinco anos é uma configuração inicial a validar, não autorização automática de expurgo. O CTN, arts. 173, 174 e 195, parágrafo único, distingue decadência, prescrição e conservação de documentos; não fornece uma regra universal de cinco anos após o upload. Guardar fundamento, marco e versão da política; litígio ou obrigação ainda vigente impede expurgo.

Estender é possível pela rota de retenção do GrydFiles, e só para frente — encurtar é recusado com FILE_RETENTION_SHORTENED_UNPROCESSABLE. O caminho para reduzir guarda não é a API: é o expurgo manual, com motivo e permissão própria.

O arquivo compartilhado entre anexos, e o que isso muda

Dois anexos podem apontar para o mesmo storedFileId — por propagação ou por dedupe do GrydFiles, que reconhece o mesmo sha256 dentro do tenant. As retenções podem divergir: o mesmo PDF anexado como GENERAL na requisição e como FISCAL_DOCUMENT na nota. Vale a mais longa, e isso é consequência natural do desenho do GrydFiles — retainUntil só anda para frente, e cada declaração de upload a empurra. Não é regra nova; é a regra existente produzindo o resultado certo.

LGPD, e onde ela cai

Pedido de eliminação por titular esbarra em legalHold e em guarda legal, e é assim que deve ser: a LGPD preserva a guarda exigida por obrigação legal ou regulatória. O que o Nexio precisa oferecer é a resposta — quais anexos contêm dado daquele titular, em quais documentos, sob qual base de retenção — e o índice por dono e por fornecedor é o que torna essa consulta possível. A execução do apagamento, quando cabível, é o expurgo manual do GrydFiles, que preserva a lápide.

Decisão fechada, saída fechada em v1.1

O estado Scanning e o envio para aprovação

O GrydFiles não libera arquivo sem veredito: Pending → Scanning → Available · Infected · Failed. Para o Nexio isso produz uma janela — curta na maioria das vezes, longa quando o arquivo é grande ou o clamd está sob carga — em que o anexo existe e o arquivo ainda não pode ser usado. O veredito chega pelo contrato registrado; esta seção é sobre o que o produto faz com a janela.

A regra: anexar durante a verificação é permitido; submeter não

O usuário anexa, continua preenchendo o documento e vê o estado do arquivo na lista. O que ele não pode é enviar o documento para aprovação enquanto qualquer anexo estiver em Pending, Scanning ou Failed. A submissão é recusada com ATTACHMENT_SCAN_PENDING_CONFLICT, e a resposta nomeia quais anexos estão pendentes.

Failed entrou na lista em 09/09/2026, e a razão é a mesma dos outros dois. Arquivo em Failed não tem veredito, e o GrydFiles recusa o download dele — o aprovador receberia na fila um documento cuja prova ele não consegue abrir. Até 08/09 a regra bloqueava só os dois primeiros estados: a fila diferida desistia em Failed, mas a submissão manual passava. As duas portas passam a fechar juntas. A saída não é esperar indefinidamente: o GrydFiles ganhou POST /files/{id}:rescan, e a tela oferece "tentar verificar de novo" no lugar de um anexo que não serve para nada.

A razão é a fila do aprovador: um aprovador não pode receber um documento cujo anexo ainda pode virar Infected. Se virar, ou ele já leu, ou o documento muda de conteúdo debaixo de uma decisão em curso — e as duas saídas são piores que esperar.

O que não vamos fazer, e está escrito para não ser reinventado

Prender o usuário na tela esperando o antivírus é a solução ruim. Ela transforma um detalhe de infraestrutura em tempo de espera do comprador, e piora exatamente quando não deveria — arquivo grande, hora de pico, base de assinaturas recarregando. Um spinner bloqueante de cinco segundos é aceitável; de quarenta, é um defeito de produto. Esta spec registra a recusa para que a implementação não caia nela por ser a mais fácil de escrever.

A saída — fechada em v1.1, e são as duas coisas

Quem tenta submeter e recebe ATTACHMENT_SCAN_PENDING_CONFLICT escolhe entre ser avisado quando o arquivo liberar — nexio.attachment.scan_completed — e deixar a submissão acontecer sozinha, por uma DeferredSubmission que ele pode cancelar enquanto ela espera. As duas escolhas são a mesma fila. Revisto em 09/09/2026: "só avisar" foi escrito como se não criasse nada, mas "registrado para aquele usuário e aquele documento" é estado, e sem linha em lugar nenhum o pedido se perdia no primeiro reinício. A fila ganha um modo — avisar, ou avisar e submeter —, e não uma segunda tabela: o gatilho, a expiração, o cancelamento e o índice de unicidade são idênticos nos dois casos, e a única diferença é o que acontece quando o último anexo libera. A peça de entrega já existia na plataforma: o GrydNotifications tem in-app pronto, com IInAppNotificationSender e UserNotification, além de push e e-mail.

As quatro perguntas que esta seção listava como sem resposta estão respondidas na spec de Notificação. Quem recebe é quem tentou submeter, não quem anexou — o caso de quem anexou já tem dono, que é o aviso obrigatório de Infected. Veredito demorado não ganha tratamento especial: a fila expira em 72 horas, o documento continua em rascunho e o alerta de operador do GrydFiles é que aponta para o problema real. A submissão automática é pedida, e é por isso que ela não precisa de confirmação depois. E ela aparece como uma faixa no próprio documento, com o que falta liberar e um cancelar. Veredito Infected ou Failed faz a fila desistir com motivo e avisar — nunca submeter.

Consequência para esta spec, e é a mudança real da v1.1: a fila não sabe submeter documento nenhum — ela pergunta ao dono. O registro de descritores ganhou por isso a nona pergunta, SubmitPolicyOf, sem a qual o módulo de submissão diferida teria que conhecer requisição, pedido, cotação, recebimento, nota e contrato — exatamente a dependência que o descritor existe para não ter.

O comportamento já especificado que permanece

O suficiente para não travar ninguém e não fechar nenhuma porta: o estado real na lista, sempre visível; a consulta em lote que a tela usa para atualizar sem vinte chamadas; a recusa de submissão com a lista dos anexos pendentes; e a revogação automática com aviso quando o veredito é Infected. Nenhuma dessas quatro coisas mudou com a decisão de mensageria fechada — ela acrescentou o aviso e a fila, e não substituiu o mecanismo.

FILE_SCAN_UNAVAILABLE não é erro do usuário. Quando o clamd está fora do ar, o GrydFiles devolve 503 — o sufixo _UNAVAILABLE que aquela spec propôs à plataforma e que o mapeador do Core ainda não tem. O Nexio repassa, não reembrulha em 400 nem em 409: a tela precisa dizer "o serviço de verificação está indisponível, tente em instantes", e o suporte precisa distinguir isso de um arquivo recusado. Reembrulhar erro de dependência em erro de cliente é como se perde uma tarde procurando um bug que não existe.

Contrato vigente · revisão consolidada

Autoria e anexos do portal

O portal da v1 usa PortalUser criado pelo convite. No upload, supplierId, portalUserId, portalAccessLinkId e sessão são obrigatórios e resolvidos no servidor; não são aceitos como autoridade a partir do corpo. ownerType é QuotationResponse nesta versão.

Anexo nasce Internal para não publicar a proposta aos concorrentes. O próprio fornecedor pode consultar seus anexos e obter download quando Available, inclusive depois de enviar a proposta enquanto tiver vínculo de leitura vigente. Rascunho permanece invisível ao comprador; resposta enviada torna-se visível conforme regras da cotação.

GrydFiles registra a credencial técnica de serviço; Nexio registra identidade convidada, canal verificado, sessão, instante e vínculo com a resposta. Isso comprova o meio usado, não necessariamente a pessoa física que controlava o e-mail. Comprador que lança proposta recebida por e-mail tem enteredBy=Buyer e autor interno explícito; não é atribuído ao fornecedor como acesso ao portal.

Autorização

Escopo, permissões e trilha

O escopo vem do dono, e fora dele a resposta é 404

companyId é derivado do dono pelo descritor e denormalizado no anexo, e o filtro por UserCompanyScope roda na consulta, sem join entre contextos. Anexo de documento de outra empresa responde 404, não 403 — os dois casos, "não existe" e "não é seu", respondem igual de propósito, que é a regra do DeliveryLocation. Um 403 confirmaria a existência do documento a quem não deveria saber que ele existe.

E há a terceira resposta, que não é nem "é seu" nem "não é seu": companyId nulo. Novo em 09/09/2026. Anexo de item, de fornecedor e de proposta de alteração bancária não pertence a empresa nenhuma, porque o dono deles é do tenant. O filtro por UserCompanyScope passa a ser "a empresa está no escopo do usuário ou o anexo não tem empresa" — a mesma forma de GLAccountCompany e BudgetPolicy, em que a linha sem empresa vale para todas. Escrever o filtro sem essa segunda metade faz a ata de visita ao fornecedor desaparecer para todos os usuários, e nenhum erro é registrado em lugar nenhum.

Permissão de anexo não substitui permissão no dono

manage:attachments é necessária e não suficiente: quem anexa precisa também poder escrever no documento, e quem responde isso é o CanWrite do descritor. Sem essa delegação, o anexo viraria uma porta lateral para escrever em documento alheio — e teríamos duas regras diferentes sobre quem manda numa requisição, o que sempre termina com as duas divergindo.

A trilha: o Nexio registra o que a plataforma não sabe

O GrydFiles já grava no AuditLog a emissão de cada URL de download. O Nexio não repete isso — repetir daria duas linhas para o mesmo ato, com risco de divergirem. O que ele registra é o que lá não existe: qual documento e qual anexo, porque do lado da plataforma só há um fileId, e um fileId não responde "quem baixou o anexo do pedido PED-2026-00871".

Usa o IAuditLogService.RecordAsync do framework, que é a via de evento de negócio manual — o interceptor de diff pega a mudança de linha, não o ato de ler. Ações registradas: attachment.created, attachment.downloaded, attachment.visibility-changed, attachment.copied, attachment.revoked. Cada uma carrega ownerType, o número legível do documento e o attachmentId.

A convenção que estes cinco nomes propuseram fechou na spec de Notificação: <entidade>.<verbo-no-passado> é a ação da trilha, e o slug do aviso é a mesma cauda com o prefixo do produto. A ação vai em AdditionalData["action"] do RecordAsync, porque o AuditLog do GrydAudit não tem coluna de ação — dar-lhe uma é pendência de plataforma, no molde do GrydFiles, e está registrada no mapa de domínio.

Cinco permissões, e nenhuma que libere download de arquivo em Scanning, Infected ou Purged. Como no GrydFiles, a ausência é a decisão.
PermissãoAlcanceObservação
read:attachmentsListar, ver metadado e pedir URL de downloadSempre recortada pelo escopo da empresa do dono e pela visibility. Todo papel que abre documento precisa dela
manage:attachmentsAnexar, descrever, trocar o tipo em rascunho e propagarNão basta: exige também poder escrever no dono. Ver acima
revoke:attachmentsRevogar anexo de outra pessoa, e revogar fora de rascunhoQuem anexou revoga o próprio anexo em rascunho só com manage. Retirar prova de documento em curso é outro ato
share-with-supplier:attachmentsMarcar visibility = SupplierSeparada de propósito — é a permissão cujo erro vaza. Rebaixar para Internal não exige nada
manage:attachment-typesAdministrar o catálogo do tenantAdministração de configuração. Mexe em retenção, então é de administrador, não de comprador

Interface

Telas

Anexo não tem tela própria — ele é um componente que aparece dentro do documento dono. As exceções são as duas telas de administração.

Documento · painel de anexos

Lista com ícone por contentType, nome, tipo, tamanho, quem anexou, quando, e o estado do arquivo à mostra — não um spinner. Anexo Supplier leva marca visível na linha, porque a pergunta "o fornecedor vê isso?" precisa ser respondida sem abrir nada. Arraste solta arquivo direto; a escolha do tipo é o único campo obrigatório.

Documento · o que está pendente

Quando algum anexo está em Scanning, o botão de enviar para aprovação fica desabilitado com o motivo escrito e a lista de quais faltam — nunca um erro depois do clique. Mesma tese da rota de blocking documents do local de entrega: a tela pergunta antes de agir.

Documento · propagação

Ao gerar o pedido a partir da requisição, um passo mostra os anexos elegíveis já marcados, com a visibilidade de cada um visível e editável ali. Os em Scanning aparecem desabilitados com o motivo. Desmarcar é um clique; a confirmação é o ato explícito.

Documento · histórico

Aba separada com os revogados: quem, quando, por quê, e se o arquivo ainda existe. É a tela que a auditoria abre, e é a razão de a revogação ser registro e não exclusão.

Portal · resposta de cotação

O fornecedor vê os anexos Supplier do documento pai e só os seus. Sobe proposta e certidão sem saber que existe um AttachmentType — o tipo é decidido pelo contexto da tela. Estado do antivírus visível também aqui, com a mesma honestidade.

Admin · tipos de anexo

Catálogo do tenant, com os tipos globais listados e não editáveis. A coluna de retenção é a primeira depois do nome, porque é o campo com consequência de anos. Trocar retenção avisa que não afeta anexos existentes — só o que vier depois.

Admin · saúde dos anexos

O relatório da varredura: órfãos encontrados por tipo de dono, anexos em Failed, arquivos Infected nos últimos 90 dias, e o peso total por empresa. Órfão não é ocorrência normal — é sintoma de consumidor que esqueceu de publicar o evento, e o número precisa ter dono.

Auditoria · rastro do arquivo

A partir de um anexo: a cadeia até o rootAttachmentId, os outros anexos que compartilham o mesmo storedFileId, e a trilha de quem baixou. Responde "onde este PDF entrou e para onde ele foi" numa tela.

API

Endpoints

Não existe DELETE /attachments/{id}, pelo mesmo motivo que não existe DELETE /files/{id} no GrydFiles: apagar é ato com consequência, e dar a ele o verbo mais fácil da API é convidar ao acidente. O que existe é revogar, que é registro.
RotaPermissãoDevolveErros
GET /attachmentsread:attachmentsOs anexos de um dono (ownerType + ownerId, obrigatórios), com o status do arquivo já resolvido em lote. Revogados só com includeRevoked400 · 404
POST /attachments/upload-intentmanage:attachmentsfileId, uploadUrl, uploadExpiresAt e os cabeçalhos do PUT. Valida dono, tipo, escopo e limites antes de reservar400 · 404 · 409 · 422
POST /attachmentsmanage:attachmentsAttachmentDto. Confirma no GrydFiles, cria o anexo e a FileReference. Idempotente por fileId: chamar duas vezes devolve o mesmo anexo400 · 404 · 409 · 422
GET /attachments/{id}read:attachmentsAttachmentDto com o número legível do documento dono, resolvido pelo descritor404
GET /attachments/statusread:attachmentsA rota da decisão 10. Recebe até 100 ids e devolve o status de cada arquivo numa chamada. Listar vinte anexos não vira vinte chamadas400
GET /attachments/{id}/download-urlread:attachmentsURL assinada de leitura, TTL do perfil. Autoriza primeiro — escopo, visibilidade, revogação — e só então chama o GrydFiles. Registra documento e anexo na trilha404 · 409 · 503
PUT /attachments/{id}manage:attachmentsAltera description e, em rascunho, attachmentTypeId entre tipos de mesma retenção. storedFileId, ownerType e ownerId são recusados quando divergem404 · 409 · 422
POST /attachments/{id}/visibilityshare-with-supplier:attachmentsPromove a Supplier. Rebaixar para Internal aceita só manage:attachments. Estado por rota própria, nunca por PUT404 · 409 · 422
POST /attachments/{id}/revokerevoke:attachmentsRevoga. Libera a referência só com o dono em rascunho. Motivo obrigatório fora dele404 · 409 · 422
POST /attachments/copymanage:attachmentsPropagação explícita, em lote: lista de attachmentId e o dono de destino. Cria um anexo e uma FileReference por item, herdando a visibilidade400 · 404 · 409 · 422
GET /attachments/copy-candidatesread:attachmentsO que a tela de propagação mostra: elegíveis, já marcados, com o motivo de cada inelegível. A tela não reimplementa a regra400 · 404
GET /attachments/{id}/chainread:attachmentsA cadeia de propagação até a raiz, mais os anexos que compartilham o mesmo storedFileId dentro do escopo do usuário404
GET /attachments/owner-typesread:attachmentsO registro, do jeito que a tela precisa: tipos aceitos, limites, se aceita visibilidade de fornecedor, para onde propaga—
POST /portal/quotations/{id}/attachmentstoken da RFQO caminho do fornecedor, com upload-intent e confirmação embutidos. supplierId vem do token; nasce Internal; ownerType é sempre QuotationResponse400 · 401 · 409 · 422
GET /attachment-typesread:attachmentsOs tipos ativos, já resolvidos — para cada code, a linha do tenant quando existe, senão a global —, filtráveis por ownerType. É o que popula o seletor400
POST PUT /attachment-typesmanage:attachment-typesAdministração do catálogo do tenant. Tipo global não muda: POST com o code de um global cria a sobreposição do tenant, e PUT sobre id global a materializa com o corpo enviado, devolvendo o id dela; code e retentionMode são imutáveis depois do primeiro uso400 · 404 · 409 · 422
PATCH /attachment-types/{id}/deactivatemanage:attachment-typesSome do seletor. Em id global, materializa a sobreposição do tenant com isActive = false — o global continua ativo nos outros tenants. Não invalida anexo existente — nunca há exclusão de tipo com uso404 · 409

Contrato

Contrato de erros

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

Os erros do GrydFiles são repassados sem reembrulho: FILE_TOO_LARGE_UNPROCESSABLE, FILE_CONTENT_TYPE_NOT_ALLOWED_UNPROCESSABLE, FILE_SIZE_MISMATCH_UNPROCESSABLE, FILE_UPLOAD_EXPIRED_CONFLICT e FILE_SCAN_UNAVAILABLE (503) chegam ao cliente com o código de lá. Traduzir "arquivo grande demais" em ATTACHMENT_SOMETHING só faria a mensagem perder o teto que a tela precisa mostrar.
CódigoHTTPQuando
ATTACHMENT_NOT_FOUND404Não existe neste tenant — ou o dono está fora do escopo do usuário. Os dois casos respondem igual, de propósito
ATTACHMENT_OWNER_NOT_FOUND404O documento dono não existe ou está fora do escopo. Vem do descritor, não de uma FK
ATTACHMENT_OWNER_TYPE_UNKNOWN400ownerType sem descritor registrado. Erro de programação do consumidor — em produção isso não chega, porque a inicialização já teria falhado
ATTACHMENT_OWNER_NOT_WRITABLE_CONFLICT409Anexar ou alterar em documento que o usuário não pode editar, ou que não aceita mais alteração. A regra é do dono, não daqui
ATTACHMENT_TYPE_NOT_FOUND404Tipo inexistente neste tenant, contando os globais
ATTACHMENT_TYPE_INACTIVE_UNPROCESSABLE422Tipo desativado. Anexo antigo com esse tipo continua válido — só o novo é recusado
ATTACHMENT_TYPE_NOT_ALLOWED_FOR_OWNER_UNPROCESSABLE422O tipo não está na interseção entre allowedOwnerTypes e o que o descritor aceita. A mensagem lista os tipos válidos ali
ATTACHMENT_TYPE_RETENTION_CHANGE_UNPROCESSABLE422Troca de tipo para outro com retenção diferente. Mudaria a data de expurgo de um arquivo já sob guarda
ATTACHMENT_LIMIT_EXCEEDED_CONFLICT409Teto de quantidade ou de peso somado, do tipo ou do descritor. A mensagem diz qual dos dois e qual é o teto
ATTACHMENT_SUPPLIER_VISIBILITY_NOT_ALLOWED_UNPROCESSABLE422Supplier num tipo com allowsSupplierVisibility = false, ou num dono que não expõe anexo. A mensagem nomeia qual das duas portas fechou
ATTACHMENT_VISIBILITY_LOCKED_CONFLICT409Alterar visibilidade depois de o documento ter ido ao fornecedor. O caminho é revogar, não fingir que ele não viu
ATTACHMENT_ALREADY_REVOKED_CONFLICT409Revogação é terminal. Não existe "desrevogar" — o caminho de volta é anexar de novo
ATTACHMENT_REVOCATION_REASON_REQUIRED_UNPROCESSABLE422Revogar fora de rascunho sem reasonCodeId. Retirar prova de documento em curso precisa de motivo nomeado
ATTACHMENT_SCAN_PENDING_CONFLICT409A decisão 8. Submeter para aprovação com anexo em Pending, Scanning ou Failed — os três desde 09/09/2026. A resposta lista quais e em que estado, porque "espere" e "mande verificar de novo" são conversas diferentes
ATTACHMENT_FILE_NOT_AVAILABLE_CONFLICT409Baixar, propagar ou enviar ao fornecedor arquivo que ainda não passou pelo antivírus — ou seja, anexo que ainda não tem FileReference. É o estado normal de "espere um pouco"
ATTACHMENT_FILE_INFECTED_CONFLICT409Separado do anterior de propósito: a tela precisa dizer o que houve e o suporte precisa distinguir fila de incidente
ATTACHMENT_FILE_PURGED_CONFLICT409O anexo existe, o conteúdo não. Traz purgedAt e o motivo, vindos da lápide do GrydFiles
ATTACHMENT_COPY_SOURCE_NOT_ELIGIBLE_UNPROCESSABLE422Propagar anexo revogado, com arquivo indisponível, ou de tipo com propagates = false
ATTACHMENT_COPY_TARGET_MISMATCH_UNPROCESSABLE422Destino de outra empresa, ou ownerType fora do PropagatesTo da origem. Anexo não atravessa empresa
ATTACHMENT_SUPPLIER_REQUIRED_UNPROCESSABLE422uploadedByKind = PortalUser sem supplierId. Não deveria acontecer — o token resolve — e existe para nunca acontecer em silêncio
ATTACHMENT_PORTAL_OWNER_NOT_ALLOWED_UNPROCESSABLE422Caminho do portal com ownerType diferente de QuotationResponse
ATTACHMENT_TYPE_CODE_ALREADY_EXISTS409Código repetido no tenant — inclusive quando a linha existente é a sobreposição de um global. Coincidir com um global não é este erro: é sobreposição
ATTACHMENT_TYPE_IN_USE_CONFLICT409Alterar code ou retentionMode de tipo já usado, ou tentar excluí-lo. O caminho é desativar
ATTACHMENT_TYPE_RETENTION_UNPROCESSABLE422retentionYears ausente em RetainUntil, ou presente nos outros modos
ATTACHMENT_DESCRIPTION_TOO_LONG400Acima de 300 caracteres

Invariantes

Regras de negócio

RN-ATT-01Todo anexo tem arquivo, tipo e dono

storedFileId, attachmentTypeId, ownerType e ownerId são obrigatórios e imutáveis. Não existe anexo solto no tenant: alguma coisa sempre responde por ele, e é essa coisa que decide quase todo o resto.

RN-ATT-02Integridade tem contrato e restrição estratégica

IDs entre contextos evitam dependência de navegação. FK estratégica é permitida quando aplicável e registrada pela arquitetura; dono polimórfico exige descritor, validação na transação, atualização explícita de estado e reconciliação. Varredura posterior não substitui uma invariante atômica.

RN-ATT-03Todo valor do enum tem descritor registrado, e o que falta derruba a inicialização

Falha no startup, não em produção. Reformulada em 09/09/2026: a verificação percorre AttachmentOwnerType e exige um descritor registrado para cada valor — é sobre o enum, que o código enxerga, e não sobre "o módulo dono publicou", que ele não enxerga. É o que faz o enum ser realmente fechado: entrar na lista custa exatamente o que custa responder às onze perguntas do registro, e as respostas dos quinze valores estão declaradas nesta página.

RN-ATT-04Nenhum agregado navega para Attachment

Não existe coleção de anexos no modelo EF de requisição, pedido ou contrato. A consulta é sempre pelo serviço de anexo, por ownerType e ownerId. É o que impede o polimorfismo de virar dependência circular entre módulos.

RN-ATT-05Empresa e operação são autorizadas

CompanyOf deriva do dono e filtra visibilidade; UserCompanyRole e teto da plataforma autorizam a operação. Fora do escopo retorna 404. Dono sem empresa segue seu contrato de recurso compartilhado, sem conceder acesso a documento empresarial.

RN-ATT-06Permissão de anexo não substitui permissão no dono

manage:attachments é necessária e não suficiente: quem anexa precisa poder escrever no documento, e quem responde isso é o CanWrite do descritor. Duas regras sobre quem manda numa requisição sempre terminam divergindo.

RN-ATT-07Cada anexo cria e mantém a sua própria FileReference

(storedFileId, "nexio.attachment", attachmentId). Cópia inclusive. É o que faz a contagem de referências do GrydFiles significar o que ela diz significar, e o que impede um documento perder o arquivo porque outro descartou o dele. Desde 09/09/2026 a primeira referência não é criada aqui: o anexo declara o par pendente na confirmação e o módulo de arquivo cria a referência na liberação. A cópia, sim, cria a sua diretamente — ela só é oferecida sobre arquivo já liberado.

RN-ATT-08Retenção é declarada no upload, resolvida do AttachmentType

retentionMode, retainUntil e legalHold vão no POST /files. O Nexio decide, o GrydFiles executa. Mudar a configuração do tipo depois não altera o que já foi anexado.

RN-ATT-09A exigência de anexo mora no consumidor

"Requisição acima de X exige três orçamentos" é regra da requisição ou do motor de aprovação, nunca do anexo. Escrevê-la aqui criaria um segundo motor de regra, paralelo e desalinhado com o primeiro.

RN-ATT-10Visibilidade exige quatro portas abertas

Tipo permite, dono permite, usuário tem share-with-supplier:attachments, anexo é editável. Rebaixar para Internal não exige nada — reduzir exposição nunca deveria ter atrito.

RN-ATT-11Depois do envio ao fornecedor, a visibilidade trava

409. Rebaixar depois do envio seria fingir que o fornecedor não viu. O caminho honesto é revogar — que o retira da tela dele daqui para a frente e deixa registrado que ele existiu.

RN-ATT-12Próprio anexo permanece consultável

Fornecedor autorizado vê anexo de sua própria resposta, inclusive Internal, e anexos comuns Supplier publicados da cotação. Nunca vê anexo de outro fornecedor por visibility isolada. Validar alvo, rodada e vínculo antes de ambos os caminhos.

RN-ATT-13Propagação é explícita e herda a visibilidade

Nunca automática. A cópia herda a visibility do original em vez de recalcular do padrão do tipo — recalcular trocaria uma decisão humana por um padrão de configuração, e o erro possível é o pior deles.

RN-ATT-14Propagação não atravessa empresa nem cria vínculo entre documentos

Destino de outra empresa é 422. E revogar a origem não revoga as cópias: cada documento responde pelo que anexou.

RN-ATT-15Não se propaga nem se baixa arquivo que não está Available

Multiplicar por dois documentos um arquivo que ainda pode virar Infected é criar dois problemas. Na tela, o item aparece desabilitado com o motivo — não some.

RN-ATT-16Não existe exclusão de anexo

Existe revogação, que é registro e é terminal. Sem DELETE na API, pelo mesmo motivo que não há DELETE /files/{id} no GrydFiles.

RN-ATT-17Revogar em rascunho libera a referência; fora de rascunho, não

A invariante central do fim de vida. Liberar em documento fechado faria o blob morrer em trinta dias e a auditoria receber a lápide — guardaríamos o registro da prova e perderíamos a prova. Fora de rascunho, quem decide a morte é a retenção.

RN-ATT-18Revogação material exige reavaliação

Retirar evidência material da revisão em aprovação invalida a decisão dependente e exige reavaliação. Preservar hash, versão, motivo e relação com assinaturas anteriores. Pedido efetivo não é cancelado silenciosamente; alteração e ação corretiva seguem seu processo.

RN-ATT-19Não se envia para aprovação com anexo sem veredito

Anexar durante o scan é permitido; submeter não. Vale para Pending, Scanning e — desde 09/09/2026 — Failed: arquivo sem veredito não abre, e o aprovador não pode receber documento cuja prova ele não consegue ler. A recusa nomeia quais anexos estão pendentes, e a tela desabilita o botão antes do clique em vez de mostrar erro depois dele.

RN-ATT-20Arquivo Infected revoga o anexo automaticamente

Com revocationSource = FileInfected, pelo contrato de veredito e na mesma transação. O aviso vai a quem anexou e, quando não há quem anexou — o caminho do portal, em que uploadedByUserId é nulo —, ao ResponsibleOf do documento: a proposta infectada de um fornecedor não pode deixar de avisar alguém dentro da empresa. Infected é terminal no GrydFiles e não há liberação manual, então não há estado intermediário a modelar aqui.

RN-ATT-21Autoria do portal é vínculo comprovado

supplierId e portalUserId obrigatórios são derivados da sessão convidada, com link e método de verificação. Nasce Internal mas permanece visível ao próprio fornecedor. Identidade convidada não é prova absoluta da pessoa física autora.

RN-ATT-22Espelha-se o imutável; o que muda é consultado em lote

originalFileName, contentType e sizeBytes são colunas. status não é — e a consulta aceita até cem ids por chamada. Espelhar estado que muda é como se cria a divergência que ninguém consegue explicar depois.

RN-ATT-23A trilha do Nexio complementa a do GrydFiles, não a repete

Lá fica a emissão da URL, por fileId. Aqui fica qual documento e qual anexo, por IAuditLogService.RecordAsync. Duas linhas para o mesmo ato acabariam divergindo.

RN-ATT-24Erro de dependência é repassado, não reembrulhado

FILE_SCAN_UNAVAILABLE chega como 503. Transformar indisponibilidade externa em erro de cliente custa uma tarde de investigação de um bug que não existe.

RN-ATT-25Tipo global não muda — o tenant sobrepõe

Nenhuma escrita do tenant alcança a linha global. Editar ou desativar um tipo global materializa a linha do tenant com o mesmo code, e é ela que passa a valer ali; os outros tenants continuam vendo o global. Mesma regra do NotificationTemplate e do ReasonCode.

RN-ATT-26A referência de arquivo nasce na liberação, e antes dela o anexo não serve para nada além de aparecer

Nova em 09/09/2026. POST /attachments declara o par pendente e não cria FileReference. Enquanto o veredito não vem, o anexo é listado com o estado real e não é referenciável, baixável, propagável nem enviável ao fornecedor. É o que mantém verdadeira, no passo normal, a invariante do GrydFiles de que nada é referenciável antes do veredito.

RN-ATT-27Atomicidade precisa de conexão e transação comuns

IFileOutcomeHandler participa da DbConnection e DbTransaction de GrydFiles. Falha reverte veredito, referência e efeito Nexio. Bancos separados não cumprem este contrato e exigem recusar configuração até haver protocolo alternativo aprovado.

RN-ATT-28O último estado conhecido do dono é gravado pelo dono, na transição

Nova em 09/09/2026. lastKnownOwnerState é escrito no mesmo método que muda o estado do documento, nunca por leitura preguiçosa e nunca pelo módulo de anexo. É o único dado que a varredura de órfãos tem quando o dono já não existe, e sem ele a terceira mitigação do polimorfismo não é executável.

RN-ATT-29Todo aviso de anexo tem destinatário resolvível

Nova em 09/09/2026. O descritor responde OpenApprovalOf para o aviso que vai aos aprovadores pendentes e ResponsibleOf — nunca vazio — para o que vai a quem responde pelo documento. Audiência que não resolve entrega a zero pessoas sem erro, e um aviso obrigatório que falha em silêncio é pior do que não existir.

RN-ATT-30companyId nulo é anexo do tenant, e o escopo por empresa não o esconde

Nova em 09/09/2026. Quando o CompanyOf do descritor devolve nulo — item, fornecedor, proposta de alteração bancária, e a instância de aprovação cujo alvo é um desses —, o anexo é do tenant e é visível em qualquer empresa que o usuário enxergue. O filtro é "empresa no escopo ou empresa nula", como em GLAccountCompany e BudgetPolicy. É a mesma lição do escopo por empresa em toda a base: ele restringe escrita, nunca apaga leitura. Sem a regra escrita, as duas implementações possíveis falham em silêncio — a estrita faz o anexo de cadastro do fornecedor sumir para todos, a frouxa o mostra a quem não deveria vê-lo.

Contexto

Referência de mercado

Como os produtos da amostra resolvem anexo
ProdutoModeloO que aproveitamos
Oracle Fusion · iProcurementDuas tabelas: FND_DOCUMENTS guarda o arquivo e FND_ATTACHED_DOCUMENTS amarra em ENTITY_NAME + PK1_VALUE, com categoria de anexo por entidadeA tese inteira. Duas camadas, amarração polimórfica com o tipo do dono em texto, e categoria como configuração. Trinta anos de produção validando o desenho
Oracle · categorias de visibilidadeTo Buyer, To Supplier, To Approver, Internal to RequisitionA ideia, simplificada. Quatro níveis viram dois: quem é do tenant vê tudo o que o escopo permite; o de fora vê o que foi marcado. Separar aprovador de comprador criaria uma matriz que ninguém mantém
SAP AribaAnexo na requisição propaga para o pedido; sinalizador de visível ao fornecedor por documentoA propagação ao longo da cadeia, e a confirmação de que ela precisa ser vista por alguém — no Ariba a reclamação recorrente é anexo que chegou ao fornecedor sem que o comprador percebesse
CoupaAnexos em requisição, pedido e fatura, com caixa "visível ao fornecedor" e comentário com anexoA tese de que anexo vive dentro do documento e não tem tela própria. O comentário com anexo é a extensão natural quando existir comentário no Nexio
ERPs com DMS acopladoSharePoint, Documentum, GED próprio: o anexo é um link para um repositório documental externo, com workflow e versionamento própriosRecusado. Traz pasta, permissão paralela e versionamento — três coisas que o Nexio não precisa e que criam um segundo sistema de autorização ao lado do primeiro. O GrydFiles é deliberadamente menor que um GED
Guarda fiscal · CTN arts. 173, 174 e 195Prazos e marcos dependem da obrigação; conservação não se reduz a uma contagem única após uploadO padrão de cinco anos requer validação fiscal por tipo e empresa, com política versionada e bloqueio de expurgo enquanto houver obrigação ou legalHold.
LGPD · art. 16Guarda permitida para cumprimento de obrigação legal ou regulatória, mesmo após o fim do tratamentoA base que sustenta o legalHold vencer o pedido de eliminação. O que o produto precisa entregar é a resposta — quais anexos, em quais documentos, sob qual retenção

Limites

Fora de escopo

Fora — é de outro módulo
  • Versionamento de documento. "Contrato v1, v2, v3" é do Contract, que tem aditivo, vigência e assinatura. Aqui, arquivo novo é anexo novo.
  • Tudo o que é do GrydFiles: upload, hash, antivírus, quarentena, dedupe, expurgo, lápide, chave do objeto e provedor de storage.
  • Arquivo com validade própria. FISPQ, certidão, laudo com número e órgão emissor são ItemDocument e SupplierDocument, pela regra de corte da decisão 1.
  • Importação em massa e relatório gerado. Usam StoredFile com retenção curta e nunca criam Attachment — não são prova de nada, são insumo e saída.
Fora agora, gancho previsto
  • Assinatura eletrônica ICP-Brasil. O campo de evidência de assinatura cabe no Contract; o anexo continua sendo o arquivo. Nada implementado.
  • OCR e extração. Ler a DANFE anexada para conferir a nota é caso de Invoice e de three-way match, e depende de decisões que ainda não existem.
  • Pré-visualização e edição. O navegador já exibe PDF e imagem pela URL assinada. Visualizador embutido é custo sem retorno enquanto isso for verdade.
  • Comentário em anexo. Só faz sentido quando existir comentário no documento — e aí é o comentário que ganha anexo, não o contrário.
  • Pasta e estrutura hierárquica. O documento é a pasta. Hierarquia dentro do anexo é o começo de um GED, e a decisão de não construir um já foi tomada.
  • E-mail para anexo. Encaminhar uma mensagem e ela virar anexo do pedido depende de identificar o documento pelo assunto — e de decidir o que fazer quando não dá.

Uma decisão a menos, e a que sobrou é pequena. A de § Scanning estava declarada em aberto na v1.0 e foi fechada em 03/09/2026 pela spec de Notificação — que também trouxe a nona pergunta do registro de descritores. O ReasonCode saiu no mesmo dia: reasonCodeId deixou de ser gancho órfão e passou a apontar para o catálogo, no uso AttachmentRevoke. Continua anulável — rascunho e revogação de sistema não pedem motivo —, e o revocationSource continua enum fechado, porque motivo de sistema não é motivo de usuário. Esta spec não tem mais nenhuma decisão em aberto.

Contrato vigente · revisão consolidada

Versão de bytes e evidência

Attachment guarda referência à versão selada, SHA-256 e identidade da evidência usados pela aprovação, além do nome/tipo/tamanho. Veredito e download apontam para os mesmos bytes. Nova versão de arquivo é nova evidência e não substitui conteúdo já assinado.

Remover anexo durante scan não deve permitir callback tardio recriar referência revogada: o desfecho verifica versão esperada do consumidor. Submissão diferida revalida revisão, autorização empresarial, evidências, orçamento e estado do documento antes de agir.