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
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 a write-path optimization: it 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 a GC-recycle hazard – 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._transaction_revision_cacheis the corresponding read-path cache for immutable transaction rows. Targeted reads first query the stored revision ids without loading ciphertext and reuse a validatedTransactiononly 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:
bucket_id (str)
objects (SecureObjectRepository | None)
- 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.
Nonemeans 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.
- 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
TRANSACTION_CATALOGUE_NAMESPACE.sensitivity.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.
- migrate_iva_deduction_authority(*, asset_profile_id)[source]¶
Migrate the catalogue and return its reciprocal Bienes authority.
- Return type:
- Parameters:
asset_profile_id (str)
- 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].
- 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:
- Parameters:
- 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
TransactionDateIndexRowrows 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 plaintextOutOfWindowTransactionIndexEntryrows (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 (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:
- Return type:
- Returns:
The
LedgerDatePartitionfor[start, end].