cadrumo.application.live.notification_documents module

Encrypted custody for the documents AEAT serves behind a notificación.

One capture, three writes, in this order: the PDF bytes go into the encrypted content-addressed adapters.persistence.storage.attachment.AttachmentStore, the sanción reading is derived from those bytes in memory, and a NotificationDocumentRecord binding the certificado to the resulting attachment_id is persisted as its own encrypted secure-object row.

The bytes never touch the filesystem. They arrive from the sede adapter in memory, are handed straight to the attachment store’s encrypted blob namespace, and are dropped. There is no temp file, no scratch directory, no plaintext cache, no on-disk extraction step, and nothing sensitive reaches a log line — only the certificado id, the digest and the byte count, which say nothing about what the document contains. There is also no browser-opening path here and none is to be added: displaying an AEAT notification is the taxpayer’s own act, not this application’s.

The legal guard governs this whole module by construction rather than by convention. Fetching runs through fetch_notification_document(), which calls assert_notification_content_readable() before anything crosses the wire, so a notification AEAT has not recorded as read produces no AEAT contact, no bytes, no attachment and no record. This module re-asserts the same guard before it does any work of its own — not because the adapter’s check is doubted, but because a future caller reaching this service by another route must meet the guard too, and a guard that sits only at the bottom of a call chain protects only the callers that go through the bottom.

See also

adapters.inbound.notificacion

Deterministic reader for the fetched document’s text layer.

domain.attachments

The byte-custody tier this service writes through; it records what the bytes are, never a fiscal figure about them.

exception NotificationDocumentNotFoundError(message=None, *, context=None, translated_message=None)[source]

Bases: SnapshotNotFoundError

Raised when no stored document record matches the requested certificado.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
class NotificationDocumentRecord(**data)[source]

Bases: BaseModel

The custody record binding one notification to its stored document bytes.

This record is a POINTER plus provenance, never the bytes: the bytes live in the encrypted attachment store under attachment_id, which is their own SHA-256. Holding the digest in both places is deliberate — the record can be verified against custody without decrypting anything.

Variables:
  • certificado_id – The notification the document was served under. Also the record’s storage key, so one notification owns exactly one row and a retried pull neither duplicates it nor rewrites it.

  • bucket_id – The profile bucket that owns both this record and the bytes.

  • attachment_id – The stored bytes’ content address in the encrypted attachment store.

  • document_sha256 – Digest of the fetched PDF, equal to attachment_id by construction and carried explicitly so a reader never has to know that.

  • byte_size – Size of the fetched PDF.

  • source_url – The AEAT detail endpoint the document was served from.

  • fetched_at – When the fetch completed.

  • sancion – The deterministic reading of the document when it is a sanción or liquidación act that parsed completely. None when the document is another kind, or when the reader REFUSED — a refusal is recorded as an absent reading, never as a zeroed one, and the bytes are kept either way so an operator can read them themselves.

  • parse_refusal – Why the reading is absent, required exactly when it is. Carried so the refusal survives in the record instead of being lost to a log line.

  • mode – Structural read-only marker.

Parameters:

data (Any)

certificado_id: AeatCertificadoId
bucket_id: BucketId
attachment_id: Hex64Str
document_sha256: ContentDigest
byte_size: int
source_url: str
fetched_at: datetime
sancion: SancionLiquidacion | None
parse_refusal: NotificationParseRefusal | None
mode: Literal['read']
property snapshot_id: str

Return the record’s storage address, which IS its certificado.

The secure-object repository addresses every payload by a snapshot_id; here that address is the certificado itself, so one notification owns exactly one row rather than accumulating near-duplicates of the same served document.

class NotificationDocumentCustody(**data)[source]

Bases: BaseModel

One custody attempt’s outcome: the record, and whether it was already held.

The record alone cannot answer whether the attempt stored anything. A retry against a certificado already in custody returns the row that was there before, byte-identical to a first store’s return, so a caller holding only the record has no way to tell an ingest from a no-op — and the CLI contract requires a guarded no-op to say so, both in its result shape and on the notices channel.

Carrying the answer on the return value rather than re-deriving it at the caller is what keeps ONE authority for the decision: the service compares the caller-supplied fields and decides, and every surface reads that decision instead of asking custody a second question of its own.

Variables:
  • record – The persisted record — the row already in custody when this attempt was a no-op, the newly written one otherwise.

  • already_in_custody – Whether the attempt stored nothing because the certificado was already held with agreeing content.

Parameters:

data (Any)

record: NotificationDocumentRecord
already_in_custody: bool
notification_document_object_key(bucket_id, certificado_id)[source]

Build the secure-object key for one bucket’s notification-document record.

Return type:

str

Parameters:
  • bucket_id (str)

  • certificado_id (str)

class NotificationDocumentService(*, settings, attachment_store, repository_factory, content_guard, document_fetcher, document_reader)[source]

Bases: object

Bucket-scoped custody and read surface over fetched notification documents.

The service is structurally read-only towards AEAT: it fetches and stores, and there is no verb here that acknowledges, submits, signs or otherwise changes anything on AEAT’s side.

Parameters:
persist_document(*, bucket_id, row, document)[source]

Take custody of already-fetched document bytes and record the binding.

Split from pull_document() so the custody boundary is exercisable without a browser: everything below the wire — encryption, the content-addressed write, the reading, the record — runs here.

Re-storing a certificado already in custody is a content-addressed no-op. A non-interactive caller may retry, and the AEAT document behind one certificado is immutable — the act was served once. So a retry carrying the same bytes returns the record that is already stored: no second attachment write, no re-run of the reading, no re-stamped fetched_at, and no second secure-object write. A retry that carries DIFFERENT content refuses instead, because either the stored row or the incoming one is wrong about a taxpayer’s served act and overwriting silently would destroy the evidence of which.

Parameters:
  • bucket_id (str) – The profile bucket taking custody.

  • row (NotificationRowProtocol) – The notification the document belongs to. Re-checked against the legal guard before anything is written.

  • document (NotificationDocumentProtocol) – The fetched bytes, held in memory.

Return type:

NotificationDocumentCustody

Returns:

A NotificationDocumentCustody carrying the persisted record — the one already in custody when this call was a no-op — and the flag saying which of the two happened.

Raises:
  • SedeNavigationError – When row is not a notification AEAT has already recorded as read.

  • LiveApplicationInputError – When the document does not belong to the supplied row, or when a document is already in custody for this certificado and the incoming one disagrees with it.

async pull_document(*, bucket_id, session, row)[source]

Fetch one already-read notification’s document and take custody of it.

The guard runs twice on this path — here, and again inside fetch_notification_document() before any request is issued. The first check is what guarantees a refused row produces no AEAT contact at all rather than a request that is discarded afterwards.

Parameters:
  • bucket_id (str) – The profile bucket taking custody.

  • session (object) – An authenticated AEAT session.

  • row (NotificationRowProtocol) – The notification to fetch. Must be one AEAT already records as read.

Return type:

NotificationDocumentCustody

Returns:

The NotificationDocumentCustody outcome. A second pull of the same certificado re-fetches the served bytes — AEAT’s own record of a redisplay is not this application’s to suppress — but stores nothing and reports already_in_custody.

Raises:

SedeNavigationError – When row is anything other than already read, or when the AEAT landing is off-policy.

show(*, bucket_id, certificado_id)[source]

Return the stored record for one certificado, or raise if none is stored.

Return type:

NotificationDocumentRecord

Parameters:
  • bucket_id (str)

  • certificado_id (str)

list_documents(*, bucket_id)[source]

Return every stored document record for the bucket, newest fetch first.

Return type:

tuple[NotificationDocumentRecord, ...]

Parameters:

bucket_id (str)