Especificação · Transversal

Anexo

O arquivo já foi resolvido: o GrydFiles guarda bytes, calcula hash, passa antivírus, conta referências e executa retenção — e não sabe o que o arquivo é. Esta spec escreve a outra metade, que é a única que o Nexio precisa saber: de qual documento este arquivo é prova, quem pode vê-lo, o que acontece com ele quando o documento avança, e o que acontece quando alguém o remove. Duas entidades, nenhuma linha de código de storage, e a resposta para os seis agregados que hoje resolveriam anexo cada um do seu jeito.

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.16 · 21/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:files 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 nunca submetido libera o arquivo; fora dele, 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 dele, quem decide a morte do arquivo é o fim da guarda, contado do fechamento do dono — ou a eliminação a pedido de titular —, nunca 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. Na v1 a tela acompanha o veredito e a pessoa envia quando o arquivo libera; nenhum processo em segundo plano submete documento em nome de ninguém. 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 entrar no portal sem conta — a forma de acesso é do Portal do Fornecedor. 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. O Attachment é gravado na reserva do upload e aparece na confirmação, quando o Nexio declara ao GrydFiles, no processo, a referência esperada — a quem o arquivo vai pertencer. Não cria FileReference: a liberação cria a referência na transação do GrydFiles, e o veredito chega ao Nexio depois do commit, pelo contrato registrado. 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
Um objeto por uploadGrydFilesCada upload é um registro e um objeto próprios. Dois anexos só compartilham arquivo por propagação, que é ato do Nexio
Contagem de referências e expurgoGrydFilesDeclara a referência esperada, cria a referência da cópia e libera referências — sempre depois do próprio commit (RN-ATT-35). Nunca apaga arquivo fora da eliminação a pedido de titular — não existe rota de exclusão do outro lado
Quanto tempo guardarNexio decide, GrydFiles executaDeclara retentionMode e retainUntil na reserva do upload, resolvidos do AttachmentType; no fechamento do dono estende a data; no fim da guarda libera a referência. Guarda legal e eliminação são atos do Nexio, que chegam ao arquivo como legalHold e expurgo
Quem pode ver este documentoNexio, antes de pedir a URLA decisão é visibility + escopo + CanRead do dono. Os hosts do Nexio não expõem /files/* — asserção 8 da Arquitetura, a lista branca de endpoints. read:files é permissão da rota HTTP do GrydFiles, que o Nexio não expõe; em processo a IFileService não confere permissão de módulo e o GrydFiles não define identidade de serviço — vale o consumidor registrado para o escopo nexio.attachment, o ato e o autor declarado marcado (contrato em processo do GrydFiles, RN-GF-28). Não ir a papel de tenant é requisito do catálogo de permissões da plataforma, na demanda — não é o que a asserção 8 verifica
O que o arquivo éNexioAttachment · AttachmentType · ownerType. O GrydFiles guarda só ownerScope e ownerKey em texto opaco

Como o Nexio fica sabendo do veredito. Pelo contrato de veredito do GrydFiles: o Nexio registra uma implementação de IFileOutcomeHandler para o escopo nexio.attachment. A transição de veredito é uma transação do GrydFiles — estado, sha256, a FileReference da referência esperada e o registro do veredito a entregar, gravados juntos —, e o veredito chega ao handler depois do commit, pelo menos uma vez. Não existe unidade de trabalho que atravesse os dois módulos: cada um tem o seu DbContext. A regra é a de todo o produto: O efeito que precisa ser atômico com o ato acontece na mesma transação — pelo próprio caso de uso, por chamada síncrona declarada entre contextos (.Contracts) ou pelo contrato de desfecho do motor; o evento de domínio é publicado depois do commit e serve para notificar, medir e integrar — nunca para produzir efeito de que o ato dependa. O veredito é verdade sem o Nexio; o que o Nexio faz com ele — revogar, avisar, destravar a tela — é reação que pode chegar depois, e nenhuma decisão do produto depende de ela já ter chegado (RN-ATT-19, RN-ATT-27).

O que o handler exige e faz, e é a lista inteira. É idempotente; decide pelo estado atual do arquivo, relido pela IFileService, e não pelo que a mensagem diz — duas entregas fora de ordem terminam no mesmo lugar; roda com o tenant do arquivo restaurado e, sem ele, falha fechado; anexo que não existe, ou cujo storedFileId não é o do arquivo, é desfecho normal — libera a referência, se houver, e confirma a entrega; anexo ainda em Reserved — a confirmação deu certo no GrydFiles e a gravação de Expected falhou — é tratado como Expected, porque o veredito só existe se a confirmação aconteceu (RN-ATT-36). Efeito que falha volta na próxima entrega; o veredito nunca é desfeito. Por desfecho: em Available, grava usableAt e fileReferenceStatus = Active — e, se o anexo foi revogado enquanto esperava, aplica à referência recém-criada a regra da revogação (RN-ATT-17); em Infected, revoga com revocationSource = FileInfected e avisa (RN-ATT-20) — se o arquivo já tinha sido liberado, o efeito segue o estado do dono (RN-ATT-37); na reprovação (Purged com rejectionCode), revoga com revocationSource = FileRejected, copia o rejectionCode e avisa; em Failed, não revoga nada — avisa quem anexou, ou o ResponsibleOf, que o arquivo precisa de nova verificação ou de remoção, uma vez por episódio.

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. Os hosts do Nexio não expõem as rotas /files/* — é a asserção 8 da Arquitetura, a lista branca de endpoints, verificada no CI. read:files, upload:files, retain:files e purge:files não vão a papel de tenant — são permissões das rotas HTTP do GrydFiles, e não ir a papel de tenant é requisito do catálogo de permissões da plataforma, registrado na demanda; a asserção 8 não o verifica. Em processo, a IFileService não confere permissão de módulo e o GrydFiles não define identidade de serviço; vale o consumidor registrado para o escopo, o ato e o autor declarado marcado. admin:system só de pessoa autenticada, só onde a spec o nomeia. É a frase única do contrato em processo (RN-GF-28), e ela vale igual aqui, na página do GrydFiles e na demanda de plataforma. Quem verifica que só o consumidor registrado resolve a porta do escopo nexio.attachment é a asserção 10 da Arquitetura, Quem resolve a porta de cada escopo é o consumidor declarado, em duas metades: teste estático entre assemblies e verificação em execução sobre o contêiner montado. Aqui o escopo é nexio.attachment, que nenhum outro assembly usa; o ato é o que a chamada declara — eliminação, guarda —; e o autor declarado vai marcado como declarado na trilha, ao lado do executor de serviço. As permissões de arquivo ficam na policy das rotas HTTP (constatação no código do Gryd.IO, FilesController.cs:242 @e4eb5a4f) e não são o que autoriza o Nexio. A SPA do Nexio fala só com /attachments, que é onde a autorização de negócio acontece. Com /files/{id}/download-url ao alcance do navegador, quem tivesse a permissão de módulo leria o comprovante bancário sem passar pelo CanRead do dono; com /files/{id}/references, prenderia arquivo alheio ou impediria a eliminaçã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 e CanWrite, confere que o tipo é aceito naquele dono e os limites, resolve a retenção, reserva o arquivo pela IFileService com o perfil nexio.attachment e grava o anexo em fileReferenceStatus = Reserved, com dono, tipo, retenção e quem reservou. A recusa cara acontece antes de gastar rede. A reserva não aparece na lista; se a gravação falhar depois de reservar, o arquivo vence sozinho no GrydFiles.

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 só aceita o fileId de uma reserva feita pelo mesmo usuário, no mesmo tenant, para o mesmo dono e o mesmo tipo — fora disso, ATTACHMENT_UPLOAD_NOT_RESERVED_CONFLICT (RN-ATT-36). Reconfere o CanWrite do dono, que pode ter mudado desde a reserva — documento que não aceita mais escrita responde ATTACHMENT_OWNER_NOT_WRITABLE_CONFLICT e nada é confirmado. Então confirma pela IFileService, declarando a referência esperada ownerScope = "nexio.attachment" · ownerKey = attachment.id — no processo, nunca por rota HTTP, que não aceita dono —, e grava Expected. Nenhuma FileReference é criada aqui. O anexo passa a aparecer na lista com o estado real; o que ele ainda não é, é referenciável, baixável ou propagável. A resposta volta com o arquivo em Scanning. Repetir a chamada repete a confirmação e devolve o mesmo anexo; reprovação na confirmação — tamanho divergente, reserva vencida — revoga o anexo com FileRejected e repassa o erro do GrydFiles.

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 da referência esperada na própria transação e, depois do commit, entrega o veredito ao Nexio. 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 com rejectionCode) chegam pelo mesmo contrato, sem referência nenhuma para desfazer: o anexo é descartado — revogado com FileInfected ou FileRejected, sem direito a arquivo —, e a tela e o aviso dizem o que aconteceu. Failed não descarta: pede nova verificação ou remoção.

O que a inversão compra. Criar a referência no passo 3, sobre um arquivo em Scanning, tornaria falsa a invariante central do módulo de arquivo — a que diz que nada é referenciável antes do veredito. E as duas saídas negativas ficariam inconsistentes por construção: a reprovação por tipo apagaria um objeto com referência ativa apontando para ele, e Infected deixaria uma referência viva apontando para blob em quarentena.

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. Criar o vínculo cedo é decisão de produto; criar a FileReference cedo seria violar a regra de quem guarda o arquivo. As duas estão separadas: o Attachment aparece 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 deixa de ser uma promessa sobre um arquivo que ainda pode 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.
AttachmentLegalHold · AttachmentErasureRequest atos de controle · Nexio A guarda legal por caso, filha do anexo (RN-ATT-32), e o pedido de eliminação de titular, sobre o arquivo, com quem registrou e quem executou (RN-ATT-33, RN-ATT-38). Nenhum dos dois traz nome de arquivo.
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 só escreve nele pelas chamadas em processo do próprio módulo — referência, retenção, guarda legal e expurgo —, sempre depois do próprio commit (RN-ATT-35).
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. 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.

Quatro tabelas: core.attachments, core.attachment_types, core.attachment_legal_holds e core.attachment_erasure_requests. Í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), que só precisa cobrir as linhas com supplierId preenchido — índice parcial é exemplo de implementação, e um índice comum serve igual —, 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. O requisito é esse, e a verificação vale na aplicação; como o banco o garante — dois índices parciais, ou um único que trate nulos como iguais — é exemplo de implementação, e a escolha do banco é da Arquitetura. 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.
lastKnownOwnerState · lastKnownOwnerStateAtenum · DateTimegeradoDraft · InProgress · Closed — o último estado do dono que o Anexo conhece, e o instante da transição que o produziu. Na criação vem do StateOf, com a hora da leitura; depois, do evento OwnerStateChanged que o dono publica depois do commit e o Anexo consome (RN-ATT-28) — o dono nunca escreve nesta tabela. Evento com occurredAt anterior ao gravado é ignorado. 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 —, e porque é o occurredAt do Closed que conta a guarda (RN-ATT-31).
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: nulo é a resposta do CompanyOf dos donos que são do tenant e não de uma empresa — item, fornecedor, qualificação do fornecedor, proposta de alteração bancária e a instância de aprovação cujo alvo é um deles. 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?opcOpcional mesmo em anexo do portal: o fornecedor pode entrar no portal sem PortalUser cadastrado, pela forma de acesso que o Portal do Fornecedor define. Ver § Portal.
supplierIdGuid?condObrigatório quando uploadedByKind = PortalUser, resolvido pelo acesso do portal — autenticação e sessão são do Portal do Fornecedor — 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. Não é base de retenção: o piso retainUntil vai ao GrydFiles na reserva, contado da data da reserva (RN-ATT-08), e a guarda conta do fechamento do dono (RN-ATT-31).
retentionMode · retentionYearsenum · int?geradoCopiados do AttachmentType no upload e imutáveis. É o que o fechamento do dono usa para calcular o fim da guarda — mudar o tipo depois não alcança o anexo (RN-ATT-08).
guardUntildate?geradoO fim da guarda: 31/12 do ano do fechamento do dono + retentionYears, gravado quando o dono vai a Closed; em UntilReleased, a própria data do fechamento; nulo em Permanent e enquanto o dono não fecha. Volta a nulo se o dono sair de Closed antes de a referência ser liberada. Ver RN-ATT-31.
fileRetainUntildate?geradoO retainUntil que o GrydFiles confirmou para o arquivo deste anexo. Menor que guardUntil é extensão pendente, que o job repete (RN-ATT-35) — enquanto isso a referência mantida continua protegendo o arquivo.

Veredito e revogação

CampoTipoRegra
usableAtDateTime?geradoQuando o veredito Available foi aplicado a este anexo — na cópia, quando a referência dela foi criada. É o "utilizável" do contrato de veredito, e nunca volta a nulo: separa o arquivo que foi prova e depois o re-scan achou infectado (RN-ATT-37) do que nunca chegou a ser.
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 nas revogações de sistema: OwnerDiscarded, OrphanSweep, FileInfected e FileRejected.
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 · FileRejected · Erasure. Separa o que uma pessoa decidiu do que o sistema concluiu — e é o que a tela usa para escrever a frase certa. FileRejected é a reprovação do arquivo pelo GrydFiles, na confirmação ou no scan.
rejectionCodestring(80)?geradoSó com revocationSource = FileRejected: o código do catálogo de erros do GrydFiles que reprovou o arquivo — FILE_CONTENT_TYPE_NOT_ALLOWED_UNPROCESSABLE, FILE_SIZE_MISMATCH_UNPROCESSABLE, FILE_OBJECT_CHANGED_UNPROCESSABLE, FILE_UPLOAD_EXPIRED_CONFLICT. Copiado, não traduzido: é o que a tela mostra e o suporte procura.
fileReferenceStatusenumgeradoOnde está a FileReference deste anexo, sem consultar o outro módulo. Reserved — reserva do upload, ainda não confirmada — ou confirmada no GrydFiles sem a gravação chegar, que o handler e o job reconhecem (RN-ATT-36) —, fora da lista · Expected — confirmada com a referência esperada, à espera do veredito · CreatePending — cópia gravada, referência ainda não criada · Active — a referência existe e segura o arquivo · ReleasePending — o Nexio decidiu liberar e o GrydFiles ainda não confirmou · Released — liberada · NeverCreated — veredito negativo antes da liberação, ou reserva que o GrydFiles nunca confirmou. A referência sai em seis casos, e só neles: revogação com o dono em rascunho nunca submetido, descarte do dono, órfão cujo último estado era rascunho, fim da guarda sem guarda legal ativa, eliminação e — pela plataforma, não pelo Nexio — arquivo liberado que o re-scan achou infectado. Ver § Revogação.
fileSyncAttemptedAt · fileSyncErrorDateTime? · string(80)?geradoA última tentativa de uma escrita pendente no GrydFiles e o código do erro que ela recebeu. Nulos sem pendência. É o que a tela de saúde ordena por idade — pendência nunca some em silêncio (RN-ATT-35).
purgedAtDateTime?geradoSó na eliminação: quando o GrydFiles confirmou o expurgo. Nulo com revocationSource = Erasure é expurgo pendente, que o job de guarda e a repetição do ato voltam a pedir (RN-ATT-33).

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, alonga a retenção dos anexos novos ou desativa o que o produto entregou. A sobreposição só alonga: a retenção do global é o piso da linha do tenant (RN-ATT-25). Para cada code, vale a linha do tenant quando existe; senão, a global. A linha global nunca muda para ninguém; o release que a altere não alcança o rótulo nem a visibilidade de quem a sobrepôs, e, se alongar a retenção, a reserva do upload aplica a mais longa das duas. 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.

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.
isControlEvidenceboolobrigA marca de prova de controle: o arquivo sustenta um controle antifraude ou uma decisão de aprovação, e eliminá-lo a pedido de titular exige segunda pessoa (RN-ATT-38) — como exige o de tipo com retenção Permanent, marcado ou não. Na semente, verdadeira em BANK_OWNERSHIP_PROOF, SIGNED_CONTACT_CHANGE_REQUEST, IDENTITY_CORRECTION_PROOF e APPROVAL_EVIDENCE. Só liga: a sobreposição de um global não a desliga (RN-ATT-25), e desligá-la em tipo do tenant já usado é recusado (ATTACHMENT_TYPE_IN_USE_CONFLICT).
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?condObrigatório com RetainUntil, recusado nos outros modos. Conta duas vezes: na reserva do upload é o piso declarado ao GrydFiles, retainUntil = data da reserva + retentionYears; no fechamento do dono é a guarda, 31/12 do ano do fechamento + retentionYears — a leitura literal do art. 173, I do CTN, em que o prazo decadencial só começa a correr no primeiro dia do exercício seguinte. A semente usa 5. Ver § Retenção.
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.
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ó.

Semente global · tenantId nulo

CódigoRetençãoVisibilidadePor que existe
GENERALRetainUntil · 5 anosInternalO anexo solto, e o tipo mais escolhido. Guarda cinco anos depois do fechamento, como os de prova: o que alguém guardou como "Geral" costuma ser justamente o que a auditoria pede
TECHNICAL_SPECRetainUntil · 5 anosSupplierDesenho, 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 anosInternalDANFE e o documento fiscal que só chega em PDF — nota de serviço municipal, cupom. O XML da NF-e não é anexo: é o documento fiscal, com chave de acesso, emitente e protocolo, e é entidade da Invoice pela decisão 1. Boleto e comprovante de pagamento não entram — pagamento é do ERP. Cinco anos é o prazo de decadência do CTN, contado do fechamento. Guarda legal não é do tipo: é ato por caso (RN-ATT-32)
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, e por isso é prova de controle (isControlEvidence)
BANK_OWNERSHIP_PROOFRetainUntil · 5 anosInternalO comprovante de titularidade da conta bancária — documento do banco que mostra o titular e o documento dele. Só em BankAccountChangeProposal, com allowsSupplierVisibility = false e propagates = false. É prova de controle antifraude (isControlEvidence) e guarda cinco anos do fechamento da proposta. A comparação do titular com o fornecedor, e a recusa de titular diferente, são do Fornecedor
SIGNED_CONTACT_CHANGE_REQUESTRetainUntil · 5 anosInternalO pedido do fornecedor para trocar e-mail ou telefone de contato de aviso ou da cadeia de reserva quando não há telefone confiável para o callback. Só em BankAccountChangeProposal de contato, com allowsSupplierVisibility = false, propagates = false e isControlEvidence — mesma retenção e mesma guarda do comprovante de titularidade. Quando ele é exigido é do Fornecedor; qual documento serve é da norma da empresa. O Nexio exige que ele exista, seja do tipo declarado e tenha veredito liberado, e grava quem anexou, quem decidiu, data e hora — e não confere assinatura digital, selo de cartório, apostila nem poderes do signatário
IDENTITY_CORRECTION_PROOFRetainUntil · 5 anosInternalO documento comprobatório da correção de identidade de fornecedor já aprovado — CPF, CPF do titular ou natureza que nasceram errados. Só em Supplier, com allowsSupplierVisibility = false, propagates = false e isControlEvidence — mesma retenção e mesma guarda do comprovante de titularidade. Existe pelo mesmo motivo do pedido assinado: sem um tipo marcado como prova de controle, o documento que sustenta a correção cairia em GENERAL e uma pessoa sozinha poderia eliminá-lo. Quando ele é exigido é do Fornecedor; qual documento serve é da norma da empresa, e o Nexio exige a existência, o tipo e o veredito, e grava o rastro
Onze tipos, e nenhum deles é "Outros". Nenhum semeado é UntilReleased: o modo fica para o tipo do tenant que queira anexo descartável, liberado no fechamento do documento. A semente é global; o cliente que precisar de "Laudo de Vibração" cria o seu, e não migra nada.

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 doze 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: doze 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, e só dos módulos presentes. Cada módulo dono registra, na inicialização, os valores de ownerType que são dele e o descritor de cada um. Todo valor registrado precisa ser valor do enum e ter um descritor completo — as doze respostas, nenhuma "não implementada" —, e o que falta derruba a aplicação no startup, não em produção às três da manhã. Valor do enum cujo módulo ainda não existe não é exigido: a base sobe com os donos que tem, sem stub, e o valor sem módulo não aceita anexo. É verificável: percorre-se o que os módulos registraram e pergunta-se ao contêiner (RN-ATT-03).

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. Draft é só o documento que nunca foi submetido — o que voltou para edição depois de passar por alguém é InProgress, porque alguém já pode ter decidido olhando para os anexos dele. Closed é o documento que não aceita mais movimento; se o dono tiver saída desse estado, a resposta volta a InProgress. Decide se a revogação libera a referência e de quando conta o fim da guarda (RN-ATT-17, RN-ATT-31)
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
CanRead(user, ownerId)Delega ao dono a autorização de leitura. Quem pode ler o documento no nível de detalhe que o anexo revela pode ler os anexos dele — read:attachments e o escopo por empresa são necessários e não bastam, como na escrita. Existe porque o arquivo carrega o que a tela do dono esconde: a planilha de premissas carrega os valores do orçamento, o comprovante carrega a conta sem máscara. Exigida em toda leitura (RN-ATT-34); as respostas estão na matriz de leitura
OpenApprovalOf(ownerId)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 outras perguntas sabe 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
PendingControlOf(ownerId)Nulo, ou a decisão de controle pendente fora da instância aberta daquele documento: devolução à espera de reenvio, lote de carga inicial por confirmar, confirmação de segunda pessoa, carência de alteração já aplicada, contestação sem desfecho, correção de identidade em curso. Existe porque a eliminação a pedido de titular espera toda decisão de controle terminar, e OpenApprovalOf só enxerga a instância do motor — o comprovante de uma carga inicial ainda por confirmar não tem instância nenhuma (RN-ATT-33, RN-ATT-39). A resposta bloqueia; nunca é mostrada a quem pede a eliminação
ResponsibleOf(ownerId)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
As doze 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.

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

Todo agregado que aceita anexo publica, depois do commit, OwnerDiscarded(ownerType, ownerId) quando um rascunho é descartado. O handler — idempotente, com o tenant do evento restaurado e falha fechada sem ele — revoga os anexos daquele dono com revocationSource = OwnerDiscarded e libera as FileReference depois do próprio commit (RN-ATT-35), porque descarte de rascunho é exatamente o caso em que o arquivo não precisa sobreviver. Anexo ainda sem veredito tem a referência esperada cancelada, e a liberação que chegar depois encontra o anexo revogado e solta a referência. É 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 registrado e pergunta ao descritor se o dono existe. Anexo de ownerType sem módulo presente não é varrido nem revogado — é contado no relatório; ausência de módulo não é ausência de dono. Ausente, o anexo é revogado com revocationSource = OrphanSweep; a referência é liberada apenas se lastKnownOwnerState = Draft — o último estado que o Anexo conheceu pelo evento do dono (RN-ATT-28), porque a essa altura não há mais a quem perguntar. O job roda por tenant, com o tenantId no payload. 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

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, que é onde o desenho chega a quem precisa cotá-lo
QuotationRequestPurchaseOrderEdital, planilha modelo, condições. Tipicamente Supplier
QuotationResponsePurchaseOrderOnde o anexo do portal cai. supplierId obrigatório, nasce Internal e nunca vira Supplier: seria a proposta de um concorrente legível aos outros (RN-ATT-12)
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 e evidência de divergência. O XML da NF-e é entidade da própria Invoice — tem chave de acesso, emitente e protocolo para digitar e filtrar —, e boleto e comprovante de pagamento não são do Nexio. Retenção fiscal de cinco anos, contada do fechamento
ContractPurchaseOrderO contrato assinado é Permanent. Versionamento é do Contract, não daqui
QuotationRequestItemPurchaseOrderItemA linha da cotação, que é onde o desenho por item de fato serve — o fornecedor cota aquele item
ApprovalRequest—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. É o uso principal do tipo semeado APPROVAL_EVIDENCE
Supplier—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. E o documento comprobatório da correção de identidade (IDENTITY_CORRECTION_PROOF), que é prova de controle: o ato grava o anexo, quem anexou, quem decidiu, data e hora
Item—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
SupplierQualification—A evidência de um ciclo de homologação: questionário respondido, relatório de visita, plano de ação de ressalva. Cada ciclo é um registro próprio — a prova de que o fornecedor foi homologado neste ciclo não pode depender do ciclo seguinte. Certidão continua sendo SupplierDocument
Budget—Planilha de premissas e memória de cálculo do exercício. Suplementação, remanejamento e redução têm dono próprio — a ata e o ofício que os justificam são do BudgetChangeRequest
BankAccountChangeProposal—O comprovante de titularidade da conta nova (BANK_OWNERSHIP_PROOF), anexado com a proposta ainda em rascunho — e, na troca de contato sem telefone confiável, o pedido assinado do fornecedor (SIGNED_CONTACT_CHANGE_REQUEST), do mesmo jeito: a submissão exige o veredito Available, e depois dela a proposta não aceita anexo nem revogação; na carga inicial, a proposta do lote aceita anexo até a confirmação do lote, que pergunta pelos comprovantes (RN-ATT-19). É 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 dois são prova de controle: enquanto a janela de contestação do fornecedor não fecha, eliminá-los apagaria a prova da fraude que ele ainda vai contestar (RN-ATT-38)
BudgetChangeRequest—A justificativa da suplementação, do remanejamento ou da redução: ata, ofício, planilha. É a solicitação que passa pelo motor e muda a verba; o Budget guarda a prova do exercício aprovado
Dezessete valores, persistidos como texto — convenção declarada do Nexio para enum —, de modo que acrescentar valor não é migração. Sete têm spec dona e respostas declaradas abaixo: ApprovalRequest, Supplier, SupplierQualification, BankAccountChangeProposal, Item, Budget e BudgetChangeRequest. Os outros dez são dos documentos ainda não especificados; cada spec que nascer declara aqui as doze respostas do seu. Dono sem spec não entra "por via das dúvidas" — QuotationResponseItem espera a Cotação.

Por que os documentos ainda não especificados estão na lista. Não é custo de migração — o enum é texto. É para que a propagação e os tipos permitidos sejam escritos desde já com o nome certo. Estar na lista não é ser dono: até a spec do documento existir e o módulo registrar o descritor, o valor não aceita anexo (ATTACHMENT_OWNER_TYPE_UNKNOWN).

As doze respostas dos donos com spec

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.

PerguntaQuotationRequestItem · préviaApprovalRequestSupplierItemBudgetBankAccountChangeProposalSupplierQualificationBudgetChangeRequest
CompanyOfA empresa da cotação paiCondicional: a empresa do documento alvo quando ele tem uma (requisição, pedido, orçamento, solicitação de revisão); nula quando o alvo é do tenant (fornecedor, qualificação, proposta bancária, item)Nula — cadastro do tenantNula — vocabulário do tenantA empresa do orçamentoNula — a proposta é do fornecedor, e fornecedor é do tenantNula — a homologação é do fornecedor, que é do tenantA empresa do orçamento que ela altera
StateOfSegue a cotação pai: Draft enquanto a RFQ nunca foi enviada, InProgress depois de enviada, Closed na adjudicação ou no cancelamentoInProgress em InApproval e Returned; Closed em Approved, Rejected e Cancelled; a reavaliação que reabre Approved volta a InProgress. Nunca Draft — a instância nasce submetidaDraft no cadastro nunca submetido — o único que aceita DELETE; InProgress em todo o resto, ativo, bloqueado ou inativo. Nunca Closed: cadastro não fecha e a inativação tem volta, então o anexo do cadastro vive enquanto o fornecedor existeDraft no item nunca submetido; InProgress em PendingApproval, Approved, Rejected e no rascunho que voltou depois de rejeitado, em qualquer fase do ciclo de vida. Nunca Closed — fase é configuração do tenant e tem voltaDraft antes da primeira submissão; InProgress em InReview, Approved, Closed (ainda aceita realizado tardio) e no Draft que voltou por devolução; Closed em Archived. Nunca MissingDraft até a submissão — é quando recebe o comprovante; InProgress da submissão (Open) até o desfecho — o fim da instância BANK_ACCOUNT_CHANGE ou, em ChainContact confirmado por segunda pessoa, sem instância, a confirmação ou a recusa dela; Closed depois: aplicada, rejeitada, recusada ou cancelada, inclusive por contestação. Na carga inicial a proposta nasce Open com o envio do lote: InProgress até o lote ser confirmado ou cancelado, e Closed depoisDraft antes de submeter; InProgress com a instância SUPPLIER aberta ou devolvida; Closed quando a instância termina. O ciclo seguinte é outro registroDraft nunca submetida; InProgress em PendingApproval e devolvida; Closed em Approved, Rejected e Cancelled
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 propostaO fornecedor mais a data do ciclo — razão social · homologação de 03/2026O número da solicitação mais o exercício
AllowedTypesTECHNICAL_SPEC · GENERALAPPROVAL_EVIDENCE · GENERALIDENTITY_CORRECTION_PROOF · GENERAL na sementeTECHNICAL_SPEC · GENERALAPPROVAL_EVIDENCE · GENERALPor changeKind: BankAccount → BANK_OWNERSHIP_PROOF · GENERAL; NoticeContact e ChainContact → SIGNED_CONTACT_CHANGE_REQUEST · 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 olhaNão — parecer de homologação é internoNão — orçamento não sai da empresa
PropagatesToPurchaseOrderItem———————
MaxCount · MaxTotalBytes5 · 25 MB10 · 50 MB20 · 100 MB10 · 50 MB10 · 50 MB3 · 15 MB20 · 100 MB10 · 50 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ícioSó em Draft, pelo proponente — e, na carga inicial, com a proposta Open e o lote ainda por confirmar, por quem enviou o lote; depois da submissão ou da confirmação, ninguém: a proposta é imutável, e o dataHash cobre o comprovanteQuem conduz a qualificação, enquanto o ciclo não fechaQuem pode editar a solicitação
CanReadDeclarado por dono na matriz de leitura, em § Visibilidade — uma resposta por linha, com o caso que ela fecha
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 do processo BUDGET daquele exercício, ou nuloA instância aberta da alteração bancária, ou nuloA instância aberta do processo SUPPLIER, ou nuloA instância aberta do processo BUDGET_CHANGE, ou nulo
PendingControlOfDelega à cotação paiReturned, à espera de reenvio; nulo no resto — em InApproval responde o OpenApprovalOf reflexivopendingIdentityCorrection preenchido — correção de identidade em curso, com ou sem a instância aberta —, ou alerta de conformidade ErpBankDivergence em Open; nulo no restoNulo — a decisão de controle do item é a instância, que o OpenApprovalOf já respondeO Draft que voltou por devolução, à espera de reenvio; nulo no restoProposta da carga inicial em Open, com o lote por confirmar; ChainContact em Open à espera da segunda pessoa; alteração aplicada ainda em carência (usableFrom no futuro — conta, ou contato pela instância com documento comprobatório); janela de contestação ainda aberta — o token de contestação entregue ao fornecedor continua válido, inclusive depois de usableFrom; contestação registrada ainda sem desfecho no Fornecedor. Nulo no restoDevolvida, à espera de reenvio; nulo no restoDevolvida, à espera de reenvio; nulo no resto — em PendingApproval responde o OpenApprovalOf
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 proponente — na carga inicial, quem enviou o lote. O portal não cria propostaQuem submeteu a qualificaçãoQuem criou a solicitação
Oito colunas, doze respostas cada: os sete donos com spec e a resposta prévia da linha de cotação, que a spec da Cotação confere. StateOf segue o status real de cada spec dona; onde o dono não fecha, está dito.

Sobre o AllowedTypes: na semente, escolher o tipo não muda quanto tempo a prova dura. Os onze tipos semeados guardam cinco anos depois do fechamento do dono — GENERAL inclusive —, menos SIGNED_CONTRACT, que é Permanent. O tipo decide classificação, visibilidade e propagação; retenção diferente só existe em tipo do tenant, e a sobreposição de um global só alonga (RN-ATT-25). Budget e BudgetChangeRequest aceitam APPROVAL_EVIDENCE para que a planilha de premissas do exercício e a ata da revisão apareçam classificadas como prova quando a auditoria as pedir; o comprovante de titularidade e o pedido assinado de troca de contato têm tipo próprio, BANK_OWNERSHIP_PROOF e SIGNED_CONTACT_CHANGE_REQUEST, porque é o tipo que a submissão da proposta confere.

E onde a semente não chega, ela não deve chegar. Os onze 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

As duas são prova 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 e a planilha que justificam uma suplementação são parte da solicitação de 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 BudgetChangeRequest e são anexadas com o dono em rascunho. A proposta bancária nasce em Draft, recebe o comprovante (BANK_OWNERSHIP_PROOF) e só é submetida quando ele tem veredito Available — sem ele o Fornecedor recusa a submissão, e por baixo vale a decisão 8; na carga inicial, o comprovante é anexado à proposta que o lote criou — em Open, aceitando anexo até a confirmação —, e a confirmação do lote pergunta ao Anexo pelos comprovantes como a submissão pergunta, e o Fornecedor decide: Failed, Infected, revogado ou eliminado é falta de evidência, com o código dele; sem falta, comprovante ainda em varredura (Pending) recusa com ATTACHMENT_SCAN_PENDING_CONFLICT (RN-ATT-19; RN-FOR-31 do Fornecedor). Submetida — ou confirmado o lote —, a proposta não aceita anexo novo nem revogação — nem a confirmação de reserva feita antes (RN-ATT-36): o aprovador decide sobre a prova que o proponente apresentou, e ela não muda debaixo da decisão.

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

A decisão 1 decide, e a spec de Fornecedor diz o mesmo. 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

Quatro donos com spec não têm empresa: Item e Supplier são vocabulário e cadastro do tenant, não da empresa, e SupplierQualification e BankAccountChangeProposal pendem 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 as empresas do escopo — e quem não tem empresa nenhuma no escopo não a vê. 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, orçamento e solicitação de revisão têm empresa; fornecedor, qualificação, proposta bancária e item não têm. O mesmo ownerType devolve Guid nos processos cujo alvo tem empresa e nulo nos outros, e isso é correto — o descritor pergunta ao alvo, não a uma tabela de de-para.

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

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.

A regra que não pode falhar

Visibilidade

Dois valores, Internal e Supplier, desde a primeira versão. A razão de não adiar é o modo de falha: adiar significa que, no dia em que a visibilidade for criada, existirá um acervo de anexos sem classificação, e alguém terá que decidir em massa o que vai ao fornecedor. O erro de uma migração dessas é vazamento, e vazamento não tem rollback.

Quatro portas, e todas precisam estar abertas

Marcar um anexo como Supplier exige, ao mesmo tempo: o AttachmentType com allowsSupplierVisibility = true; o descritor do dono respondendo que aquele documento pode expor anexo; o usuário com a permissão share-with-supplier:attachments; e o anexo em estado editável. Falhar qualquer uma é 422 ou 409, nunca uma exposição parcial. Quatro portas parece excessivo até a primeira vez que a planilha de custo interno vaza numa RFQ.

O GrydFiles não participa desta decisão — e é bom que não participe

A permissão read:files libera pedir uma URL de download: é permissão da rota HTTP do GrydFiles, que os hosts do Nexio não expõem — em processo a IFileService não confere permissão de módulo e o GrydFiles não define identidade de serviço —, e não vai a papel de tenant — requisito do catálogo de permissões da plataforma, na demanda; o que a asserção 8 da Arquitetura verifica é que os hosts do Nexio não expõem /files/*. Quem decide se este usuário pode ver este documento é o Nexio, antes de chamar o GrydFiles. O módulo de arquivo emitir URL curta para quem já foi autorizado é a fronteira correta: ele não sabe o que é fornecedor, cotação nem concorrente, e ensiná-lo isso seria mover regra de negócio para a camada errada.

A consequência prática é uma frase que vale para todo o produto: uma URL assinada emitida é uma autorização já concedida. O TTL curto do perfil é o que limita o dano de um link repassado, e é por isso que ele é curto.

Rebaixar é livre; promover é decisão

Passar de Supplier para Internal não precisa de permissão especial — reduzir exposição nunca deveria ter atrito. O caminho contrário é o controlado. E há um ponto sem volta: depois que o documento foi enviado ao fornecedor, a visibilidade fica travada (ATTACHMENT_VISIBILITY_LOCKED_CONFLICT). Rebaixar depois do envio seria fingir que o fornecedor não viu; o caminho honesto é revogar, o que retira o anexo da tela do fornecedor daqui para a frente e deixa registrado que ele existiu.

Isolamento entre fornecedores não é visibilidade

Numa cotação com cinco concorrentes, nenhum pode ver o anexo do outro — e isso não se resolve com visibility. Resolve-se com autoria: o portal devolve ao fornecedor autenticado os anexos que ele mesmo subiu nas respostas dele (uploadedByKind = PortalUser e o supplierId dele), em qualquer visibilidade, mais os anexos Supplier do próprio QuotationRequest, que são para todos por definição. O que o comprador acrescentou à resposta de um fornecedor — mesmo com o supplierId preenchido — não volta ao portal, e anexo de resposta nunca vira Supplier. É uma comparação de coluna, não uma regra especial — e foi um dos três ganhos que motivaram a decisão 9.

A matriz de leitura · CanRead(user, ownerId)

Escopo por empresa e read:attachments dizem se a pessoa pode estar ali; o dono diz se ela pode ver o que o arquivo mostra. O arquivo não se recorta — a planilha de premissas traz todos os valores, o comprovante traz a conta inteira —, então a condição de leitura é a do nível de detalhe mais alto que o dono protege, não a da tela resumida. Toda leitura pergunta (RN-ATT-34); quem não passa recebe 404, e a listagem não traz a linha — nem o nome do arquivo, que também é dado. Todo dono que passa por aprovação inclui quem tem tarefa na instância dele, pendente ou decidida: é a evidência que sustenta a decisão.

Uma resposta por tipo de dono. Os documentos ainda não especificados têm a regra mínima declarada; a spec de cada um confere e detalha, sem afrouxar.
Tipo de donoLê o anexo, além de escopo e read:attachmentsNão lê — e o que isso fecha
Budgetread:budget-amounts-all, ou tarefa na instância BUDGET daquele exercícioQuem tem só read:budgets, read:budget-states ou read:budget-amounts com recorte. A planilha de premissas não vaza valor para quem a tela do orçamento não mostra valor
BudgetChangeRequestread:budget-amounts-all; read:budget-amounts cujo recorte cubra todas as linhas da solicitação; quem a criou; tarefa na instância BUDGET_CHANGERecorte que cobre só parte das linhas: a justificativa de um remanejamento fala das duas pontas
BankAccountChangeProposalQuem tem read-unmasked:supplier-banks; o proponente, com a proposta em Draft ou Open — na carga inicial, quem enviou o lote —; quem tem tarefa ativa ou já decidida na instância daquela proposta; e, na carga inicial, para as propostas desse lote — antes da confirmação, o remetente e quem tem approve:supplier-banks com identidade verificada e não é o remetente — nem pelo usuário, nem pelo CPF; depois da confirmação, só o confirmador. É a lista da RN-FOR-36 do Fornecedor, copiada, e vale igual para os dois anexos da proposta — o comprovante de titularidade (BANK_OWNERSHIP_PROOF) e o pedido assinado (SIGNED_CONTACT_CHANGE_REQUEST)Quem tem read:suppliers, ou read:supplier-banks com números mascarados; o proponente depois que a proposta termina; antes da confirmação do lote, quem tem approve:supplier-banks sem identidade verificada, e quem tem o CPF do remetente com outro usuário; depois dela, quem não confirmou aquele lote — o remetente inclusive. O comprovante de titularidade é do tenant (RN-ATT-30) e passa pelo escopo — não passa por aqui
Supplier · SupplierQualificationread:suppliers; na qualificação, também tarefa na instância SUPPLIERQuem não abre o cadastro do fornecedor
Itemread:catalogQuem não abre o catálogo
ApprovalRequestQuem lê a instância — requisitante, quem tem tarefa nela, quem tem audit:approvals — e passa no CanRead do documento alvoQuem lê a fila mas não o alvo: o parecer anexado à aprovação de um orçamento revela o orçamento
QuotationResponseNinguém do comprador antes do envio da proposta; depois dele, quem lê a cotação — e, em cotação com abertura sigilosa, só depois da abertura. No portal, só o que o próprio fornecedor subiu (RN-ATT-12)O comprador que conduz a cotação, antes do envio ou da abertura. O anexo da proposta não quebra o sigilo que o preço tem. Regra mínima para a spec da Cotação
QuotationRequest · QuotationRequestItemQuem lê a cotaçãoRegra mínima para a spec da Cotação
Requisition · RequisitionItem · PurchaseOrder · PurchaseOrderItem · Receipt · Invoice · ContractQuem lê o documento, no nível de detalhe mais alto que a spec dele protegerRegra mínima; cada spec declara a sua linha aqui ao nascer

Os três casos que a matriz fecha, conferidos. (1) Pessoa com read:attachments e read:budgets, sem read:budget-amounts-all, pede a URL da planilha de premissas do orçamento: o escopo passa, o CanRead do Budget não — 404, e a linha some da lista. (2) Pessoa de qualquer empresa com read:attachments abre o comprovante bancário de um fornecedor: companyId nulo deixa passar o escopo, e só lê quem tem read-unmasked:supplier-banks, o proponente enquanto a proposta não termina, quem tem ou teve tarefa na instância, ou, na carga inicial e para aquele lote, quem tem approve:supplier-banks com identidade verificada e não é o remetente antes da confirmação e o confirmador depois — os demais recebem 404. (3) Comprador abre a resposta de um fornecedor antes do envio, ou antes da abertura numa cotação sigilosa: o CanRead do QuotationResponse é falso para todo usuário interno, e nem a cadeia de propagação mostra o elo.

Quem vê o quê
QuemInternalSupplierRevogado
Usuário com escopo na empresa do dono e CanRead do donoSim, com read:attachmentsSimSó na aba de histórico, com marca
Usuário com escopo, sem CanRead do dono404404404
Usuário fora do escopo da empresa404404404
Aprovador na filaSim — é a evidência que sustenta a decisãoSimNão. Ver § Revogação
Fornecedor no portal, o que ele mesmo subiuSim — nasce Internal e continua deleNão se aplica: anexo de resposta nunca vira SupplierNão
Fornecedor no portal, todo o restoNão — inclusive o que o comprador anexou à resposta deleSó os do documento pai, nunca o de outro fornecedorNão
Auditoria interna, com CanRead do donoSimSimSim, e é o ponto — com quem revogou, quando e por quê

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, criada depois do commit (RN-ATT-35).

O que ela não faz

Não copia bytes — é o mesmo objeto. Não acontece sozinha. Não atravessa empresa sem escopo nas duas. 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.

Entre empresas, só com escopo nas duas

A compra centralizada pode relacionar documentos de empresas diferentes do mesmo tenant; como a demanda de várias empresas se consolida é decisão da spec da Cotação. A propagação acompanha: o destino pode ser de outra empresa quando quem propaga lê a origem (escopo e CanRead do dono de origem) e escreve no destino (escopo e CanWrite do dono de destino), e o ownerType do destino está no PropagatesTo da origem. Faltando qualquer um, o documento responde 404, como todo documento fora de escopo. A cópia grava o companyId do destino e herda a visibilidade; a tela mostra, antes da confirmação, que o arquivo passa a outra empresa (RN-ATT-14).

Não é rateio. Cada documento continua de uma empresa só, e cada cópia responde ao escopo da empresa dela: quem enxerga só a empresa de destino vê a cópia, e não a origem.

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 nunca submetido (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 fornecedorMantida até o fim da guardaContinua preso. A referência sai no fim da guarda contada do fechamento (RN-ATT-31), e não sai sob guarda legal
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 Infected ou reprovado, antes de liberadoRevogado com revocationSource = FileInfected ou FileRejected. A tela diz o que houveNunca existiu — nasceria na liberação, que não aconteceu. Nada a liberarO infectado vai para a quarentena do GrydFiles; o reprovado é apagado, com lápide. Decisão dele
Arquivo liberado que o re-scan achou infectadoRevogado com revocationSource = FileInfected, em qualquer estado do dono, com o aviso que o estado pede (RN-ATT-37)Liberada pelo GrydFiles, em todos os anexos do arquivo, na transação do re-scanVai para a quarentena, cujo expurgo respeita o retainUntil e o legalHold do arquivo. Não é eliminação
Eliminação a pedido de titularRevogado com revocationSource = Erasure — ele e todas as cópias do mesmo arquivo —, com o dono em qualquer estado menos com aprovação em curso ou decisão de controle pendente, que bloqueiam a execução (RN-ATT-33). Prova de controle, arquivo Permanent ou anexo ainda sob guarda pede segunda pessoa (RN-ATT-38); o GrydFiles é consultado antes de gravar (RN-ATT-39)Liberada em todos elesExpurgo manual pedido na execução, como eliminação, com lápide (RN-ATT-33)

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. Por isso a referência de documento que já passou por alguém só sai no fim da guarda, contado do fechamento — ou pela eliminação a pedido de titular, que é outro ato, com permissão própria.

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 anexo de documento já submetido e ainda em aprovação é permitido quando o CanWrite do dono deixa — a proposta bancária submetida não deixa —, com motivo, e notifica os aprovadores pendentes. Não invalida a aprovação — invalidar por conta própria seria o módulo de anexo tomando uma decisão do motor de aprovação. O que ele faz é avisar quem precisa saber.

A guarda: piso no upload, fim contado do fechamento do dono

Na reserva do upload o Nexio resolve retentionMode e retainUntil do AttachmentType, envia ao GrydFiles pela IFileService e congela no anexo o modo e os anos. A data enviada é só o piso: o GrydFiles não expurga arquivo com referência ativa nem com retainUntil no futuro, e a referência é do Nexio.

O fim da guarda conta do fechamento do dono (RN-ATT-31). Quando o dono vai a Closed — o occurredAt do evento que o dono publica (RN-ATT-28) —, cada anexo dele com referência mantida, ativo ou revogado, e o que o re-scan tornou infectado depois de liberado, ganha guardUntil = 31/12 do ano do fechamento + retentionYears, e o retainUntil do arquivo é estendido até essa data no GrydFiles, depois do commit (RN-ATT-35) — e só anda para frente. A razão é aritmética e é fiscal: o prazo decadencial do art. 173, I do CTN começa no primeiro dia do exercício seguinte, e contar do upload deixaria o arquivo até dois anos curto num processo longo. O job diário attachment-guard-release libera a referência dos anexos cujo guardUntil passou, que não estão sob guarda legal e cujo dono, perguntado na hora, continua fechado; dali em diante o expurgo é regra do GrydFiles. Em UntilReleased — só em tipo do tenant — o fim da guarda é a data do fechamento; em Permanent não existe.

Dono que sai de Closed — a instância aprovada que a reavaliação reabre — zera o guardUntil dos anexos ainda referenciados, e o próximo fechamento recalcula. Dono que nunca fecha — cadastro de fornecedor e de item — mantém os anexos enquanto existir. Errar para mais custa armazenamento; errar para menos custa a prova. Encurtar guarda não é a rota de retenção, que recusa (FILE_RETENTION_SHORTENED_UNPROCESSABLE): é a eliminação a pedido de titular.

O arquivo compartilhado entre anexos, e o que isso muda

Dois anexos podem apontar para o mesmo storedFileId — por propagação. Cada cópia tem a sua referência e o seu guardUntil, contado do fechamento do dono dela, e as guardas podem divergir: o mesmo PDF numa requisição que fechou em 2026 e num pedido que fechou em 2027. Vale a mais longa, sem regra nova: o arquivo vive enquanto alguma referência existir, e retainUntil só anda para frente.

Guarda legal é ato por caso, nunca propriedade do tipo

Litígio, fiscalização e auditoria externa congelam um caso, não um tipo de documento. Guarda legal por tipo transformaria cinco anos em "nunca" para todo documento fiscal de todo cliente, e ninguém conseguiria encerrá-la. Por isso o AttachmentType não tem legalHold: quem tem create:attachment-legal-holds coloca a guarda sobre um anexo, ou sobre todos os anexos de um dono, com a referência do caso e o motivo. Cada colocação é uma linha de AttachmentLegalHold; retirar é outro ato, também com motivo, que encerra só aquela linha. Dois casos sobre o mesmo anexo são duas linhas, e encerrar um não solta o outro (RN-ATT-32). Colocar e retirar tomam a mesma trava do arquivo que a execução da eliminação toma — é o que impede a guarda de nascer no meio de uma eliminação, e a eliminação de correr por cima de uma guarda recém-posta (RN-ATT-39).

O efeito é a referência que não sai — e o legalHold do arquivo ligado. Com guarda ativa, o fim da guarda não libera a referência, a revogação em rascunho não a libera, e a eliminação é recusada (ATTACHMENT_UNDER_LEGAL_HOLD_CONFLICT). A referência mantida não basta sozinha: o re-scan que acha vírus num arquivo liberado libera todas as referências dele, e a quarentena tem prazo próprio. Por isso a primeira guarda ativa sobre qualquer anexo de um arquivo liga também o legalHold do StoredFile, com o caseReference como motivo e quem colocou como autor, e a retirada da última guarda ativa sobre os anexos daquele arquivo pede o desligamento — as duas chamadas depois do commit, idempotentes, com a pendência à vista em fileHoldAppliedAt e fileHoldReleasedAt (RN-ATT-35). O sinal do arquivo é um só; quem conta os casos é o Nexio. Desligar a guarda que o Nexio ligou é pedido do mesmo escopo, com quem retirou a guarda como autor declarado — também na repetição pelo job, que não tem usuário —, e não pede admin:system: é o requisito da RN-GF-27 do GrydFiles, registrado na demanda de plataforma. Enquanto ele não estiver entregue, a pendência fica aberta e o arquivo, preso — o erro possível é para mais. Colocar e retirar são atos de controle: registro isolado por tenant, gravado na transação do ato, e os eventos attachment-legal-hold.placed e attachment-legal-hold.lifted depois do commit. A linha da guarda não traz nome de arquivo nem conteúdo — gerir guarda não dá leitura de anexo —, e colocar, retirar e listar valem só no escopo da empresa do dono, com 404 fora dele.

Eliminação a pedido de titular (LGPD)

A LGPD preserva a guarda exigida por obrigação legal (art. 16) e manda eliminar o dado tratado sem base. O produto entrega as duas coisas. A resposta — quais anexos, em quais documentos, sob qual guarda — sai do índice por dono e por fornecedor. O ato é POST /attachments/{id}/erase, com permissão própria, erase:attachments: registra o pedido do titular e o executa — na hora, ou pela mão de uma segunda pessoa quando o arquivo é prova de controle ou ainda está sob guarda (RN-ATT-38) —, e é o único caminho que libera a referência de documento em curso ou fechado antes do fim da guarda (RN-ATT-33).

O ato exige a referência do pedido do titular, a data-limite de resposta a ele (dueAt) e um comentário com a avaliação de que não há base legal de guarda — a decisão jurídica é de quem tem a permissão, e fica registrada; o motivo é sempre DADO_PESSOAL_INDEVIDO. No registro, e de novo na execução, três condições recusam, e na recusa nada muda: algum dos anexos do tenant que apontam para o mesmo arquivo está sob guarda legal (ATTACHMENT_UNDER_LEGAL_HOLD_CONFLICT); o arquivo tem referência ativa de outro escopo além de nexio.attachment (ATTACHMENT_ERASURE_FILE_REFERENCED_CONFLICT — o documento de cadastro que aproveitou o arquivo é tratado no módulo dele; não há caminho automático entre consumidores, e na v1 o caso não ocorre, porque o Nexio é o único consumidor de escopo de anexo do GrydFiles — RN-GF-25); ou o GrydFiles não aceitaria o expurgo — guarda de qualquer detentor, inclusive a posta pela rota dele, sem escopo; estado do arquivo; e, na execução, em Permanent, dois autores distintos — no registro a consulta roda em modo registro, com um autor só —, conferido pela consulta de pré-condição em processo — que nomeia todos os pares do escopo nexio.attachment daquele arquivo, o original e as cópias — antes de qualquer gravação (RN-ATT-39). Três condições bloqueiam, e nenhuma delas recusa o registro. Duas são do dono: algum desses donos tem aprovação em curso (OpenApprovalOf diferente de nulo) ou decisão de controle pendente fora da instância (PendingControlOf diferente de nulo — lote de carga inicial por confirmar, contato à espera da segunda pessoa, carência, janela de contestação ainda aberta ou contestação sem desfecho, alerta de divergência com o ERP em aberto, devolução à espera de reenvio, correção de identidade em curso), qualquer que seja o CanWrite. A terceira é de alcance: quem registra tem escopo e CanRead no anexo em que agiu — sem isso a rota responde 404 —, mas não em todos os donos dos outros anexos do mesmo arquivo. Nesse caso o pedido também é registrado, bloqueado. Recusar era responder ao exercício de um direito com silêncio: nada gravado, nenhum prazo correndo, ninguém avisado, nenhuma saída declarada. Registrado, o pedido nasce Requested, o dueAt corre desde o registro, e o bloqueio nomeia o alcance que falta — as empresas fora do escopo e os tipos de dono cuja leitura foi negada, nunca o número nem o nome do documento —, para quem for resolver saber o que conceder. O aviso sai a quem registrou e ao titular, e o administrador do tenant é escalado no mesmo ato, porque conceder escopo é dele: enquanto ninguém tiver o alcance inteiro, não há quem execute. A quem registrou a resposta diz apenas que o pedido ficou bloqueado por alcance incompleto — que o arquivo tem anexo fora do alcance dele é o mínimo para ele não repetir o pedido, e é tudo o que ele fica sabendo. Na execução as três recusam: ATTACHMENT_ERASURE_APPROVAL_IN_PROGRESS_CONFLICT, ATTACHMENT_ERASURE_CONTROL_DECISION_PENDING_CONFLICT e ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN — quem executa precisa do alcance inteiro, e é aí que a eliminação parcial é impedida: ela não elimina nada, porque o arquivo continuaria preso pela cópia que ficou. Eliminar a prova debaixo de uma decisão aberta tiraria de quem decide o que ele precisa olhar; a eliminação espera a decisão terminar, e o desfecho que depende da prova é recusado do lado do dono — o Fornecedor, para o comprovante bancário. E revogar, trocar o nome e publicar a eliminação de um arquivo que o GrydFiles vai recusar daria o dado por eliminado com os bytes guardados para sempre — por isso a consulta vem antes. Executada, numa transação do Nexio, com o arquivo travado — a mesma trava que a cópia e a guarda legal tomam — e cada dono travado como a submissão dele o trava (RN-ATT-39), todos esses anexos — o original e as cópias — são revogados com revocationSource = Erasure e marcados ReleasePending, originalFileName e description dão lugar a texto neutro — nome de arquivo também é dado pessoal — e o ato de controle é registrado com quem registrou e quem executou. Depois do commit (RN-ATT-35) sai attachment-erasure-request.executed — o ato foi executado, a eliminação não está concluída — e o job de guarda, sem usuário no contexto, libera as referências e pede o expurgo ao GrydFiles como eliminação (RN-GF-25), nomeando todos os pares do escopo nexio.attachment daquele arquivo — o original e as cópias, liberados na transação do expurgo —, com a marcação, o motivo, a referência do pedido e quem registrou e quem executou como autores declarados (RN-GF-27) — a lápide suprime o nome original. O evento attachment.erased só sai quando o GrydFiles grava purgedAt em todos esses pares: até lá o pedido fica Executed, com a pendência à vista no anexo, e só o job repete, com os autores gravados — nunca por usuário, que o GrydFiles recusaria por autor declarado diferente de quem está no contexto. Expurgo recusado — guarda de qualquer detentor, ou referência de outro escopo criada nesse meio-tempo — não vira pendência silenciosa: o pedido vai a PurgeBlocked, com o código — FILE_LEGAL_HOLD_CONFLICT, guarda de algum detentor, ou FILE_STILL_REFERENCED_CONFLICT, referência ativa de outro escopo — e a hora, à vista na fila ao lado do dueAt do titular, e o evento attachment-erasure-request.purge-blocked sai depois do commit dessa transição — nome próprio, que não se confunde com o attachment-erasure-request.blocked do registro bloqueado; o job segue repetindo, o aviso sai a quem registrou e ao titular, e, com o dueAt vencido, também ao administrador do tenant. Expurgo que ainda não voltou não é melhor do que expurgo recusado: pedido Executed com purgedAt nulo e dueAt vencido escala igual — mesmo aviso, mesma fila, mesmo destaque —, porque do lado do titular o prazo venceu do mesmo jeito, e falha transitória que ninguém repara é indistinguível de bloqueio. E pedido parado vence do mesmo jeito: pedido Requested bloqueado — por aprovação, por decisão de controle ou por alcance — com o dueAt vencido escala igual, com o mesmo aviso, a mesma fila e o mesmo destaque dos outros dois, e com o administrador do tenant na audiência. Bloqueio não para o relógio do titular: avisá-lo uma vez, no registro, e deixar o prazo correr em silêncio é a mesma pendência silenciosa do expurgo, um passo antes. E o bloqueio por alcance ganha saída declarada: quem concede escopo no tenant — update:company-scopes, da Empresa — pode encerrar o pedido bloqueado por alcance incompleto, com motivo escrito, pela própria rota de cancelamento: o pedido vai a Cancelled, o titular é avisado como em todo cancelamento, o ato fica registrado com quem encerrou, e o arquivo volta à propagação (RN-ATT-15). Sem essa saída, quem tivesse erase:attachments e um anexo legível registrava um pedido que ninguém pode executar — o alcance nunca fica inteiro por conta própria — e prendia POST /attachments/copy sobre o arquivo inteiro, sem prazo e sem fim. Por que encerramento por gente, e não caducidade por prazo: um relógio que cancela sozinho responde outra vez com silêncio ao exercício de um direito, e ainda por decurso, sem que ninguém tenha decidido nada; assim a conta fica com quem tem o poder de desfazer o bloqueio — conceder o escopo, ou encerrar o pedido dizendo por que não o concede —, e a escolha fica escrita. E o encerramento fecha o laço: enquanto o alcance de quem registra continuar incompleto, pedido novo bloqueado por alcance sobre o mesmo arquivo é recusado (ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN) — o caminho é pedir escopo, não repetir o pedido; quem tem o alcance inteiro registra e executa normalmente. Depois do ato, o único instrumento que segura os bytes é a guarda legal: o job relê a guarda antes de cada chamada e não relê OpenApprovalOf nem PendingControlOf — esses são conferidos no registro e relidos sob a trava na execução (RN-ATT-39), não depois. A consequência fica escrita: decisão ou contestação aberta depois do commit do ato não segura o expurgo, e já não teria o que preservar do lado do Nexio — o nome foi neutralizado e as referências, liberadas; quem precisa dos bytes para uma decisão aberta põe guarda legal por caso, que vence o pedido do titular, e o pedido fica PurgeBlocked. Publicar a eliminação de bytes que continuam lá é o erro que não se corrige depois. É ato de controle.

Prova de controle, ou anexo ainda sob guarda: quem registra o pedido não executa

O registro grava um AttachmentErasureRequest e calcula, sobre todos os anexos do arquivo, se a eliminação pede segunda pessoa. Pede quando algum deles é de tipo com isControlEvidence = true — na semente, BANK_OWNERSHIP_PROOF, SIGNED_CONTACT_CHANGE_REQUEST e APPROVAL_EVIDENCE —, ou de tipo com retenção Permanent — SIGNED_CONTRACT, na semente —, qualquer que seja a marca, ou está dentro da guarda: com o dono fora de Draft, a referência ainda não liberada, ou guardUntil no futuro — o anexo de documento fechado que o re-scan achou infectado depois de liberado continua sob guarda (RN-ATT-37) —, ou o dono ainda não fechado. Sem segunda pessoa e sem bloqueio, o registro executa, e o pedido nasce Executed com o mesmo usuário nas duas pontas. Com segunda pessoa, ou com bloqueio (RN-ATT-33), o pedido nasce Requested e nada é revogado. Pedido bloqueado continua registrado: o titular tem prazo, e o registro é a prova de que o pedido foi recebido. A fila mostra o bloqueio — calculado na leitura, nunca gravado — e o dueAt; o aviso sai ao titular, pelo e-mail informado no registro, dizendo só que o pedido aguarda a conclusão de uma decisão em curso — sem nome de arquivo, de documento ou de decisão —, e, sem e-mail, a quem registrou, que responde pelo canal do pedido. O aviso de pedido pendente vai a quem pode executar — erase:attachments, escopo e CanRead em todos os donos, menos quem registrou quando o pedido pede segunda pessoa; sem ninguém assim, ao administrador do tenant, e o pedido fica parado. Executa POST /attachments/erasure-requests/{requestId}/execute — com segunda pessoa, por usuário diferente de quem registrou (ATTACHMENT_ERASURE_SAME_PERSON_FORBIDDEN); sem ela, também quem registrou, depois de o bloqueio cair —, com as condições reconferidas na hora e a marca recalculada — o que endureceu entre o registro e a execução vale; o que afrouxou, não. Quem registrou, ou outra pessoa com o mesmo alcance, cancela o pedido pendente com comentário (RN-ATT-38).

A razão é a do núcleo de controle bancário: uma pessoa que pede e executa sozinha a eliminação do comprovante de uma troca de conta fraudulenta já fechada apaga a prova da própria fraude, e o pedido do titular vira a porta; em Permanent, o que se apaga não tem data para deixar de ser prova. Nos demais casos — anexo de rascunho nunca submetido, sem a marca, de tipo com prazo, cuja guarda já terminou — o que está em jogo é o dado pessoal, não a prova, e uma pessoa basta, com o ato registrado.

A segunda pessoa é outro usuário. A comparação é por userId, como nas decisões do motor: sem comparação por CPF e sem regra de conflito de interesse — o proponente, o aprovador, o remetente do lote, o autor do callback ou quem anexou a prova podem registrar ou executar. Risco aceito, e registrado: dois usuários da mesma pessoa, ou o aprovador de uma troca fraudulenta, fazem as duas pontas. O que o mitiga fica: o ato de controle registrado com os dois autores, o aviso ao segundo antes de ele executar e a trilha do ato. Identidade verificada por CPF, como a do núcleo bancário, e regra de conflito de interesse foram consideradas e não entram.

Campos · AttachmentLegalHold

CampoTipoRegra
id · tenantIdGuid · GuidgeradoUma linha por colocação de guarda sobre um anexo.
attachmentIdGuidobrig→ Attachment, no mesmo contexto. Imutável.
caseReferencestring(80)obrigO caso: número do processo, do auto de infração, da auditoria. Imutável. No máximo uma guarda ativa por (attachmentId, caseReference).
reasonstring(500)obrigPor que a guarda existe. Texto de pessoa: nunca vai para variável de aviso.
placedByUserId · placedAtGuid · DateTimegeradoQuem colocou e quando.
liftedByUserId · liftedAt · liftReasonGuid? · DateTime? · string(500)?condPreenchidos juntos, uma vez, na retirada. Nulos é guarda ativa. A linha nunca é apagada nem reescrita depois disso.
fileHoldAppliedAt · fileHoldReleasedAtDateTime? · DateTime?geradoQuando o GrydFiles confirmou o legalHold do arquivo por esta guarda, e quando o arquivo deixou de depender dela — desligado, ou segurado por outra guarda ativa. fileHoldAppliedAt nulo com a guarda ativa, ou fileHoldReleasedAt nulo com a guarda retirada, é pendência (RN-ATT-35).
Filha do anexo, sem rota de edição. Existe como linha, e não como flag no anexo, porque dois casos sobre o mesmo arquivo são comuns — fiscalização e litígio — e encerrar um não pode soltar o outro.

Campos · AttachmentErasureRequest

CampoTipoRegra
id · tenantIdGuid · GuidgeradoUm pedido de eliminação de titular sobre um arquivo do tenant.
storedFileId · attachmentIdGuid · GuidobrigO arquivo, que é o que se elimina — com todas as cópias (RN-ATT-33) —, e o anexo pelo qual o pedido foi aberto. Imutáveis. No máximo um pedido Requested por (tenantId, storedFileId).
requestReference · commentstring(80) · string(1000)obrigA referência do pedido do titular e a avaliação de que não há base legal de guarda. Texto de pessoa: nunca vai para variável de aviso.
dueAt · requesterEmailDateTime · string(254)?obrig · condA data-limite de resposta ao titular, informada no registro, e o e-mail pelo qual ele recebe o aviso de pedido bloqueado — opcional; sem ele, o aviso vai a quem registrou. Imutáveis. O e-mail é dado pessoal do titular: só o lê quem tem erase:attachments e o alcance do pedido, e nunca vai a outra variável de aviso.
requiresSecondPersonboolgeradoCalculado no registro e de novo na execução: algum anexo do arquivo de tipo com isControlEvidence ou com retenção Permanent, ou dentro da guarda. Só passa de falso a verdadeiro (RN-ATT-38).
statusenumgeradoRequested · Executed — ato gravado, expurgo pendente — · PurgeBlocked — o GrydFiles recusou o expurgo, e o job repete — · Purged · Cancelled. Terminais são Purged e Cancelled; Executed e PurgeBlocked alternam até o purgedAt chegar (RN-ATT-33).
requestedByUserId · requestedAtGuid · DateTimegeradoQuem registrou e quando.
executedByUserId · executedAtGuid? · DateTime?condPreenchidos juntos, uma vez, na execução. Com requiresSecondPerson, executedByUserId nunca é igual a requestedByUserId. Com requestedByUserId, são os dois autores declarados do expurgo (RN-ATT-33).
purgedAt · purgeBlockedSince · purgeBlockedCodeDateTime? · DateTime? · string(60)?condpurgedAt é gravado quando o GrydFiles confirma o expurgo de todos os pares do escopo, e é o que leva o pedido a Purged e publica attachment.erased (RN-ATT-33). Recusa do expurgo preenche purgeBlockedSince e purgeBlockedCode — o código que o GrydFiles devolveu, FILE_LEGAL_HOLD_CONFLICT (guarda de algum detentor) ou FILE_STILL_REFERENCED_CONFLICT (referência ativa de outro escopo) — e leva a PurgeBlocked; a repetição que passa os limpa. Nunca trazem nome de arquivo.
cancelledByUserId · cancelledAt · cancelCommentGuid? · DateTime? · string(500)?condPreenchidos juntos, uma vez, no cancelamento.
Sem rota de edição e sem nome de arquivo. O pedido nunca é apagado nem reescrito: executar e cancelar preenchem os seus campos uma vez. O bloqueio do registro não é campo: é calculado na leitura e na execução (RN-ATT-33). O bloqueio do expurgo é outra coisa — é o estado PurgeBlocked, gravado, porque o ato já aconteceu e o titular tem prazo.

Decisão fechada

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. O estado vem do GrydFiles na hora da submissão, não do que o contrato de veredito já aplicou: anexo cujo arquivo já está Infected ou reprovado, e ainda não foi revogado, também bloqueia — e também a reserva de upload que ainda pode ser confirmada (Reserved): o arquivo que chegasse depois da submissão entraria num documento que não aceita mais escrita (RN-ATT-19, RN-ATT-36).

Failed bloqueia pela mesma razão 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. A saída não é esperar indefinidamente: o arquivo só chega a Failed depois das tentativas automáticas do GrydFiles, e a tela oferece "verificar de novo", que o Nexio pede ao GrydFiles pela IFileService (POST /attachments/{id}/rescan), ou "remover".

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 — um spinner que bloqueia até o veredito — é 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 na v1: a tela mostra o estado, e a pessoa envia quando libera

Ninguém fica preso: a pessoa continua preenchendo, sai da tela e volta. Quem tenta enviar com anexo sem veredito recebe ATTACHMENT_SCAN_PENDING_CONFLICT, com a lista dos anexos e o estado de cada um. A tela não precisa chegar a essa resposta: ela acompanha o estado pela consulta em lote (GET /attachments/status), mostra ao lado do botão de enviar o que ainda falta liberar e o torna disponível quando o último veredito chega. O 409 é a guarda do servidor — vale para a tela desatualizada, para a integração e para o clique que chega junto com o veredito. Anexo em Failed sai por "verificar de novo" (POST /attachments/{id}/rescan) ou por revogação; anexo em Infected já foi revogado pelo contrato de veredito (RN-ATT-20), e a pessoa anexa outro arquivo.

Nenhum processo em segundo plano submete documento. Submeter abre aprovação e, no desfecho, empenha verba; fazer isso fora da requisição de uma pessoa exigiria restaurar a identidade, as permissões e o escopo dela e registrar que o ato foi feito em nome dela — execução delegada, que a plataforma não oferece. Por isso o registro de descritores não tem pergunta sobre como submeter o dono: o dono submete a si mesmo, na requisição de quem clicou, e pergunta ao anexo se há algo pendente. Submeter sozinho quando o arquivo liberar está em § Fora de escopo, como v2.

FILE_SCAN_UNAVAILABLE não é erro do usuário. Quando o clamd está fora do ar, o GrydFiles devolve 503 — pelo sufixo _UNAVAILABLE, que o mapeador do Core leva a 503. 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.

Origem externa

Anexo do portal do fornecedor

A porta do fornecedor — convite, conta, sessão, rotas e quem pode entrar — é da spec do Portal do Fornecedor, e esta página não a repete. O que é do anexo, e está fixado aqui, é a autoria: o anexo vindo do portal não pode ser modelado depois: é o caso que decide três campos, e acrescentá-los mais tarde significaria voltar em anexos já gravados sem saber de quem eles são. Metade da amarração já existe — a spec de Fornecedor diz que SupplierContact é opcionalmente ligado a um PortalUser.

supplierId obrigatório, portalUserId opcional

A assimetria é deliberada e vem do mundo real: o fornecedor pode entrar no portal sem PortalUser cadastrado — a forma de acesso é do Portal do Fornecedor —, e nesse caso não há usuário a registrar; mas sempre há fornecedor, porque o acesso do portal o identifica. supplierId é resolvido pelo portal e gravado denormalizado no anexo, nunca lido do contato: contato muda de empresa, é desligado ou apagado, e a autoria tem que ficar congelada. É a regra 1 do mapa de domínio aplicada à autoria.

Nasce Internal, sempre

O defaultVisibility do tipo é ignorado no caminho do portal. A proposta que o fornecedor A enviou não é visível ao fornecedor B nem a nenhum outro no portal — além do próprio A, quem lê é o comprador. Marcar como Supplier depois não existe (ATTACHMENT_SUPPLIER_VISIBILITY_NOT_ALLOWED_UNPROCESSABLE): a proposta de um concorrente ficaria legível aos outros. O documento que o comprador quer mandar a todos, ele anexa ao QuotationRequest. E Internal não tira o anexo do próprio autor: o portal devolve ao fornecedor o que ele mesmo subiu (RN-ATT-12).

Os três ganhos que motivaram a decisão

Trilha legível. "Anexado por Fornecedor X" sem decodificar token nem cruzar tabela de sessão.

Isolamento barato. O que seria uma regra especial de visibilidade vira comparação de supplierId — a mesma coluna, o mesmo índice, nenhum caso particular no código de autorização.

Promoção a SupplierDocument. Uma certidão que chegou anexada numa cotação pode virar documento de cadastro porque já se sabe de quem ela é. Sem supplierId no anexo, essa promoção exigiria alguém digitar o fornecedor de novo — e digitar de novo é onde o dado erra.

Duas identidades, e nenhuma finge ser a outra

O uploadedBy do StoredFile continua sendo identidade do tenant no GrydAuth: o portal não tem GrydAuth, então quem aparece lá é o serviço que fez o upload. A autoria de negócio é do Attachment, em uploadedByKind, portalUserId e supplierId. Tentar forçar a identidade do fornecedor na camada de plataforma criaria usuário de tenant para quem não é do tenant — e é assim que se abre uma porta que ninguém queria abrir.

O que muda no caminho do portal
AspectoAnexo internoAnexo do portal
AutenticaçãoToken do GrydAuthA do Portal do Fornecedor, que define sessão e rota. A reserva do upload fica amarrada ao acesso do portal e ao supplierId (RN-ATT-36)
uploadedByKindUserPortalUser
supplierIdOpcionalObrigatório, resolvido pelo portal
Visibilidade inicialDo AttachmentTypeSempre Internal, e nunca Supplier
ownerType aceitoOs registrados pelos módulos presentes (RN-ATT-03)Só QuotationResponse na v1
Perfil e antivírusIdênticos. É o caminho mais importante do ClamAV — é o único em que um terceiro sobe arquivo
Escopo por empresaUserCompanyScopeDerivado do documento da RFQ, não do fornecedor

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. Anexo de item, de fornecedor, de qualificação do 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 as empresas do escopo — e, para quem não tem empresa nenhuma no escopo, para nenhuma. 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

create:attachments e update:attachments são necessárias e não suficientes: 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".

É evento de negócio do Nexio, isolado por tenant — 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, attachment.rescan-requested. Cada uma carrega ownerType, o número legível do documento e o attachmentId. O caminho do framework para gravar a trilha é demanda de plataforma, em correção pelo Gryd.IO (isolamento de tenant, permissão e gravação na transação do ato); esta página fixa o requisito, não o mecanismo.

A convenção dos nomes 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. Nem todo evento tem aviso: attachment.created, attachment.copied, attachment.visibility-changed, attachment.rescan-requested, attachment.quarantined, attachment-legal-hold.placed, attachment-legal-hold.lifted e attachment-erasure-request.executed são eventos de domínio sem linha no catálogo de avisos na v1 — alimentam a trilha e quem quiser consumi-los, e não têm destinatário nem slug. A ausência no catálogo é a declaração de que não há aviso, não esquecimento. O pedido de eliminação segue a mesma regra, com a entidade dele, e cada momento tem um nome só: attachment-erasure-request.registered (nasce livre, à espera da segunda pessoa), attachment-erasure-request.blocked (nasce travado), attachment-erasure-request.executed (ato gravado, expurgo pendente), attachment-erasure-request.purge-blocked (expurgo recusado) e attachment-erasure-request.cancelled; attachment.erased — os bytes saíram — é fato do arquivo. Os avisos são os slugs de mesma cauda na Notificação.

PermissãoAlcanceObservação
read:attachmentsListar, ver metadado e pedir URL de downloadSempre recortada pelo escopo da empresa do dono, pelo CanRead do dono e pela visibility. Necessária e nunca suficiente. Todo papel que abre documento precisa dela
create:attachmentsAnexar e propagarNão basta: exige também poder escrever no dono. Ver acima
update:attachmentsDescrever, trocar o tipo em rascunho, pedir nova verificação de anexo em Failed e rebaixar a visibilidade para InternalNão basta: exige também poder escrever no dono. Ver acima
lookup:attachmentsAlimentar o seletor de tipo de anexo (GET /attachment-types)read e lookup não se implicam
revoke:attachmentsRevogar anexo de outra pessoa, e revogar fora de rascunhoQuem anexou revoga o próprio anexo em rascunho só com create:attachments, a mesma de anexar. 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
create:attachment-typesCriar tipo no catálogo do tenant, inclusive a sobreposição de tipo globalAdministração de configuração. Mexe em retenção, então é de administrador, não de comprador
update:attachment-typesAlterar tipo do catálogo do tenant, inclusive materializar a sobreposição de tipo globalMesma administração de configuração
delete:attachment-typesDesativar tipo do catálogo do tenantDesativar é exclusão lógica e usa delete, mesmo sendo PATCH. Nunca invalida anexo existente
create:attachment-legal-holdsColocar guarda legal, sobre um anexo ou sobre todos os anexos de um donoAto de controle. Sempre recortada pelo escopo da empresa do dono. Não dá leitura de anexo — a linha da guarda não traz nome de arquivo nem conteúdo, e o número legível do dono só vai a quem passa no CanRead dele
delete:attachment-legal-holdsRetirar a guarda legal, com liftReasonAto de controle. Retira só a linha indicada; as outras guardas do anexo continuam
read:attachment-legal-holdsListar guarda legalMesmo recorte pelo escopo da empresa do dono e mesma regra de CanRead para o número legível. read não dá colocar nem retirar
erase:attachmentsRegistrar, executar, cancelar e listar pedido de eliminação de titularA mais restrita do módulo: é a única que tira prova antes do fim da guarda. Ato de controle; exige também o CanRead de todos os donos do arquivo, e prova de controle ou anexo sob guarda pedem duas pessoas com ela (RN-ATT-38)
Treze permissões, e nenhuma que libere download de arquivo em Scanning, Infected ou Purged. Como no GrydFiles, a ausência é a decisão.

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

Enquanto algum anexo não tem veredito limpo, o botão de enviar para aprovação fica indisponível, com a lista do que falta e o estado de cada um: a reserva ainda não confirmada diz "enviando"; Pending e Scanning dizem "verificando"; Failed traz duas ações na própria linha — "verificar de novo" e "remover"; o infectado e o reprovado já saíram da lista, com aviso. O botão volta sozinho quando o último veredito chega, pela consulta em lote. Se o clique chegar antes da tela, o 409 é a guarda do servidor e lista os mesmos anexos — 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 os que ele mesmo subiu — nunca os de outro fornecedor, nem o que o comprador acrescentou à resposta dele. 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 —, e sobrepor um global só aceita retenção igual ou mais longa.

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 — com os infectados depois de liberados à parte —, escritas pendentes no GrydFiles por idade (RN-ATT-35), 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.

Admin · pedidos de eliminação

A fila de quem executa: pedidos Requested com o dono, o número legível, a referência do pedido do titular, o comentário, quem registrou, a data-limite e se está bloqueado — e, ao lado, os pedidos PurgeBlocked, com o código do GrydFiles — FILE_LEGAL_HOLD_CONFLICT ou FILE_STILL_REFERENCED_CONFLICT —, desde quando e o dueAt vencido em destaque — e, no mesmo destaque, os Executed com purgedAt nulo e dueAt vencido, que são o mesmo problema sem código: o ato foi feito e os bytes continuam lá, então alguém precisa olhar. No mesmo destaque entram os Requested bloqueados com o dueAt vencido, qualquer que seja o bloqueio: bloqueio não para o relógio do titular. O pedido bloqueado por alcance incompleto aparece a quem o registrou e ao administrador do tenant, com o alcance que falta e sem dono nem número legível, porque é ele quem concede escopo — e é dele também o botão de encerrar o pedido, com motivo escrito, quando conceder o escopo não é o caminho (RN-ATT-38) — e o botão de executar indisponível para quem registrou quando o pedido pede segunda pessoa, e para todos enquanto está bloqueado, com o motivo. Aberto o pedido, a tela mostra as condições da RN-ATT-33 e a pré-condição do GrydFiles (RN-ATT-39) como estão agora, antes do clique; o servidor as reconfere de qualquer jeito.

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

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-intentcreate:attachmentsfileId, uploadUrl, uploadExpiresAt e os cabeçalhos do PUT. Valida dono, tipo, escopo, CanWrite e limites antes de reservar, e grava a reserva, que não aparece na lista400 · 404 · 409 · 422
POST /attachmentscreate:attachmentsAttachmentDto. Só sobre reserva do mesmo usuário, tenant, dono e tipo (RN-ATT-36), e reconfere o CanWrite do dono antes de confirmar. Confirma pela IFileService declarando a referência esperada; não cria FileReference. Repetir com o mesmo fileId repete a confirmação e devolve o mesmo anexo400 · 404 · 409 · 422 · 503
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, CanRead do dono, visibilidade, revogação — e só então chama o GrydFiles. Registra documento e anexo na trilha404 · 409 · 503
PUT /attachments/{id}update: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}/rescanupdate:attachmentsPede ao GrydFiles, pela IFileService, nova verificação de anexo em Failed. Exige o CanWrite do dono; em dono que não aceita mais escrita, revoke:attachments e o CanRead. 200 com o anexo em Scanning404 · 409 · 503
POST /attachments/{id}/visibilityshare-with-supplier:attachmentsPromove a Supplier. Rebaixar para Internal aceita só update: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 nunca submetido, depois do commit (RN-ATT-35). Motivo obrigatório fora dele404 · 409 · 422
POST /attachments/{id}/eraseerase:attachmentsEliminação a pedido de titular: requestReference, dueAt e comment, requesterEmail opcional; exige escopo e CanRead no anexo em que age — 404 sem eles —, nenhuma guarda e a pré-condição do GrydFiles (RN-ATT-39); alcance incompleto nos demais donos bloqueia o pedido, não o recusa (RN-ATT-33). Grava o AttachmentErasureRequest. Sem segunda pessoa e sem bloqueio, executa — revoga o anexo e as cópias do mesmo arquivo e registra o ato; quem libera as referências e pede o expurgo é o job, depois do commit (RN-ATT-35) — e devolve 201 com o pedido Executed e o purgedAt nulo enquanto o expurgo está pendente; com segunda pessoa, ou com dono em aprovação, decisão de controle pendente ou alcance incompleto, 201 com o pedido Requested — bloqueado, se for o caso, com o motivo do bloqueio —, e nada revogado. Sobre anexo com expurgo pendente, 200 com o pedido: quem repete o expurgo é o job. RN-ATT-33, RN-ATT-38, RN-ATT-39403 · 404 · 409 · 422 · 503
POST /attachments/erasure-requests/{requestId}/executeerase:attachmentsExecuta o pedido Requested — por usuário diferente de quem o registrou, quando pede segunda pessoa; reconfere na hora escopo, CanRead, guarda legal, referência de outro escopo, pré-condição do GrydFiles, aprovação em curso e decisão de controle pendente, com os donos travados (RN-ATT-39). 200 com o pedido Executed403 · 404 · 409 · 503
POST /attachments/erasure-requests/{requestId}/cancelerase:attachmentsEncerra o pedido Requested, com comment. Pedido bloqueado por alcance incompleto é encerrado também por quem tem update:company-scopes — a saída do bloqueio que ninguém dentro do pedido derruba —, e esse encerramento fica fora da cadeia de cancelamentos (RN-ATT-38). Todo cancelamento avisa o titular; do segundo sobre o mesmo arquivo em diante, por usuário diferente do que cancelou o anterior, e o aviso alcança também o administrador do tenant (RN-ATT-38). 200 com o pedido Cancelled403 · 404 · 409 · 422
GET /attachments/erasure-requestserase:attachmentsOs pedidos, filtráveis por status — inclusive PurgeBlocked, que é o que precisa de gente — só os de arquivo cujos donos todos passam no escopo e no CanRead de quem lista; os outros não vêm — com uma exceção: o pedido bloqueado por alcance incompleto vem também para quem o registrou e para o administrador do tenant, com a referência, o dueAt e o alcance que falta, e sem dono nem número legível. Traz dono, número legível, quem registrou, se pede segunda pessoa, se está bloqueado e por quê, e o dueAt — nunca nome de arquivo400
POST /attachments/{id}/legal-holdscreate:attachment-legal-holdsColoca guarda legal no anexo: caseReference e reason. Só no escopo da empresa do dono; fora dele, 404. 201 com a linha, sem nome de arquivo404 · 409 · 422
POST /attachments/legal-holdscreate:attachment-legal-holdsColoca guarda legal em todos os anexos de um dono (ownerType + ownerId) cuja referência não foi liberada, com o mesmo caseReference. Só no escopo da empresa do dono; fora dele, 404. Tudo ou nada. 201 com as linhas400 · 404 · 409 · 422
POST /attachments/{id}/legal-holds/{holdId}/liftdelete:attachment-legal-holdsRetira aquela guarda, com liftReason. Só no escopo da empresa do dono; fora dele, 404. 200 com a linha. As outras guardas do anexo continuam404 · 409 · 422
GET /attachments/legal-holdsread:attachment-legal-holdsAs guardas, filtráveis por caseReference, dono e ativas. Só as de anexo no escopo de quem lista — o de companyId nulo, para quem tem alguma empresa no escopo (RN-ATT-30); as outras não vêm. Traz attachmentId; o dono (ownerType, ownerId) e o número legível só quando quem lista passa no CanRead dele — o da proposta bancária carrega os últimos dígitos da conta —; nunca nome de arquivo400
POST /attachments/copycreate:attachmentsPropagação explícita, em lote: lista de attachmentId e o dono de destino. Cria um anexo por item, herdando a visibilidade; a referência de cada um é criada depois do commit (RN-ATT-35). Destino de outra empresa exige escopo nas duas (RN-ATT-14). Arquivo com pedido de eliminação Requested, Executed ou PurgeBlocked é recusado, e a chamada toma a trava do arquivo (ATTACHMENT_ERASURE_IN_PROGRESS_CONFLICT, RN-ATT-15)400 · 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ário. Elo cujo dono o usuário não lê não aparece404
GET /attachments/owner-typesread:attachmentsO registro, do jeito que a tela precisa: tipos aceitos, limites, se aceita visibilidade de fornecedor, para onde propaga—
POST rota do fornecedoracesso do portalRota, autenticação e sessão são definidas pelo Portal do Fornecedor. O que é do Anexo vale em qualquer rota: upload-intent e confirmação embutidos; supplierId resolvido pelo portal; nasce Internal; ownerType é sempre QuotationResponse; a reserva e a confirmação seguem a RN-ATT-36, com o acesso do portal no lugar do usuário400 · 401 · 409 · 422
GET /attachment-typeslookup: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-typescreate:attachment-types (POST) · update:attachment-types (PUT)Administraçã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; a sobreposição só aceita retenção igual ou mais longa que a do global (RN-ATT-25); code e retentionMode são imutáveis depois do primeiro uso, e isControlEvidence só liga400 · 404 · 409 · 422
PATCH /attachment-types/{id}/deactivatedelete: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
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.

Contrato

Contrato de erros

O status HTTP é derivado do código, nunca da mensagem. O mapeador do Core reconhece dez sufixos — _NOT_FOUND → 404 · _ALREADY_EXISTS, _ALREADY_ASSIGNED e _CONFLICT → 409 · _FORBIDDEN, _ACCESS_DENIED e _BLOCKED → 403 · _UNAUTHORIZED → 401 · _UNPROCESSABLE → 422 · _UNAVAILABLE → 503 · sem sufixo → 400 —, e código exato vence sufixo.

CódigoHTTPQuando
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 fora do enum, ou de módulo que não está registrado nesta aplicação. Com o módulo presente e o descritor faltando, a inicialização já teria falhado (RN-ATT-03)
ATTACHMENT_OWNER_NOT_WRITABLE_CONFLICT409Nas rotas de anexo — reserva, confirmação, alteração, visibilidade, revogação — sobre documento que o usuário não pode editar, ou que não aceita mais alteração, como a proposta bancária submetida. A confirmação reconfere, porque o dono pode ter mudado desde a reserva (RN-ATT-36). A regra é do dono; o código é deste módulo, e as páginas donas o citam
ATTACHMENT_UPLOAD_NOT_RESERVED_CONFLICT409POST /attachments com fileId que não é reserva do mesmo usuário — ou do mesmo acesso, no portal —, do mesmo tenant, para o mesmo dono e tipo: arquivo de outro anexo, reserva alheia, dono trocado. Nada é gravado (RN-ATT-36)
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_TYPE_RETENTION_BELOW_GLOBAL_UNPROCESSABLE422Sobreposição de tipo global com retenção mais curta que a dele — no POST, no PUT ou ao reativar. A sobreposição só alonga (RN-ATT-25)
ATTACHMENT_TYPE_CONTROL_EVIDENCE_REQUIRED_UNPROCESSABLE422Sobreposição de tipo global que desliga isControlEvidence. A marca de prova de controle só liga (RN-ATT-25)
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
REASON_CODE_REQUIRED_UNPROCESSABLE422Do Motivo: revogar fora de rascunho sem reasonCodeId. Retirar prova de documento em curso precisa de motivo nomeado. Código de outro uso ou desativado responde REASON_CODE_USAGE_NOT_ALLOWED_UNPROCESSABLE ou REASON_CODE_INACTIVE_UNPROCESSABLE, também de lá
ATTACHMENT_UNDER_LEGAL_HOLD_CONFLICT409Eliminar anexo — ou outro anexo do mesmo arquivo — com guarda legal ativa, ou com o arquivo sob guarda de qualquer detentor no GrydFiles, inclusive a posta pela rota dele — é a tradução do FILE_LEGAL_HOLD_CONFLICT do GrydFiles (RN-ATT-39). A resposta diz só que há guarda; os caseReference ativos vêm apenas para quem tem read:attachment-legal-holds — o número do processo não é informação de quem pede a eliminação
ATTACHMENT_ERASURE_FILE_REFERENCED_CONFLICT409O arquivo tem referência ativa de outro escopo além de nexio.attachment. É a tradução do FILE_STILL_REFERENCED_CONFLICT do GrydFiles (RN-GF-25). Nada foi revogado; a resposta nomeia os escopos
ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN403Executar eliminação com algum anexo do mesmo arquivo em dono fora do escopo ou do CanRead de quem executa. No registro não recusa — o pedido é gravado bloqueado por alcance, com o prazo correndo e o administrador do tenant escalado (RN-ATT-33) —, com uma exceção: depois de um pedido bloqueado por alcance sobre o mesmo arquivo ter sido encerrado por quem concede escopo, registro novo que siga sem o alcance inteiro recusa aqui, e o caminho é pedir escopo (RN-ATT-38). Nada foi revogado; a resposta não identifica o documento
ATTACHMENT_ERASURE_REQUEST_REQUIRED_UNPROCESSABLE422Eliminação sem requestReference, sem dueAt ou sem comment, ou com dueAt posterior ao do pedido cancelado sobre o mesmo arquivo, ou cancelamento de pedido sem comment. A avaliação jurídica fica escrita, ou o ato não acontece
ATTACHMENT_ALREADY_ERASED_CONFLICT409Eliminar anexo cujo expurgo já foi confirmado. Com expurgo pendente não é erro: o ato devolve o pedido, e só o job repete o expurgo
ATTACHMENT_ERASURE_IN_PROGRESS_CONFLICT409Propagar arquivo com pedido de eliminação Requested, Executed ou PurgeBlocked. Cópia nova sobre pedido em curso nasceria com o dado pessoal, ou prenderia o titular sem prazo (RN-ATT-15)
ATTACHMENT_ERASURE_APPROVAL_IN_PROGRESS_CONFLICT409Executar eliminação com aprovação em curso em algum dono dos anexos do arquivo (OpenApprovalOf não nulo). Nada foi revogado. No registro não é erro: o pedido nasce bloqueado e espera a instância terminar (RN-ATT-33)
ATTACHMENT_ERASURE_CONTROL_DECISION_PENDING_CONFLICT409Executar eliminação com decisão de controle pendente fora da instância em algum dono (PendingControlOf não nulo): lote de carga inicial por confirmar, contato à espera da segunda pessoa, carência, contestação, devolução à espera de reenvio, correção de identidade. Nada foi revogado; a resposta não identifica o documento. No registro não é erro: o pedido nasce bloqueado (RN-ATT-33)
ATTACHMENT_ERASURE_FILE_NOT_PURGEABLE_CONFLICT409A consulta de pré-condição do GrydFiles diz que o expurgo como eliminação seria recusado — arquivo em Pending ou Scanning, ou Permanent sem dois autores distintos (FILE_ERASURE_DUAL_CUSTODY_CONFLICT). Arquivo em quarentena não recusa por estado. Nada foi gravado; a resposta traz o código do GrydFiles. Guarda e referência de outro escopo têm os códigos próprios; trilha durável indisponível repassa FILE_AUDIT_TRAIL_UNAVAILABLE (503) (RN-ATT-39)
ATTACHMENT_ERASURE_SAME_PERSON_FORBIDDEN403Executar, com o mesmo usuário que o registrou, pedido que pede segunda pessoa (RN-ATT-38)
ATTACHMENT_ERASURE_CANCEL_SAME_PERSON_FORBIDDEN403Cancelar, pela segunda vez ou mais sobre o mesmo arquivo, com o usuário que cancelou o pedido anterior. O vaivém de registrar e cancelar sozinho deixava o pedido do titular sem nunca vencer (RN-ATT-38)
ATTACHMENT_ERASURE_REQUEST_ALREADY_EXISTS409Já existe pedido Requested para o mesmo arquivo. A ordem é declarada: o escopo e o CanRead do anexo em que se age são conferidos antes — sem eles a resposta é 404 —, e só quem passa recebe o id do pedido. O alcance nos demais donos não entra nessa ordem: ele bloqueia o pedido em vez de recusá-lo (RN-ATT-33). Devolver o id primeiro contava a quem não enxerga nem o anexo em que age que existe pedido, e sobre qual arquivo
ATTACHMENT_ERASURE_REQUEST_NOT_FOUND404Pedido inexistente neste tenant
ATTACHMENT_ERASURE_REQUEST_CLOSED_CONFLICT409Executar ou cancelar pedido que não está Requested: Executed, PurgeBlocked, Purged ou Cancelled (RN-ATT-38)
ATTACHMENT_LEGAL_HOLD_NOT_FOUND404Guarda inexistente neste tenant, ou de outro anexo
ATTACHMENT_LEGAL_HOLD_ALREADY_EXISTS409Já existe guarda ativa com o mesmo caseReference sobre o anexo. Na colocação por dono, recusa o lote inteiro e lista quais
ATTACHMENT_LEGAL_HOLD_LIFTED_CONFLICT409Retirar guarda já retirada. Retirada é terminal; o caminho de volta é colocar outra
ATTACHMENT_LEGAL_HOLD_REASON_REQUIRED_UNPROCESSABLE422Colocar sem caseReference ou reason, ou retirar sem liftReason
ATTACHMENT_SCAN_PENDING_CONFLICT409A decisão 8. Submeter para aprovação com anexo não revogado cujo arquivo, consultado na hora, não está Available, inclusive reserva Reserved ainda confirmável: Pending, Scanning, Failed, ou Infected e reprovado ainda não revogados pelo contrato. Na confirmação do lote da carga inicial bancária valem os estados que o Fornecedor declara, copiados: linha sem comprovante e sem callback Confirmed, comprovante revogado, arquivo eliminado, veredito Infected, anexo em estado reprovado, veredito Failed e reserva de envio Reserved ainda confirmável são falta de evidência, com o código dele (BANK_INITIAL_LOAD_EVIDENCE_MISSING_CONFLICT), que lista a linha e o estado de cada uma; sem nenhuma falta, Pending e Scanning são espera e recusam com este código. Failed é falta, não espera. A resposta lista quais e em que estado, porque "espere", "mande verificar de novo" e "anexe outro" são conversas diferentes
ATTACHMENT_RESCAN_NOT_FAILED_CONFLICT409Pedir nova verificação de anexo cujo arquivo não está em Failed. Re-scan de arquivo liberado é do acervo, feito pela plataforma
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 recebeu o veredito Available. É 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_UNPROCESSABLE422ownerType do destino fora do PropagatesTo da origem. Destino de empresa em que quem propaga não tem escopo responde 404, como documento fora de escopo (RN-ATT-14)
ATTACHMENT_SUPPLIER_REQUIRED_UNPROCESSABLE422uploadedByKind = PortalUser sem supplierId. Não deveria acontecer — o portal 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, desligar isControlEvidence nele, 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
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.

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-02Nenhuma chave estrangeira cruza o contexto

Nem para o StoredFile, que é de outro módulo, nem para o dono, que é de outro bounded context. A integridade é mantida pelo registro de descritores, pelo evento de descarte e pela varredura diária — as três, sempre. Duas delas não bastam.

RN-ATT-03Todo tipo de dono registrado tem descritor completo, e o que falta derruba a inicialização

Falha no startup, não em produção. Cada módulo dono registra os seus valores de AttachmentOwnerType e o descritor de cada um; a verificação percorre o que foi registrado e exige, para cada valor, ser valor do enum, ter exatamente um descritor e responder às doze perguntas. Valor sem módulo presente não é exigido e não aceita anexo (ATTACHMENT_OWNER_TYPE_UNKNOWN); anexo já gravado cujo dono é de módulo ausente não é lido nem varrido — sem descritor não há CanRead, e a resposta é 404. O enum persiste como texto: acrescentar valor não é migração.

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-05companyId é derivado do dono, e fora do escopo a resposta é 404

Derivado pelo descritor, denormalizado, filtrado por UserCompanyScope. "Não existe" e "não é seu" respondem igual — um 403 confirmaria a existência de um documento a quem não deveria saber que ele existe. O caso do companyId nulo está na RN-ATT-30, e não é exceção a esta regra: é dono que não tem empresa, não anexo fora de escopo.

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

create:attachments e update:attachments são necessárias e não suficientes: 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 tem 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. A primeira referência não é criada pelo Nexio: o anexo declara a referência esperada na confirmação e o módulo de arquivo cria a referência na liberação (RN-ATT-26, RN-ATT-36). A cópia cria a sua depois do commit (RN-ATT-35) — ela só é oferecida sobre arquivo já liberado.

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

retentionMode e o piso retainUntil, contado da data da reserva, vão ao GrydFiles na reserva, pela IFileService; o modo e os anos ficam gravados no anexo. O Nexio decide, o GrydFiles executa. Mudar a configuração do tipo depois não altera o que já foi anexado. Guarda legal não vem do tipo (RN-ATT-32).

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-12No portal, o fornecedor lê o que ele mesmo subiu e o que o documento pai compartilha

O portal devolve ao fornecedor autenticado, nas respostas dele, os anexos com uploadedByKind = PortalUser e o supplierId dele, em qualquer visibilidade e enquanto não revogados, mais os anexos Supplier do QuotationRequest. O que o comprador acrescentou à resposta — mesmo com supplierId preenchido — não volta ao portal. Anexo de QuotationResponse nunca vira Supplier (ATTACHMENT_SUPPLIER_VISIBILITY_NOT_ALLOWED_UNPROCESSABLE): seria a proposta de um concorrente legível aos outros. Isolamento entre fornecedores é comparação de coluna, não visibilidade.

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 entre empresas exige escopo nas duas, e não cria vínculo entre documentos

Destino de outra empresa do mesmo tenant é aceito quando quem propaga passa no escopo e no CanRead do dono de origem e no escopo e no CanWrite do dono de destino; faltando qualquer um, o documento responde 404. A cópia grava o companyId do destino. Não é rateio: cada documento continua de uma empresa. 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, nem se propaga arquivo em eliminação

Multiplicar por dois documentos um arquivo que ainda pode virar Infected é criar dois problemas. Também não se propaga arquivo com pedido de eliminação Requested, Executed ou PurgeBlocked (ATTACHMENT_ERASURE_IN_PROGRESS_CONFLICT): cópia nova sobre pedido em curso ou nasceria ativa com o dado pessoal que o titular mandou eliminar, ou prenderia o pedido dele sem prazo. A cópia toma a trava do storedFileId (RN-ATT-39), então ou ela é anterior ao conjunto que a execução leu e é eliminada junto, ou espera e é recusada. 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 nunca submetido libera a referência; fora dele, a referência fica até o fim da guarda

A invariante central do fim de vida. Liberar ao revogar em documento que já passou por alguém 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 do rascunho nunca submetido, o Nexio só libera a referência no fim da guarda (RN-ATT-31) ou pela eliminação (RN-ATT-33), e nunca sob guarda legal (RN-ATT-32). A liberação pelo re-scan que acha vírus é da plataforma, e a guarda legal a cobre pelo legalHold do arquivo (RN-ATT-37).

RN-ATT-18Anexo revogado sai da decisão e fica no histórico

Não aparece para o aprovador na fila, porque não é mais evidência válida; aparece na aba de histórico com quem, quando e por quê. Esconder de quem decide agora, mostrar para quem confere depois.

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

Anexar durante o scan é permitido; submeter não. O dono pergunta ao Anexo na submissão, e o Anexo consulta o GrydFiles na hora — não o que o contrato de veredito já aplicou: bloqueia todo anexo não revogado cujo arquivo não está Available — Pending, Scanning, Failed, e também Infected ou reprovado ainda não revogado. Anexo em Reserved conta como pendente: a reserva que ainda pode ser confirmada, e a que o GrydFiles confirmou sem a gravação chegar (RN-ATT-36); só não bloqueia a reserva cujo arquivo, relido, continua Pending com a URL de upload vencida, que não se confirma mais. Sem isso, reservar em rascunho, submeter e confirmar depois punha anexo novo em documento que não aceita mais escrita. Arquivo sem veredito não abre, e o aprovador não pode receber documento cuja prova ele não consegue ler. A recusa (ATTACHMENT_SCAN_PENDING_CONFLICT) nomeia os anexos e o estado de cada um; a tela deixa o botão indisponível antes do clique, e o 409 é a guarda do servidor. A pergunta é exposta em processo, no .Contracts do Anexo, para uma lista de donos, e vale igual para a confirmação do lote da carga inicial bancária, que é a submissão das propostas do lote (RN-FOR-31 do Fornecedor): a pergunta devolve o estado de cada comprovante, e quem decide o que cada estado significa é o Fornecedor, que declara: linha sem comprovante e sem callback Confirmed, comprovante revogado, arquivo eliminado, veredito Infected, anexo em estado reprovado, veredito Failed e reserva de envio Reserved ainda confirmável valem como falta de evidência, com o código dele, BANK_INITIAL_LOAD_EVIDENCE_MISSING_CONFLICT, que lista a linha e o estado de cada uma; sem nenhuma falta, Pending e Scanning valem como espera e recusam a confirmação com o código do Anexo, ATTACHMENT_SCAN_PENDING_CONFLICT. Failed é falta, não espera — sair dele depende de :rescan, que ninguém é obrigado a pedir, e a janela de 60 dias corre contra o controle. O Anexo responde o estado e não amplia nem reduz a lista.

RN-ATT-20Arquivo Infected revoga o anexo automaticamente

Com revocationSource = FileInfected, pelo handler do contrato de veredito, depois do commit do GrydFiles (RN-ATT-27). 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. Arquivo que já tinha sido liberado e o re-scan achou infectado segue a RN-ATT-37.

RN-ATT-21Anexo do portal exige supplierId e nasce Internal

supplierId resolvido pelo acesso do portal e gravado denormalizado, congelando a autoria; portalUserId opcional, porque o acesso sem conta não tem usuário. Rota e autenticação são do Portal do Fornecedor. O defaultVisibility do tipo é ignorado neste caminho.

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, como evento de negócio do Nexio isolado por tenant. O caminho do framework para gravá-lo é demanda de plataforma; a trilha é requisito desta página, o mecanismo não. 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, e a sobreposição só alonga a retenção

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. A retenção do global é o piso da sobreposição, na ordem UntilReleased < RetainUntil (mais anos, mais longo) < Permanent: POST e PUT de sobreposição com retenção mais curta, e a reativação de uma sobreposição assim, respondem ATTACHMENT_TYPE_RETENTION_BELOW_GLOBAL_UNPROCESSABLE; e a reserva do upload aplica a mais longa entre a sobreposição e o global vigente — o que cobre desativar e recriar e o release que alonga o global. Nenhum caminho do tenant encurta a guarda de um tipo do produto — nem desliga a marca de prova de controle dele: sobreposição com isControlEvidence = false sobre global marcado responde ATTACHMENT_TYPE_CONTROL_EVIDENCE_REQUIRED_UNPROCESSABLE. Mesma mecânica 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

POST /attachments declara a referência esperada 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-27O veredito chega depois do commit do GrydFiles, e o handler do Anexo é idempotente

O Nexio registra o IFileOutcomeHandler do escopo nexio.attachment. Os quatro desfechos — Available, Infected, reprovação (Purged com rejectionCode) e Failed — chegam depois do commit do GrydFiles, pelo menos uma vez. O handler relê o arquivo pela IFileService e decide pelo estado atual; roda com o tenant do arquivo restaurado e falha fechado sem ele; anexo inexistente, ou que não aponta para o arquivo, libera a referência, se houver, e confirma; anexo em Reserved é tratado como Expected (RN-ATT-36); aplicar duas vezes dá o mesmo resultado. Available grava usableAt e Active — e, em anexo revogado enquanto esperava, aplica a RN-ATT-17 à referência; Infected revoga (RN-ATT-20, RN-ATT-37); reprovação revoga com FileRejected e o rejectionCode; Failed só avisa. Efeito que falha volta na próxima entrega; o veredito nunca é desfeito, e nenhuma decisão do produto depende de o handler já ter rodado (RN-ATT-19).

RN-ATT-28O estado do dono chega por evento, e quem grava é o Anexo

Todo dono publica, depois do commit de cada transição que muda o estado mapeado, OwnerStateChanged(ownerType, ownerId, state, occurredAt), com state em Draft · InProgress · Closed e occurredAt no instante da transição. O Anexo consome: grava lastKnownOwnerState e lastKnownOwnerStateAt em todos os anexos do dono; ignora evento com occurredAt anterior ao gravado, então a ordem de chegada não importa; é idempotente; roda com o tenant do evento restaurado e falha fechado sem ele. Anexo criado depois grava o StateOf do momento, com a hora da leitura. Closed calcula a guarda pelo occurredAt (RN-ATT-31); a saída de Closed a zera. O dono nunca escreve em anexo, e o fechamento não depende do Anexo: atraso na entrega só alonga a guarda, porque até o evento chegar a referência fica.

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

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 vale para as empresas do escopo

Quando o CompanyOf do descritor devolve nulo — item, fornecedor, qualificação do 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 para quem tem ao menos uma empresa no escopo", como em GLAccountCompany e BudgetPolicy: linha sem empresa vale para as empresas do escopo. Quem não tem empresa nenhuma no escopo não lê anexo nenhum, nem os de companyId nulo — escopo vazio é nenhuma empresa, e conjunto vazio nunca vira ausência de filtro (RN-ATT-05). 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.

RN-ATT-31O fim da guarda conta do fechamento do dono, e a referência sai por job depois dele

Quando o dono vai a Closed — pelo occurredAt do OwnerStateChanged (RN-ATT-28) —, cada anexo com referência mantida, e o que o re-scan tornou infectado depois de liberado (RN-ATT-37), ganha guardUntil = 31/12 do ano do occurredAt + retentionYears congelado no anexo — o ano tomado em UTC, que num fuso a oeste de Greenwich nunca é anterior ao local, então o erro possível é para mais —, e o retainUntil do arquivo é estendido até lá depois do commit, confirmado em fileRetainUntil (RN-ATT-35). Em UntilReleased vale a data do occurredAt; em Permanent, nada. O job diário attachment-guard-release roda por tenant, com o tenantId no payload e o escopo aberto explicitamente, e libera a referência quando guardUntil passou, não há guarda legal ativa e o StateOf do dono, perguntado na hora, é Closed ou Missing — senão zera o guardUntil e não libera. Dono que sai de Closed zera o guardUntil dos anexos ainda referenciados; anexo criado com o dono já fechado nasce com ele calculado. Saber do fechamento com atraso só alonga a guarda.

RN-ATT-32Guarda legal é ato por caso, com motivo, segura a referência e liga o legalHold do arquivo

Colocar exige create:attachment-legal-holds, caseReference e reason, sobre um anexo ou sobre todos os anexos de um dono, tudo ou nada; retirar exige delete:attachment-legal-holds e liftReason, e encerra só a linha indicada. No máximo uma guarda ativa por (attachmentId, caseReference) (ATTACHMENT_LEGAL_HOLD_ALREADY_EXISTS); retirar duas vezes é 409. Com qualquer guarda ativa, nenhum caminho do Nexio libera a referência do anexo e a eliminação é recusada. Colocar e retirar tomam a mesma trava do storedFileId que a execução da eliminação toma (RN-ATT-39): guarda pedida enquanto uma eliminação corre espera a trava, e a eliminação que chega depois relê a guarda sob a trava e recusa. Depois do ato a promessa continua valendo — o job relê a guarda antes de pedir o expurgo e, com guarda ativa de qualquer detentor, não pede: o pedido fica PurgeBlocked (RN-ATT-33). Sem isso, "nenhum caminho libera" seria verdade até o ato e mentira depois dele. A primeira guarda ativa sobre qualquer anexo de um arquivo liga também o legalHold do StoredFile, com o caseReference como motivo e quem colocou como autor, e a retirada da última guarda ativa sobre os anexos daquele arquivo pede o desligamento, com quem retirou como autor — sem admin:system, pelo mesmo escopo que ligou (RN-GF-27); as duas chamadas depois do commit, com a pendência em fileHoldAppliedAt e fileHoldReleasedAt (RN-ATT-35). É o que protege a prova quando o re-scan libera as referências do arquivo. Colocar, retirar e listar valem só no escopo da empresa do dono: fora dele, 404, e a listagem não traz a linha; o dono e o número legível dele só vão a quem passa no CanRead do dono, e o caseReference de uma guarda nunca vai a quem não tem a permissão. Tipo nunca carrega guarda legal. Colocar e retirar são atos de controle.

RN-ATT-33Eliminação a pedido de titular alcança todas as cópias, ou nenhuma, e nunca com decisão pendente no dono

POST /attachments/{id}/erase exige erase:attachments, requestReference, dueAt e comment (requesterEmail opcional), grava o AttachmentErasureRequest e executa na hora quando o pedido não pede segunda pessoa nem está bloqueado (RN-ATT-38); o motivo é DADO_PESSOAL_INDEVIDO. Recusam, no registro e de novo na execução, sem mudar nada: guarda legal ativa em algum dos anexos do tenant que apontam para o arquivo; referência ativa de outro escopo — sem caminho automático entre consumidores, caso que na v1 não ocorre, porque o Nexio é o único consumidor de escopo de anexo (RN-GF-25); e a pré-condição do GrydFiles que falha (RN-ATT-39). Bloqueiam, qualquer que seja o CanWrite: dono com aprovação em curso — OpenApprovalOf diferente de nulo —, dono com decisão de controle pendente fora da instância — PendingControlOf diferente de nulo; os donos cobertos e o que cada um responde estão na tabela de respostas do descritor — e alcance incompleto: quem registra passa no escopo e no CanRead do anexo em que age, sem os quais a resposta é 404, e não passa em todos os donos dos demais anexos do arquivo. No registro, nenhum dos três recusa: o pedido nasce Requested, bloqueado, com o dueAt correndo, e o titular é avisado (RN-ATT-38); o bloqueio por alcance nomeia, na leitura do pedido — não é campo, como nenhum bloqueio do registro —, as empresas e os tipos de dono que faltam, nunca o documento, e escala ao administrador do tenant, que é quem concede escopo, porque sem alcance inteiro não há quem execute. Na execução, os três recusam: ATTACHMENT_ERASURE_APPROVAL_IN_PROGRESS_CONFLICT, ATTACHMENT_ERASURE_CONTROL_DECISION_PENDING_CONFLICT e ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN — quem executa precisa do alcance inteiro, ou a eliminação seria parcial e não eliminaria nada. Executada, numa transação do Nexio, com o arquivo e os donos travados (RN-ATT-39): revoga todos com Erasure, marca as referências ReleasePending, troca nome original e descrição por texto neutro e registra o ato de controle com quem registrou e quem executou. Depois do commit (RN-ATT-35) sai attachment-erasure-request.executed, e o job de guarda, sem usuário no contexto, libera as referências e pede o expurgo como eliminação (RN-GF-25) nomeando todos os pares do escopo nexio.attachment do arquivo — o original e as cópias, liberados na transação do expurgo —, com a marcação, o motivo, a referência do pedido e quem registrou e quem executou como autores declarados (RN-GF-27) — usuários distintos em Permanent e em todo pedido com segunda pessoa —, nome suprimido na lápide. attachment.erased só sai com purgedAt gravado em todos esses pares; até lá o pedido fica Executed, com a pendência à vista. Em processo a IFileService não confere permissão de módulo e o GrydFiles não define identidade de serviço (RN-GF-28): vale o consumidor registrado para nexio.attachment, o ato e os autores declarados marcados. Expurgo recusado — guarda de qualquer detentor, ou referência de outro escopo criada nesse meio-tempo — leva o pedido a PurgeBlocked, com o código — FILE_LEGAL_HOLD_CONFLICT ou FILE_STILL_REFERENCED_CONFLICT — e a hora, visível na fila ao lado do dueAt, e publica attachment-erasure-request.purge-blocked depois do commit — evento próprio, distinto do attachment-erasure-request.blocked que o registro bloqueado publica; o job segue repetindo, o aviso sai a quem registrou e ao titular, e ao administrador do tenant com o dueAt vencido. Pedido Executed com purgedAt nulo e dueAt vencido escala igual — mesmo aviso, mesma fila, mesmo destaque: do lado do titular o prazo venceu do mesmo jeito, e falha transitória que ninguém repara é indistinguível de bloqueio. Pedido Requested bloqueado com dueAt vencido escala igual, qualquer que seja o bloqueio — aprovação, controle ou alcance —: mesmo aviso, mesma fila, mesmo destaque, com o administrador do tenant na audiência. E o bloqueio por alcance tem saída: quem concede escopo no tenant (update:company-scopes) encerra o pedido com motivo escrito, pela rota de cancelamento (RN-ATT-38); enquanto o alcance de quem registra continuar incompleto, pedido novo bloqueado por alcance sobre o mesmo arquivo responde ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN. A repetição é só do job, com os autores gravados: repetir o ato sobre anexo com expurgo pendente não abre pedido novo nem chama o GrydFiles — devolve 200 com o pedido e a pendência; sobre anexo já expurgado, ATTACHMENT_ALREADY_ERASED_CONFLICT. Enquanto houver pedido Requested, Executed ou PurgeBlocked sobre o arquivo, POST /attachments/copy o recusa (RN-ATT-15).

RN-ATT-34Ler anexo exige o CanRead do dono, em toda rota de leitura

Escopo por empresa e read:attachments são necessários e não bastam. Listagem, metadado, consulta de estado em lote, URL de download, cadeia de propagação, candidatos e origem de cópia, histórico e eliminação perguntam CanRead(user, ownerId) ao descritor. Falso responde 404, como fora de escopo (RN-ATT-05); na listagem a linha não vem. Leitura sobre vários donos pergunta por dono — a implementação pode responder em lote, a semântica é por dono. Dono sem descritor registrado responde falso. O portal não usa esta pergunta: lá valem supplierId e visibility (RN-ATT-12). As respostas estão na matriz de leitura.

RN-ATT-35Toda escrita no GrydFiles acontece depois do commit do Nexio, é idempotente e fica à vista até confirmar

O GrydFiles tem DbContext próprio, e nenhuma transação atravessa os dois. Criar a referência da cópia, liberar referência, estender retainUntil, ligar e desligar legalHold e pedir expurgo são chamadas em processo feitas depois do commit que decidiu o ato: o Nexio grava a decisão com a pendência — CreatePending, ReleasePending, fileRetainUntil abaixo de guardUntil, fileHoldAppliedAt ou fileHoldReleasedAt nulos, purgedAt nulo com Erasure —, chama, e grava a confirmação. Toda chamada é idempotente: referência por (escopo, chave), liberação de referência já liberada, data que só anda para frente. A que falha grava fileSyncAttemptedAt e fileSyncError e é repetida pelo job attachment-guard-release, por tenant, com o tenantId no payload. O job não age em nome de ninguém: nas chamadas que o GrydFiles registra com autor — ligar e desligar legalHold, pedir o expurgo da eliminação — ele leva como dado o autor do ato já gravado (quem colocou ou retirou a guarda; na eliminação, quem registrou e quem executou), e desligar a guarda que o escopo nexio.attachment ligou não pede admin:system. O expurgo da eliminação é pedido sempre pelo job, também a primeira vez, e nunca repetido por usuário (RN-ATT-33). Antes de cada chamada de expurgo o job relê a guarda legal — e só ela. OpenApprovalOf e PendingControlOf são conferidos no registro e relidos sob a trava na execução (RN-ATT-39), nunca depois: passado o commit do ato, o nome já foi neutralizado e as referências, liberadas, e o que resta a preservar são os bytes. A consequência é declarada — decisão ou contestação aberta depois do ato não segura o expurgo; quem precisa dos bytes para ela põe guarda legal por caso, o único instrumento que segura o expurgo depois do ato (RN-ATT-32), e o pedido fica PurgeBlocked. Pendência com prazo vencido escala: pedido Executed com purgedAt nulo e dueAt vencido recebe o mesmo aviso e o mesmo destaque na fila que o PurgeBlocked (RN-ATT-33). É o requisito da RN-GF-27 do GrydFiles e da demanda de plataforma; enquanto não estiver entregue, a pendência fica aberta e à vista, e o arquivo, preso. Pendência nunca some sozinha: a tela de saúde a mostra por idade. Nenhuma pendência solta arquivo antes da hora — o que prende pode atrasar sem perder prova, e o que solta só é chamado depois de a decisão estar gravada.

RN-ATT-36O fileId só vira anexo na reserva de quem o reservou, e a referência esperada é declarada no processo

POST /attachments/upload-intent reserva o arquivo pela IFileService e grava o anexo em Reserved, com dono, tipo, retenção e quem reservou. POST /attachments só aceita fileId de reserva em Reserved ou Expected do mesmo usuário, no mesmo tenant, para o mesmo dono e o mesmo tipo; qualquer outro — arquivo já liberado de outro anexo, reserva de outra pessoa, dono trocado — responde ATTACHMENT_UPLOAD_NOT_RESERVED_CONFLICT, e nada é gravado. É o que impede anexar arquivo de outro dono e ler por aí o que o CanRead dele esconde. A confirmação reconfere o CanWrite do dono antes de chamar o GrydFiles: dono que deixou de aceitar escrita desde a reserva — a proposta bancária submetida — responde ATTACHMENT_OWNER_NOT_WRITABLE_CONFLICT, e nada é confirmado. Aceito, o Nexio confirma pela IFileService declarando a referência esperada ("nexio.attachment", attachment.id) — nunca por rota HTTP, que não aceita dono — e grava Expected; repetir a chamada repete a confirmação e devolve o mesmo anexo. Reprovação na confirmação revoga com FileRejected. Confirmação que deu certo no GrydFiles e não chegou a gravar Expected não deixa referência órfã: o handler do veredito trata Reserved como Expected (RN-ATT-27), e o job relê o arquivo pela IFileService antes de encerrar a reserva — arquivo que já saiu de Pending vai a Expected e segue o veredito; só a reserva que o GrydFiles não confirmou até vencer a URL de upload vai a NeverCreated, sem nunca ter aparecido na lista. No portal, a reserva fica amarrada ao acesso do portal e ao supplierId, no lugar do usuário.

RN-ATT-37Arquivo liberado que o re-scan acha infectado revoga o anexo em qualquer estado do dono, e isso não é eliminação

O GrydFiles leva o arquivo de Available a Infected, libera todas as referências e entrega um veredito por anexo — o arquivo propagado em N documentos recebe N. O handler revoga cada anexo ainda ativo com FileInfected e grava Released; usableAt preenchido é o que diz que ele foi prova. O efeito segue o estado do dono. Draft: aviso a quem anexou, ou ao ResponsibleOf. InProgress: o mesmo aviso e mais os aprovadores pendentes do OpenApprovalOf — a prova de uma decisão em curso deixou de abrir, e decidir o que fazer com a instância é do motor e do dono, não do Anexo. Closed: aviso obrigatório ao ResponsibleOf e a quem anexou — e, se nenhum dos dois estiver ativo, a quem tem create:attachment-legal-holds no tenant, que decide se o caso pede guarda legal; o anexo continua no histórico com a marca "infectado depois do fechamento", guardUntil e as guardas legais continuam valendo, e o objeto fica na quarentena do GrydFiles, cujo expurgo respeita retainUntil e legalHold. Não é eliminação: nenhum expurgo pedido, nome e descrição preservados, nenhum ato de controle. O pedido de eliminação do titular continua possível sobre o arquivo em quarentena, pelo caminho de sempre (RN-ATT-33): o GrydFiles aceita o ato de eliminação também em Infected e Failed (RN-GF-25), e a lápide fica como prova do incidente. O Nexio não oferece o conteúdo em quarentena — acesso forense é da plataforma. O evento attachment.quarantined sai depois do commit.

RN-ATT-38Eliminação de prova de controle, de arquivo Permanent ou de anexo ainda sob guarda exige segunda pessoa, que é outro usuário

O pedido pede segunda pessoa quando algum anexo do arquivo é de tipo com isControlEvidence = true, ou de tipo com retenção Permanent, qualquer que seja a marca, ou está dentro da guarda — dono fora de Draft e qualquer uma destas: referência ainda não liberada, guardUntil no futuro, dono ainda não fechado. Pedindo, nasce Requested e nada é revogado; só executa POST /attachments/erasure-requests/{requestId}/execute, com erase:attachments, por usuário diferente de quem registrou (ATTACHMENT_ERASURE_SAME_PERSON_FORBIDDEN), com as condições da RN-ATT-33 reconferidas e requiresSecondPerson recalculado na hora — só passa de falso a verdadeiro. A segunda pessoa é outro usuário: a comparação é por userId, sem CPF e sem regra de conflito de interesse. É risco aceito que dois usuários da mesma pessoa, ou o aprovador de uma troca fraudulenta, façam as duas pontas; mitigam o ato de controle registrado com os dois autores, o aviso ao segundo e a trilha do ato. Pedido bloqueado (RN-ATT-33) também nasce Requested; sem segunda pessoa, quem registrou pode executá-lo quando o bloqueio cair. No registro bloqueado o aviso sai ao titular pelo requesterEmail — sem nome de arquivo, de documento ou de decisão — ou, sem e-mail, a quem registrou; a fila mostra o bloqueio, calculado na leitura, e o dueAt. O registro publica uma de duas ações, nunca as duas: attachment-erasure-request.blocked quando o pedido nasce travado, com o bloqueio e o dueAt como variáveis, e attachment-erasure-request.registered quando nasce livre, à espera da segunda pessoa. A transição a PurgeBlocked é outro momento e tem nome próprio, attachment-erasure-request.purge-blocked (RN-ATT-33) — nenhum dos três compartilha nome. Pedido que nasce travado não avisa quem executa: ninguém executa enquanto o bloqueio não cai, e o bloqueio é calculado na leitura, sem evento de saída; o pedido espera na fila com o bloqueio à vista, e o dueAt vencido escala ao administrador do tenant (RN-ATT-33). Bloqueio por alcance incompleto é o único que quem registrou não derruba sozinho: o aviso vai também ao administrador do tenant, com as empresas e os tipos de dono que faltam e sem o documento, e executa quem tiver o alcance inteiro; até lá o pedido aparece na fila de quem registrou e na do administrador, sem dono nem número legível. No máximo um pedido Requested por arquivo (ATTACHMENT_ERASURE_REQUEST_ALREADY_EXISTS), conferido depois do escopo e do CanRead — o id do pedido não vai a quem não enxerga os donos; cancelar exige comment, e pedido Executed, PurgeBlocked, Purged ou Cancelled não se executa nem se cancela (ATTACHMENT_ERASURE_REQUEST_CLOSED_CONFLICT). Todo cancelamento avisa o titular. Cancelar devolve o arquivo à propagação (RN-ATT-15) e o anexo ao acervo: o pedido do titular deixou de existir, e não há primeiro cancelamento de rotina que ele possa ignorar. Depois do commit sai attachment-erasure-request.cancelled, e o aviso vai ao titular pelo requesterEmail — sem nome de arquivo nem de documento — ou, sem e-mail, a quem registrou, que responde pelo canal do pedido. Pedido bloqueado por alcance incompleto tem um encerrador a mais: a mesma rota de cancelamento aceita, além de quem tem erase:attachments, quem concede escopo no tenant (update:company-scopes) — e só sobre pedido Requested bloqueado por alcance —, com comment como em qualquer cancelamento e o ato registrado com quem encerrou; fora desse caso a rota exige erase:attachments, e sem ela responde 403 de permissão. É a saída do único bloqueio que ninguém dentro do pedido derruba, e ela não entra na cadeia abaixo: não exige usuário diferente do que cancelou o anterior nem aperta o dueAt do pedido seguinte, porque a cadeia existe contra quem registra e cancela para não executar, e quem encerra aqui não registrou e não podia executar. Depois dele, registro novo bloqueado por alcance sobre o mesmo arquivo é recusado enquanto o alcance continuar incompleto (ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN) — sem isso o laço recomeçava e a propagação ficava presa de novo. Risco aceito: quem concede escopo pode encerrar pedidos de titular em série; mitigam o motivo escrito, o aviso ao titular em todo cancelamento e o ato registrado. Cancelar tem limite: do segundo cancelamento de pedido sobre o mesmo storedFileId em diante, cancelar exige usuário diferente do que cancelou o anterior (ATTACHMENT_ERASURE_CANCEL_SAME_PERSON_FORBIDDEN), e é a partir do segundo que o aviso alcança também o administrador do tenant, e o pedido novo sobre esse arquivo não aceita dueAt posterior ao do pedido cancelado (ATTACHMENT_ERASURE_REQUEST_REQUIRED_UNPROCESSABLE): sem limite, uma pessoa com o mesmo alcance registrava e cancelava para sempre, e o relógio do titular nunca corria. Depois do commit do registro livre, o evento attachment-erasure-request.registered leva o aviso a quem pode executar — erase:attachments, escopo e CanRead em todos os donos, exceto quem registrou —, e, sem ninguém assim, ao administrador do tenant: o pedido fica parado, e nunca volta à mesma pessoa. Sem segunda pessoa e sem bloqueio, o registro executa no mesmo ato e o pedido nasce Executed, com o mesmo usuário nas duas pontas e o ato registrado; sai só attachment-erasure-request.executed, porque não houve pedido à espera de ninguém. O pedido nunca é apagado nem reescrito.

RN-ATT-39A eliminação confere o GrydFiles antes de gravar e trava os donos como a submissão os trava

Antes de qualquer gravação, o registro e a execução perguntam ao GrydFiles, em processo, pela pré-condição do expurgo como eliminação daquele arquivo, por todos os pares do escopo nexio.attachment — a consulta responde o mesmo que o expurgo responderia (RN-GF-25) —, em dois modos. No registro, modo registro: vai um autor só, quem está registrando, e a dupla custódia não é conferida — a segunda pessoa ainda não existe, e exigi-la aí recusaria todo pedido de titular sobre arquivo Permanent, que é justamente o que sempre pede segunda pessoa. Na execução, modo execução: vão os dois autores que o pedido levará, com o executor sendo o usuário do contexto e quem registrou indo como autor declarado — o GrydFiles isenta esse caminho da recusa por autor declarado diferente de quem está no contexto (RN-GF-27), sem a qual a execução por gente responderia 401 e o pedido do titular não se executaria nunca. Falha é recusa sem efeito: guarda legal de qualquer detentor — o escopo nexio.attachment, outro escopo ou a rota do GrydFiles, sem escopo —, que o GrydFiles devolve como FILE_LEGAL_HOLD_CONFLICT → ATTACHMENT_UNDER_LEGAL_HOLD_CONFLICT, que diz só que há guarda; referência ativa de outro escopo, FILE_STILL_REFERENCED_CONFLICT → ATTACHMENT_ERASURE_FILE_REFERENCED_CONFLICT; estado que não aceita a eliminação — Pending ou Scanning; Infected e Failed aceitam (RN-GF-25) —, ou, no modo execução, Permanent sem dois autores distintos (FILE_ERASURE_DUAL_CUSTODY_CONFLICT) → ATTACHMENT_ERASURE_FILE_NOT_PURGEABLE_CONFLICT, com o código do GrydFiles; trilha durável indisponível → FILE_AUDIT_TRAIL_UNAVAILABLE (503). Nenhuma revogação, troca de nome, registro de ato ou attachment.erased acontece antes dessa resposta. Passada a consulta, a execução trava, na sua transação, o storedFileId primeiro — a trava do arquivo, que a cópia (RN-ATT-15), a guarda legal (RN-ATT-32) e outra execução sobre o mesmo arquivo também tomam — e depois cada dono dos anexos com a mesma trava que a submissão daquele dono toma, em ordem de (ownerType, ownerId), para duas execuções não se cruzarem. Sob as travas ela relê o conjunto de anexos do arquivo, OpenApprovalOf, PendingControlOf e as linhas de AttachmentLegalHold, e só então revoga. A trava do arquivo é o que fecha as três janelas que a trava por dono deixava abertas: a cópia gravada entre a leitura do conjunto e o commit, que ficaria ativa com o dado pessoal; a guarda legal ligada no meio, que a execução não veria; e a segunda execução simultânea. A submissão concorrente de um dono comum (ex.: Budget) espera a trava e, ao seguir, encontra os anexos revogados; a execução que chega depois da submissão encontra a instância aberta e recusa. A trava é requisito, não recurso de banco: trava pessimista da linha do dono (por exemplo SELECT … FOR UPDATE) ou token de versão do dono incrementado na mesma transação, com a mesma consequência. Mudança no GrydFiles entre a consulta e o expurgo — guarda ou referência nova — não desfaz o ato: o pedido vai a PurgeBlocked com o código (RN-ATT-33). E o job relê a guarda legal antes de cada chamada: com guarda ativa de qualquer detentor ele não pede o expurgo, e o pedido fica PurgeBlocked — nenhum byte sai debaixo de guarda, que é o que a RN-ATT-32 promete. Dependência de plataforma, com o estado de hoje: a consulta de pré-condição, o expurgo de Permanent com dois autores declarados e o expurgo sem usuário no contexto são requisitos da RN-GF-25, da RN-GF-27 e da demanda de plataforma. Os dois modos, a lista de pares e a isenção do 401 para o autor declarado do ato de eliminação são requisitos da RN-GF-25 e da RN-GF-27, pedidos ao GrydFiles. No código do Gryd.IO (e4eb5a4f) a IFileService não tem a consulta; o expurgo exige admin:system de pessoa no contexto em Permanent e para desligar guarda (FileService.cs:687, :791–803), só expurga arquivo Available (:786), quando a RN-GF-25 abre a eliminação também para a quarentena, e, sem usuário, lança exceção em vez de responder (:831). A eliminação não vai a produção antes disso: sem a consulta, nenhuma execução acontece, e o pedido fica Requested, à vista — o erro possível é para mais. Critérios de aceite: (a) SIGNED_CONTRACT com guarda posta pela rota do GrydFiles → 409 na execução, nenhum anexo revogado, nome intacto, nenhum attachment.erased; (b) Permanent executado por quem registrou → 403 antes da consulta; (c) submissão do dono e execução simultâneas → ou a instância abre e a execução recusa com 409, ou a execução revoga antes e a submissão vê os anexos revogados — nunca instância aberta sobre anexo eliminado; (d) BANK_OWNERSHIP_PROOF de proposta de carga inicial com o lote por confirmar → execução 409 por PendingControlOf, e o pedido continua Requested; (e) POST /attachments/copy daquele arquivo durante a execução → ou espera a trava e é recusado com ATTACHMENT_ERASURE_IN_PROGRESS_CONFLICT, ou entrou no conjunto antes da leitura e é eliminado junto — nunca sobra cópia ativa com o dado pessoal; (f) guarda legal ligada durante a execução → ou ela espera a trava e o ato conclui, ou a execução a vê sob a trava e recusa com 409 — nunca sai attachment-erasure-request.executed com guarda ativa, e o job nunca expurga com guarda ativa; (g) decisão de controle aberta no dono depois do commit do ato → o job segue e o expurgo acontece, que é o comportamento declarado (RN-ATT-35); guarda legal posta antes de a chamada do job sair → nenhum byte apagado e o pedido em PurgeBlocked; (h) registro por quem tem escopo e CanRead no anexo em que age e não em todos os donos dos demais anexos do arquivo → o pedido nasce Requested, bloqueado por alcance, com o dueAt correndo, o titular avisado e o administrador do tenant escalado, e nada revogado; a execução por essa mesma pessoa responde ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN; (i) pedido Requested bloqueado — por aprovação, por controle ou por alcance — com o dueAt vencido → sai o mesmo aviso do PurgeBlocked, com o administrador do tenant na audiência, e o pedido aparece no mesmo destaque da fila, sem que nada seja revogado; (j) encerramento, por quem tem update:company-scopes, de pedido bloqueado por alcance → o pedido fica Cancelled com o motivo registrado, o titular é avisado, POST /attachments/copy daquele arquivo volta a passar (RN-ATT-15) e registro novo bloqueado por alcance sobre o mesmo arquivo responde 403 enquanto o alcance continuar incompleto.

RN-ATT-40Prova de controle que deixa de valer avisa o dono, por qualquer caminho

Anexo de tipo com isControlEvidence = true que perde o veredito avisa o dono — e não só pelo re-scan. Revogação (POST /attachments/{id}/revoke, qualquer revocationSource), eliminação a pedido de titular (RN-ATT-33) e reprovação do arquivo (FileRejected, com o rejectionCode) têm o mesmo efeito que a RN-ATT-37 dá ao veredito Infected: a prova que sustentava um ato de controle deixou de abrir. O aviso sai depois do commit, com a audiência da RN-ATT-37 pelo estado do dono — Draft: quem anexou, ou o ResponsibleOf; InProgress: isso e os aprovadores pendentes do OpenApprovalOf; Closed: obrigatório ao ResponsibleOf e a quem anexou, com fallback a quem tem create:attachment-legal-holds — e leva o tipo, o dono e por que a prova saiu, nunca nome de arquivo. É esse aviso que o dono usa de gatilho: no Fornecedor, o ControlEvidenceLost de BANK_OWNERSHIP_PROOF, SIGNED_CONTACT_CHANGE_REQUEST e IDENTITY_CORRECTION_PROOF — os dois últimos não têm conta bancária atrás e, sem esta regra, não tinham nada que os disparasse. O Anexo avisa; o que fazer com o ato é do dono — reabrir a instância, recusar o desfecho ou exigir prova nova é decisão dele, e o Anexo não muda estado de documento por causa disto. Critérios de aceite: (a) revogação de IDENTITY_CORRECTION_PROOF com a correção de identidade em curso → aviso ao dono, e nada muda do lado do Anexo; (b) eliminação a pedido de titular de BANK_OWNERSHIP_PROOF de lote já confirmado → aviso ao dono, sem nome de arquivo e sem o comentário do pedido; (c) FileRejected de anexo com a marca → o mesmo aviso, com o rejectionCode; (d) anexo sem a marca isControlEvidence → nenhum aviso novo, em nenhum dos três caminhos.

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 art. 173Cinco anos de prazo decadencial para a Fazenda constituir o créditoO retentionYears = 5 da semente, contado do fechamento do dono. É o número que aparece em auditoria, e por isso está na semente e não numa configuração que alguém esquece de preencher
LGPD · art. 16Guarda permitida para cumprimento de obrigação legal ou regulatória, mesmo após o fim do tratamentoA base que faz a guarda vencer o pedido de eliminação — e, sem base, o ato de eliminação (RN-ATT-33). 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, 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.
  • XML da NF-e. É o documento fiscal — chave de acesso, emitente, protocolo — e é entidade da Invoice, pela mesma regra de corte. O DANFE, que é a representação, pode ser anexo.
  • Boleto e comprovante de pagamento. Pagamento é do ERP; o Nexio não guarda nem um nem outro.
  • 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
  • Submeter sozinho quando o anexo liberar — v2. A pessoa pediria, e um processo em segundo plano enviaria o documento para aprovação depois do veredito. Depende de execução delegada — identidade, permissões e escopo de quem pediu, restaurados e reavaliados fora da requisição, com a marca "executado em nome de" —, requisito registrado na demanda de plataforma. Quando voltar, volta com uma pergunta de submissão no descritor do dono; na v1 a pessoa envia (ver § Scanning).
  • 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á.

Nenhuma decisão em aberto. A de § Scanning está fechada, e o ReasonCode está especificado: reasonCodeId aponta 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.

Registro

Histórico de revisões

VersãoDataO que mudou
v1.1730/10/2026Permissões padronizadas em read/lookup/create/update/delete (decisão de 30/10/2026). manage:attachments → create:attachments (POST /attachments/upload-intent, POST /attachments, POST /attachments/copy) e update:attachments (PUT /attachments/{id}, POST /{id}/rescan, rebaixar visibilidade); manage:attachment-types → create:attachment-types (POST), update:attachment-types (PUT) e delete:attachment-types (PATCH /deactivate); manage:attachment-legal-holds → create:attachment-legal-holds (colocar), delete:attachment-legal-holds (retirar, /lift) e read:attachment-legal-holds (listar); manage:company-scopes, citada da Empresa, → update:company-scopes (quem concede escopo). GET /attachment-types, que popula o seletor, passa de read:attachments a lookup:attachments. A tabela de permissões vai de sete a treze linhas.
v1.1621/09/2026Validação de 18/09, achado 25. Um nome por momento na eliminação a pedido do titular. A execução publicava attachment.erasure-requested, o mesmo nome que a Notificação usava para o aviso do registro; passa a publicar attachment-erasure-request.executed, sem aviso. Decisão do Roger: o registro publica uma de duas ações, nunca as duas — attachment-erasure-request.registered quando o pedido nasce livre, à espera da segunda pessoa, e attachment-erasure-request.blocked quando nasce travado; o registro que executa na hora publica só o .executed. Pedido que nasce travado não avisa quem executa; espera na fila, e o dueAt vencido escala como antes (RN-ATT-33, RN-ATT-38, critério (f) da RN-ATT-39, nota da convenção de nomes).
v1.1520/09/2026Recuos da validação de 18/09 (achados 11 e 12). Portal: rota, autenticação e sessão do fornecedor saem desta página e passam a ser citadas do Portal do Fornecedor — a linha de endpoint vira “rota do fornecedor”, e “token da RFQ” e “link de uso único” saem de campo, regra e erro; ficam aqui só a autoria e o isolamento (RN-ATT-12, RN-ATT-21, RN-ATT-36). Entre empresas (RN-ATT-14): sai a descrição de como a cotação adjudica; como a demanda de várias empresas se consolida é da spec da Cotação.
v1.1413/09/2026Correção pós-validação R7. Prazo do titular: o dueAt vencido escala também no pedido Requested bloqueado — por aprovação, por controle ou por alcance —, com o mesmo aviso, a mesma fila e o mesmo destaque do PurgeBlocked e do Executed com purgedAt nulo, e com o administrador do tenant na audiência: bloqueio não para o relógio (RN-ATT-33, RN-ATT-39 critério (i)). Saída do bloqueio por alcance: quem concede escopo no tenant (manage:company-scopes) encerra o pedido bloqueado por alcance incompleto, com motivo escrito, pela rota de cancelamento — fora da cadeia de cancelamentos —, e registro novo bloqueado por alcance sobre o mesmo arquivo é recusado enquanto o alcance continuar incompleto; escolhido o encerramento por gente em vez de caducidade por prazo (RN-ATT-33, RN-ATT-38, RN-ATT-39 critério (j), ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN). Códigos do GrydFiles nomeados: FILE_LEGAL_HOLD_CONFLICT ao lado de ATTACHMENT_UNDER_LEGAL_HOLD_CONFLICT e FILE_STILL_REFERENCED_CONFLICT ao lado de ATTACHMENT_ERASURE_FILE_REFERENCED_CONFLICT, também no purgeBlockedCode, na fila e na RN-ATT-39. Prova de controle: RN-ATT-40 nova — revogação, eliminação a pedido de titular e reprovação de anexo isControlEvidence avisam o dono, como já fazia o re-scan, e é esse aviso que dispara o ControlEvidenceLost do Fornecedor. Menores: a citação do contrato em processo ganha o número e o nome da asserção 10 da Arquitetura; declarado que os sete eventos de domínio do Anexo sem linha no catálogo são eventos sem aviso na v1.
v1.1312/09/2026Correção pós-validação R6. Eliminação: pedido do titular com alcance incompleto deixa de ser recusado no registro — nasce Requested, bloqueado, com o dueAt correndo, o alcance que falta nomeado na leitura, aviso ao titular e a quem registrou e escalonamento ao administrador do tenant; o ATTACHMENT_ERASURE_SCOPE_INCOMPLETE_FORBIDDEN passa a valer só na execução, e a fila e o GET mostram esse pedido a quem o registrou e ao administrador. Prazo vencido: pedido Executed com purgedAt nulo escala como o PurgeBlocked. Cancelamento: o titular é avisado em todos; o aviso ao alcance de controle continua a partir do segundo. Depois do ato só a guarda legal segura os bytes — o job não relê OpenApprovalOf nem PendingControlOf, e a consequência está escrita. Eventos: a transição a PurgeBlocked ganha nome próprio, attachment-erasure-request.purge-blocked, distinto do attachment-erasure-request.registered do registro bloqueado; o cancelamento publica attachment-erasure-request.cancelled. Lote da carga inicial: a lista de falta e de espera passa a ser a que o Fornecedor fecha na RN-FOR-31, copiada literalmente. EV-33 citada (arquivo referenciado por dois escopos não é caso da v1). Erro ATTACHMENT_ERASURE_REQUEST_CLOSED_CONFLICT alinhado aos quatro estados terminais. Fonte sem link de página, como o par declara.
v1.1212/09/2026Correção pós-validação R5. Eliminação: no ato sai attachment.erasure-requested, e attachment.erased só quando o GrydFiles grava purgedAt em todos os pares; recusa do expurgo vira estado próprio PurgeBlocked, com código, hora, dueAt à vista, repetição pelo job e aviso a quem registrou, ao titular e — vencido o prazo — ao administrador (RN-ATT-33, campos purgedAt/purgeBlockedSince/purgeBlockedCode). A consulta de pré-condição roda em dois modos — registro, com um autor só e sem dupla custódia; execução, com o executor do contexto e quem registrou como autor declarado, isento do 401 — e nomeia todos os pares do escopo, original e cópias, liberados na transação do expurgo (RN-ATT-39). Travas: a execução trava o storedFileId antes dos donos; colocar e retirar guarda legal e POST /attachments/copy tomam a mesma trava; a execução relê AttachmentLegalHold sob ela e o job não expurga com guarda ativa (RN-ATT-32, RN-ATT-39, critérios (e) e (f)). Propagação recusada sobre arquivo com pedido em curso (ATTACHMENT_ERASURE_IN_PROGRESS_CONFLICT, RN-ATT-15). Controle pendente: PendingControlOf passa a incluir a janela de contestação com token válido e o alerta ErpBankDivergence em aberto. Prova de controle: o anexo do ato de troca de canal perde a conferência de assinatura — o sistema exige o anexo, o tipo e o veredito e grava o rastro — e nasce IDENTITY_CORRECTION_PROOF para a correção de identidade (onze tipos semeados). Contratos copiados: matriz de leitura com a frase literal do Fornecedor ("não é o remetente — nem pelo usuário, nem pelo CPF") e o remetente na coluna certa; estados do lote exatamente como o Fornecedor os declara; asserção 8 é lista branca de endpoints; frase única do contrato em processo (RN-GF-28). Menores: a rota erase não libera nem expurga — quem faz é o job; o id do pedido só vai depois do escopo e do CanRead; cancelamento repetido exige usuário diferente (ATTACHMENT_ERASURE_CANCEL_SAME_PERSON_FORBIDDEN) e o pedido novo não estica o dueAt.
v1.1111/09/2026Correção pós-validação R4. Eliminação: arquivo de tipo Permanent sempre com segunda pessoa; o registro e a execução consultam a pré-condição de expurgo do GrydFiles antes de gravar — guarda de qualquer detentor, inclusive a sem escopo, estado, dois autores em Permanent — (RN-ATT-39, ATTACHMENT_ERASURE_FILE_NOT_PURGEABLE_CONFLICT); pergunta nova do descritor PendingControlOf (doze perguntas) e recusa na execução com decisão de controle pendente fora da instância (ATTACHMENT_ERASURE_CONTROL_DECISION_PENDING_CONFLICT); o registro com aprovação ou controle pendente é aceito bloqueado, com dueAt e requesterEmail e aviso ao titular; a execução trava os donos como a submissão; "dentro da guarda" inclui guardUntil futuro e dono não fechado; segunda pessoa é outro usuário, risco aceito registrado (RN-ATT-33, RN-ATT-38). GrydFiles: em processo não se confere permissão — consumidor registrado, ato e autor declarado marcado; citação RN-GF-26 → RN-GF-27; expurgo pedido e repetido só pelo job, com os dois autores. Proposta bancária: carga inicial Open = InProgress e aceita anexo até a confirmação; ChainContact sem instância; comprovante do lote sem veredito → ATTACHMENT_SCAN_PENDING_CONFLICT, Failed/Infected/revogado/eliminado → falta, código do Fornecedor; matriz de leitura com a lista da RN-FOR-36 (antes e depois da confirmação). GET /attachments/legal-holds omite o dono sem CanRead. Conferido no código do Gryd.IO (e4eb5a4f): permissões de arquivo só na policy das rotas; admin:system ainda conferido em processo; sem a consulta de pré-condição e sem expurgo sem usuário — a eliminação espera a plataforma (RN-ATT-39). No mesmo dia: a eliminação alcança arquivo em quarentena (Infected, Failed), alinhada à RN-GF-25; citações à RN-GF-28 e a FILE_ERASURE_DUAL_CUSTODY_CONFLICT.
v1.1011/09/2026Correção pós-validação R3. Eliminação: recusada com aprovação em curso em algum dono do arquivo (ATTACHMENT_ERASURE_APPROVAL_IN_PROGRESS_CONFLICT); dupla custódia quando o arquivo é prova de controle ou ainda está sob guarda — entidade AttachmentErasureRequest, rotas POST /attachments/erasure-requests/{requestId}/execute e …/cancel e GET /attachments/erasure-requests, marca isControlEvidence no AttachmentType (RN-ATT-33, RN-ATT-38, RN-ATT-25); o expurgo é pedido como eliminação, com quem executou como autor. Confirmação: reconfere o CanWrite do dono; Reserved conta como pendente na submissão e na confirmação do lote da carga inicial bancária; o handler trata Reserved como Expected e o job relê o arquivo antes de NeverCreated (RN-ATT-19, RN-ATT-27, RN-ATT-36). Leitura: o comprovante bancário segue a lista do Fornecedor — proponente em Draft ou Open, tarefa ativa ou decidida, confirmador do lote; linha sem empresa vale para as empresas do escopo, e escopo vazio não lê nada (RN-ATT-30); rotas de guarda legal no escopo da empresa do dono, caseReference só para quem gere guarda (RN-ATT-32). Plataforma: o job leva o autor do ato como dado e desligar guarda posta pelo próprio escopo não pede admin:system (RN-ATT-35); hosts sem /files/* (asserção 8) separados de permissão de arquivo fora de papel de tenant (catálogo, na demanda). Semente com dez tipos: entra SIGNED_CONTACT_CHANGE_REQUEST. Piso de retenção contado da reserva; índices sem premissa de banco.
v1.911/09/2026Validação de 10/09 e correções pós-validação da onda 1. Leitura e escrita: CanRead entra no descritor no lugar de SubmitPolicyOf, com a matriz de leitura (RN-ATT-34); a guarda de inicialização vale sobre os módulos registrados, e o enum persiste como texto (RN-ATT-03); StateOf pelos status reais das specs donas, com SupplierQualification e BudgetChangeRequest como donos. Arquivo: o veredito chega depois do commit do GrydFiles a um handler idempotente, com os quatro desfechos (RN-ATT-27); o par pendente vira referência esperada, declarada no processo, e o fileId só vira anexo na reserva do mesmo usuário, tenant, dono e tipo (RN-ATT-36); toda escrita no GrydFiles acontece depois do commit do Nexio, com pendência visível (RN-ATT-35); arquivo liberado que o re-scan acha infectado revoga o anexo com efeito por estado do dono, sem ser eliminação (RN-ATT-37); rota POST /attachments/{id}/rescan; o módulo de arquivo não faz mais dedupe. Colunas: fileReferenceReleased dá lugar a fileReferenceStatus; novas retentionMode · retentionYears, guardUntil, fileRetainUntil, usableAt, rejectionCode, fileSyncAttemptedAt · fileSyncError, purgedAt e lastKnownOwnerStateAt; revocationSource ganha FileRejected e Erasure. Estado do dono: chega pelo evento OwnerStateChanged, e o occurredAt do fechamento conta a guarda (RN-ATT-28). Guarda: fim contado do fechamento do dono (RN-ATT-31), GENERAL e TECHNICAL_SPEC com cinco anos; guarda legal por caso em AttachmentLegalHold, que liga também o legalHold do arquivo (RN-ATT-32); eliminação a pedido de titular, alcançando todas as cópias ou nenhuma (RN-ATT-33); a sobreposição do tenant só alonga a retenção (RN-ATT-25). Donos e portal: propagação entre empresas com escopo nas duas (RN-ATT-14); o portal devolve o que o próprio fornecedor subiu, e anexo de resposta nunca vira Supplier (RN-ATT-12); BankAccountChangeProposal em Draft até a submissão, escrita só pelo proponente em rascunho e leitura por read-unmasked:supplier-banks; tipo semeado BANK_OWNERSHIP_PROOF; XML da NF-e é entidade da Invoice, e boleto sai da semente. Contratos: submissão diferida fora da v1 (v2 em § Fora de escopo); read:files no plural; dez sufixos de erro; REASON_CODE_REQUIRED_UNPROCESSABLE do Motivo no lugar do erro próprio; notas de data saem do corpo.
v1.5 – v1.808–09/09/2026Validação de 08/09. A FileReference deixa de nascer na confirmação e passa a nascer na liberação; o contrato de veredito é declarado; lastKnownOwnerState para a varredura de órfãos; perguntas OpenApprovalOf e ResponsibleOf; companyId nulo para dono do tenant (RN-ATT-30); donos ApprovalRequest, Supplier, Item, Budget, BankAccountChangeProposal e QuotationRequestItem com respostas declaradas; Failed bloqueia a submissão; a propagação da linha de requisição passa pela cotação; certidão de fornecedor volta a ser SupplierDocument.
v1.1 – v1.403–06/09/2026Decisão de § Scanning fechada com a spec de Notificação, que trouxe a nona pergunta do descritor; reasonCodeId passa a apontar para o ReasonCode, uso AttachmentRevoke; sobreposição do tipo global pelo tenant (achado A6 da contra-análise); RequisitionLine → RequisitionItem e PurchaseOrderLine → PurchaseOrderItem — linha de documento é <Documento>Item.
v1.002/09/2026Primeira versão: duas camadas com a regra de corte pelo metadado, dono polimórfico com registro de descritores e três mitigações, visibilidade Internal | Supplier, propagação explícita com referência por cópia, revogação conforme o estado do dono, retenção declarada pelo tipo, anexo do portal amarrado ao supplierId.