aeat.adapters.persistence.profile.transactions module

Encrypted SQL repository for the transaction catalogue.

TransactionCatalogueRepository is the only sanctioned read/write path for the transaction catalogue. It stores one encrypted secure-object row per transaction — keyed transaction:{bucket_id}:{transaction_id} inside the aeat.domain.transactions.bucket namespace at SensitivityClass FINANCIAL — so a single-transaction mutation rewrites only that row instead of re-encrypting the whole catalogue (the prior single-blob shape was O(n) write amplification per ledger edit). Each row wraps its Transaction in an Envelope before serialisation; no plaintext transaction row, JSON catalogue, or envelope file lands on disk.

This concrete repository is the persistence adapter behind the read-side TransactionCatalogueRepositoryProtocol. It lives in the persistence adapter (not in transactions) because its secure-object coupling is SQL/crypto-bound; the domain package owns only the pure surface — the ImportSummary record, the transaction_object_key() / transaction_index_object_key() key-derivation helpers, and the TX_BUCKET_NAMESPACE / schema-version constants that name the persisted envelope contract. The namespace/version constants are redeclared here as the persisted-envelope contract; the strings are preserved to avoid orphaning stored envelopes.

Writes go through the SecureObjectRepository atomic upsert+delete batch (apply_batch()) so a multi-transaction mutation — and any sibling-catalogue co-writes (bucket-event history, invoices) passed to save_with_secure_object_writes — commit all-or-nothing, preserving the co-write atomicity the single-blob save had. The diff that decides which rows to write or delete is driven by a decryption-free namespace_payload_hashes() scan, so an unchanged transaction is never rewritten.

See also

TransactionCatalogueRepositoryProtocol

Domain port this concrete persistence adapter implements.

Transaction

Domain transaction payload stored one encrypted row at a time.

TRANSACTION_CATALOGUE_NAMESPACE

Central namespace, sensitivity, schema-version, and object-key contract for transaction secure objects.

SecureObjectRepository

Runtime-created encrypted storage boundary used for atomic batches.

ledger

Application ledger workflows that consume this repository through the transaction catalogue boundary.

class TransactionCatalogueRepository(*, bucket_id, objects=None)[source]

Bases: object

Repository over the encrypted SQL-backed transaction catalogue.

Every instance is bound to one profile bucket via bucket_id. The catalogue is stored as one secure-object row per transaction (keyed transaction:{bucket_id}:{transaction_id}) inside the TRANSACTION_CATALOGUE_NAMESPACE namespace, so two operator profiles never share transaction storage and a single-transaction mutation touches a single row. Each Transaction payload and the bucket membership index are wrapped in Envelope before SecureObjectRepository persists them. The class exposes the concrete load/save implementation behind TransactionCatalogueRepositoryProtocol.

_serialized_hash_cache is the O3 write-path lever (2026-07-06-ledger-perf-optimization-adr): memoizes the stored-envelope SHA-256 of each loaded frozen Transaction instance, populated once per row at load() and consulted by _reconcile() before re-serializing an untouched row.

Keying is identity-based (id(transaction)), not value-based: Transaction’s pydantic-generated __hash__ is unusable as a dict key because raw_fields is stored as a mappingproxy (unhashable), which rules out a plain WeakKeyDictionary (it hashes the key object itself). A bare id() integer key alone would risk the GC-recycle hazard the ADR warns against – a collected instance’s address could be reused by an unrelated object – so each cache entry is paired with a finalize callback that evicts the id() entry the INSTANT its Transaction is garbage-collected, before the address could be recycled for a different object. Transaction is strict-frozen, so a content edit always produces a NEW instance rather than mutating the loaded one; the edited instance’s id() is simply absent from the cache (a miss, correctly falling through to fresh serialize-and-hash). The cache never substitutes for the save-time namespace_payload_hashes store-side scan; it only skips re-deriving the FRESH-SERIALIZATION side of that comparison for rows the same process already loaded unchanged.

Parameters:
property bucket_id: str

Return the profile bucket id this repository is bound to.

exists()[source]

Return whether this bucket holds any persisted transactions.

Return type:

bool

load()[source]

Return the persisted catalogue, assembled from this bucket’s rows.

The per-bucket membership index names exactly the transaction ids this bucket owns; only the rows whose digest the index lists are read, so a shared secure store never leaks another bucket’s transactions.

Return type:

TransactionCatalogue

Returns:

The deserialised TransactionCatalogue, or a fresh empty instance when this bucket has no transactions.

Raises:
save(catalogue)[source]

Persist catalogue as per-transaction encrypted rows.

Only rows whose content changed are rewritten; transactions removed from the catalogue are deleted. The whole diff commits atomically.

Parameters:

catalogue (TransactionCatalogue) – The TransactionCatalogue to persist.

Return type:

None

save_with_secure_object_writes(catalogue, extra_writes)[source]

Persist catalogue plus related secure objects in one unit of work.

The per-transaction diff (changed rows + deletions) and extra_writes (e.g. bucket-event history, invoice catalogue) commit atomically, so a ledger mutation and its co-emitted records remain all-or-nothing.

Parameters:
Return type:

None

load_for_date_range(start, end)[source]

Return the persisted catalogue filtered to [start, end] inclusive.

Reads the plaintext, non-sensitive TransactionDateIndexRow routing rows for this bucket to select the candidate transaction ids whose filing date (value_date or booked_date) falls in the window, then decrypts only those rows via one targeted batch load_many() – never a full-namespace scan-and-decrypt of every row in the bucket.

The index is a derived, rebuildable cache: correctness never depends on it being present or complete. When the index has no rows for this bucket, or its row count for this bucket diverges from the encrypted membership index (a staleness signal – e.g. a row written before this index existed, or a prior crash between the two writes), this method transparently falls back to a full load() and filters in memory, exactly reproducing the pre-index result.

Parameters:
  • start (date) – Inclusive lower bound of the filing-date window.

  • end (date) – Inclusive upper bound of the filing-date window.

Return type:

TransactionCatalogue

Returns:

The TransactionCatalogue containing only transactions whose filing date falls within [start, end].

partition_by_date_range(start, end)[source]

Split this bucket’s catalogue into an in-window half and an out-of-window remainder.

The O2 period-first partition (2026-07-05-ledger-latency-budget-adr): runs a completeness gate against the plaintext TransactionDateIndexRow rows for this bucket – the index row count and id set must exactly match the encrypted membership index – before trusting the index for a partition. On a completeness match, only the in-window transaction ids are decrypted through one targeted batch load_many(); out-of-window ids are reported as plaintext OutOfWindowTransactionStub rows (id + filing date only, never decrypted). On a completeness MISMATCH – a stale or partially-synced index – this falls back to a full load() and partitions the result in memory, so correctness never depends on the index being present or fresh (ledger-participation-index-is-derived-rebuildable): a stale index costs a slower read, never a silent drop from either half.

Both paths return the identical LedgerDatePartition shape; index_complete records which path served the read.

Parameters:
  • start (date) – Inclusive lower bound of the filing-date window.

  • end (date) – Inclusive upper bound of the filing-date window.

Return type:

LedgerDatePartition

Returns:

The LedgerDatePartition for [start, end].

rebuild_date_index()[source]

Rebuild this bucket’s plaintext date index from the encrypted catalogue.

The index is derived and rebuildable (ledger-participation-index-is-derived-rebuildable): correctness never depends on it, so this is an explicit maintenance/recovery operation, not something callers need on the normal read/write path. Performs a full load() (decrypting every row once) and rewrites the index rows for this bucket to exactly match it.

Return type:

int

Returns:

The number of index rows written for this bucket.