cadrumo.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 cadrumo.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 TRANSACTION_CATALOGUE_NAMESPACE, which names the persisted envelope contract.

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 a write-path optimization: it 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 a GC-recycle hazard – 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.

_transaction_revision_cache is the corresponding read-path cache for immutable transaction rows. Targeted reads first query the stored revision ids without loading ciphertext and reuse a validated Transaction only while that exact revision remains current. A changed, deleted, or revision-less row is loaded and validated again, so the cache cannot hide an external writer or legacy payload.

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_revision()[source]

Return a digest that changes whenever this bucket’s catalogue does, without decrypting rows.

The digest covers the membership index’s revision and every listed row’s stored revision, so an added, removed or rewritten transaction each changes it. None means no stable revision can be stated – a listed row is missing, or a row predates recorded revisions – and a caller must then treat the catalogue as changed.

Return type:

str | 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:

TransactionCatalogue

Returns:

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

Raises:
migrate_iva_deduction_authority(*, asset_profile_id)[source]

Migrate the catalogue and return its reciprocal Bienes authority.

Return type:

BienesInversionIvaRegister

Parameters:

asset_profile_id (str)

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].

load_by_ids(transaction_ids)[source]

Return only the securely addressed transaction rows.

This is the targeted counterpart to load(): callers that already possess authoritative contributor ids need not decrypt and validate an unrelated full bucket merely to fingerprint those contributors. The same row-schema and addressed-identity checks run in _load_transactions_by_ids(); nonexistent ids are omitted exactly as they would be from a full-catalogue lookup.

Return type:

TransactionCatalogue

Parameters:

transaction_ids (Iterable[str])

partition_by_date_range(start, end)[source]

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

The period-first partition 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 OutOfWindowTransactionIndexEntry 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 (aeat-ledger-contract): a stale index costs a slower read, never a silent drop from either half.

Membership in the in-window half is decided by OVERLAP against each row’s transaction_eligible_date_span(), not by its filing date. A consuming aggregator applies its own tax-timing gate to the decrypted rows, so this half is a candidate superset: a row booked in one quarter that carries an IVA criterio-de-caja devengo or collection date in another is decrypted and offered to the aggregator rather than reported out-of-window undecrypted, which would drop its observation with no diagnostic. Both halves stay disjoint and total.

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

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

Return type:

LedgerDatePartition

Returns:

The LedgerDatePartition for [start, end].