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
TransactionCatalogueRepositoryProtocolDomain port this concrete persistence adapter implements.
TransactionDomain transaction payload stored one encrypted row at a time.
TRANSACTION_CATALOGUE_NAMESPACECentral namespace, sensitivity, schema-version, and object-key contract for transaction secure objects.
SecureObjectRepositoryRuntime-created encrypted storage boundary used for atomic batches.
ledgerApplication ledger workflows that consume this repository through the transaction catalogue boundary.
- class TransactionCatalogueRepository(*, bucket_id, objects=None)[source]¶
Bases:
objectRepository 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 (keyedtransaction:{bucket_id}:{transaction_id}) inside theTRANSACTION_CATALOGUE_NAMESPACEnamespace, so two operator profiles never share transaction storage and a single-transaction mutation touches a single row. EachTransactionpayload and the bucket membership index are wrapped inEnvelopebeforeSecureObjectRepositorypersists them. The class exposes the concrete load/save implementation behindTransactionCatalogueRepositoryProtocol._serialized_hash_cacheis the O3 write-path lever (2026-07-06-ledger-perf-optimization-adr): memoizes the stored-envelope SHA-256 of each loaded frozenTransactioninstance, populated once per row atload()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 becauseraw_fieldsis stored as amappingproxy(unhashable), which rules out a plainWeakKeyDictionary(it hashes the key object itself). A bareid()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 afinalizecallback that evicts theid()entry the INSTANT itsTransactionis garbage-collected, before the address could be recycled for a different object.Transactionis strict-frozen, so a content edit always produces a NEW instance rather than mutating the loaded one; the edited instance’sid()is simply absent from the cache (a miss, correctly falling through to fresh serialize-and-hash). The cache never substitutes for the save-timenamespace_payload_hashesstore-side scan; it only skips re-deriving the FRESH-SERIALIZATION side of that comparison for rows the same process already loaded unchanged.- Parameters:
bucket_id (str)
objects (SecureObjectRepository | None)
- 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:
- Returns:
The deserialised
TransactionCatalogue, or a fresh empty instance when this bucket has no transactions.- Raises:
ClassificationError – If a row’s inner envelope class is not
SensitivityClass.FINANCIAL.EnvelopeVersionError – If a row’s inner envelope schema version is higher than the consumer supports.
StoredTransactionDriftError – If a row payload fails pydantic schema validation on deserialization.
- save(catalogue)[source]¶
Persist
catalogueas 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) – TheTransactionCatalogueto persist.- Return type:
- save_with_secure_object_writes(catalogue, extra_writes)[source]¶
Persist
catalogueplus 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:
catalogue (
TransactionCatalogue) – TheTransactionCatalogueto persist.extra_writes (
tuple[SecureObjectWrite,...]) – Additional secure object writes to commit atomically.
- Return type:
- load_for_date_range(start, end)[source]¶
Return the persisted catalogue filtered to
[start, end]inclusive.Reads the plaintext, non-sensitive
TransactionDateIndexRowrouting rows for this bucket to select the candidate transaction ids whose filing date (value_dateorbooked_date) falls in the window, then decrypts only those rows via one targeted batchload_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:
- Return type:
- Returns:
The
TransactionCataloguecontaining 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 plaintextTransactionDateIndexRowrows 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 batchload_many(); out-of-window ids are reported as plaintextOutOfWindowTransactionStubrows (id + filing date only, never decrypted). On a completeness MISMATCH – a stale or partially-synced index – this falls back to a fullload()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
LedgerDatePartitionshape;index_completerecords which path served the read.- Parameters:
- Return type:
- Returns:
The
LedgerDatePartitionfor[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 fullload()(decrypting every row once) and rewrites the index rows for this bucket to exactly match it.- Return type:
- Returns:
The number of index rows written for this bucket.