aeat.domain.transactions._models module¶
Strict immutable transaction-catalogue boundary models.
Defines Transaction, ClassificationHistoryEntry, and TransactionCatalogue.
Every model is strict + frozen + extra="forbid"; no dataclasses or bare dict[str, Any] at the boundary.
- derive_transaction_id(raw)[source]¶
Return the stable transaction hash for one raw transaction.
This content hash is the single authority for storage, audit, and machine consumers, and it intentionally changes when an id-affecting fact is edited (an
updatere-derives it and records the superseded id as aprevious_transaction_idon the heir’sTransactionEditLineageEntrychain). The operator-facing lineage convenience that lets an old, written-down handle still resolve to the current row throughledger history/view/track(seeapplication.ledger.resolve_lineage_transaction_id()) is a read-side lookup layer over this authoritative id; it never freezes or re-mints the id, so the content-addressing invariant import dedup relies on is untouched.- Parameters:
raw (
RawTransaction) – The upstream immutable raw transaction emitted by a provider.- Return type:
- Returns:
A lowercase SHA-256 digest derived from the provider identity, effective value date, amount, and narrative fields.
- normalise_movement_reference(value)[source]¶
Return a provider-agnostic normalised form of a transaction narrative.
OFX and CSV exports of the same bank movement describe it with different verbatim narratives (an OFX
MEMOversus a CSV reference column), and a later manual edit may further reword the description. Cross-format and post-edit deduplication therefore cannot key on the raw narrative.This collapses a narrative to a stable comparison token: Unicode is NFKD-decomposed and combining accents are dropped (
Ó->o), the result is lower-cased, and every run of non-alphanumeric characters is squeezed out. Two narratives that differ only in accents, casing, punctuation, or whitespace map to the same token.
- derive_import_fingerprint(raw, *, direction=None)[source]¶
Return the stable cross-format import-dedup fingerprint for a raw row.
Unlike
derive_transaction_id()— which keys on the provider identifier and the verbatim narrative and therefore changes when a transaction is edited or re-exported in a different file format — this fingerprint keys only on the movement identity an operator would recognise: the effective date, amount magnitude, currency, direction, and the normalised narrative (seenormalise_movement_reference()).The fingerprint is stamped onto
Transactionat import time and carried verbatim through every later edit, so re-importing the same statement (or the same movements exported as a different file format) recognises the row as already present. Import callers that have parsed flow direction must pass it; callers without a parse-boundary direction receive an explicitUNSPECIFIEDdiscriminator.- Return type:
- Parameters:
raw (RawTransaction)
direction (TransactionDirection | str | None)
- derive_movement_day_key(raw)[source]¶
Return the coarse (effective date, amount) key for a raw row.
Two rows that share this key but not the full
derive_import_fingerprint()are likely — but not confidently — the same movement: same day, same amount, divergent narrative. The import path uses this to warn the operator about a probable cross-format duplicate rather than silently importing it.- Return type:
- Parameters:
raw (RawTransaction)
- class DecisionProvenance(**data)[source]¶
Bases:
BaseModelTyped provenance for one classification decision.
Carries the classifier that decided (
decided_by, in the sameauto/manual/rule:<id>/llm:<model>/derived:<basis>shape asClassificationHistoryEntry.classified_by), when it decided (decided_at), the free-text justification, an optional confidence in[0, 1], and whether the decision was a manual override of an automated classification. This is the typed replacement for the formerlydict-widened reservedClassificationHistoryEntry.provenancepayload; a persisted record must carry a typed provenance, never a baredict[str, object].- Variables:
decided_by – Classifier source string in the approved
auto/manual/rule:<id>/llm:<model>/derived:<basis>shape.decided_at – Timezone-aware UTC timestamp of the decision.
reason – Free-text justification (may be empty).
confidence – Optional decision confidence in
[0, 1].manual_override –
Truewhen the decision manually overrode an earlier automated classification.
- Parameters:
- decided_by: str¶
- decided_at: datetime¶
- reason: str¶
- confidence: Decimal | None¶
- manual_override: bool¶
- class ClassificationHistoryEntry(**data)[source]¶
Bases:
BaseModelOne frozen record in a transaction’s classification chain.
The
confidenceandprovenancefields default toNoneand are populated by writers without a schema bump because the field list is stable;provenanceis the typedDecisionProvenancerecord (never a baredict).- Variables:
business_classification – The
BusinessClassificationdecided at this point in the chain.business_pct – Required when
business_classificationisBusinessClassification.MIXED; must beNoneotherwise. Coupling enforced via_validate_business_pct_coupling().classified_at – Timezone-aware UTC timestamp of the decision.
classified_by – Classifier source string in the
auto/manual/rule:<id>/llm:<model>/derived:<basis>shape.reason – Free-text justification (may be empty).
category_id – Optional
domain.categories.SpendingCategoryforeign key.notes – Free-text notes (may be empty).
confidence – Optional decision confidence in
[0, 1].provenance – Optional reserved provenance payload; the pydantic type intentionally widens to a dict so future writers can replace it with a typed record without a breaking schema change.
- Parameters:
business_classification (BusinessClassification)
business_pct (Decimal | None)
classified_at (datetime)
classified_by (str)
reason (str)
category_id (str | None)
notes (str)
confidence (Decimal | None)
provenance (DecisionProvenance | None)
- business_classification: BusinessClassification¶
- business_pct: Decimal | None¶
- classified_at: datetime¶
- classified_by: str¶
- reason: str¶
- category_id: str | None¶
- notes: str¶
- confidence: Decimal | None¶
- provenance: DecisionProvenance | None¶
- class TransactionEvidenceProvenanceEntry(**data)[source]¶
Bases:
BaseModelActor/source lineage for evidence linked to one transaction.
- Parameters:
- evidence_id: str¶
- evidence_kind: Literal['purchase_invoice_evidence', 'attachment']¶
- actor: str¶
- source_command: str¶
- linked_at: datetime¶
- bucket_event_id: str | None¶
- class TransactionEditLineageEntry(**data)[source]¶
Bases:
BaseModelOne durable manual correction applied to a transaction row.
- Parameters:
- previous_transaction_id: TransactionId¶
- actor: str¶
- source_command: str¶
- edited_at: datetime¶
- bucket_event_id: str | None¶
- class TransactionLifecycleLineageEntry(**data)[source]¶
Bases:
BaseModelOne durable lifecycle transition applied to a transaction row.
- Parameters:
previous_state (TransactionLifecycleState)
state (TransactionLifecycleState)
actor (str)
source_command (str)
changed_at (datetime)
reason (str)
bucket_event_id (str | None)
- previous_state: TransactionLifecycleState¶
- state: TransactionLifecycleState¶
- actor: str¶
- source_command: str¶
- changed_at: datetime¶
- reason: str¶
- bucket_event_id: str | None¶
- class SplitLineage(**data)[source]¶
Bases:
BaseModelSplit-lineage anchor embedded on a parent/child/merged transaction.
- Variables:
split_group_id – Lowercase 64-char SHA-256 derived deterministically by
derive_split_group_id()from the parent’stransaction_idplus the sorted child amounts and narratives. Identical inputs yield identical group ids so re-emission is idempotent by construction.role – Position in the lineage — PARENT, CHILD, or MERGED.
sibling_transaction_ids – For PARENT, every child id; for CHILD, the parent id followed by every other child id; for MERGED, the cohort of merged child ids (the original parent id is not included — the parent has its own MERGED entry on the archived parent record). Sorted lexicographically.
- Parameters:
- split_group_id: str¶
- role: SplitRole¶
- sibling_transaction_ids: tuple[str, ...]¶
- derive_split_group_id(*, parent_transaction_id, child_amounts, child_narratives)[source]¶
Deterministically derive the
split_group_idfor a split cohort.Identical (parent_id, amounts, narratives) tuples yield an identical group id; this is what makes split-event re-emission idempotent. Caller is responsible for amount/narrative pairing — the function sorts amounts and narratives independently before hashing because the group id identifies the cohort, not the per-child ordering.
- class Transaction(**data)[source]¶
Bases:
BaseModelImmutable transaction wrapper that preserves raw provenance verbatim.
- Variables:
transaction_id – Lowercase 64-char SHA-256 derived deterministically from the wrapped raw record by
derive_transaction_id(). Re-validated on every parse to detect tampering.raw – The verbatim
domain.transactions._raw_transaction.RawTransaction.direction – Closed
TransactionDirection.business_classification – Current
BusinessClassificationdecision; defaults toBusinessClassification.NOT_YET_PROCESSED.business_pct – Required when
business_classificationisBusinessClassification.MIXED;Noneotherwise.invoice_id – Optional invoice foreign key.
category_id – Optional
domain.categories.SpendingCategoryforeign key.taxable_base – Optional IVA-exclusive base amount.
iva_rate – Optional IVA rate expressed as a decimal fraction.
iva_amount – Optional IVA amount on the row.
irpf_category – Optional IRPF-specific category key.
usage_ratio_id – Optional proportionality reference.
prorrata_reference – Optional IVA prorrata substrate reference.
art_104_tres_exclusion – Operator-declared LIVA art. 104.Tres denominator-exclusion tag. Set ONLY for the two judgment exclusions the ledger cannot infer – foreign permanent establishment (1.º) and non-habitual inmobiliario/financiero operations (4.º); the transaction boundary rejects any auto-derived member (art. 7 no-sujeta, art. 9.1.d autoconsumo, bienes-inversión disposal, direct cuotas) since those are recognised from the category / register / structure. When set, the annual prorrata volume rollup excludes this operation from both terms of the art. 104.Dos ratio; the operation’s own IVA cuota treatment is unaffected.
Nonefor every operation that is not an art. 104.Tres judgment exclusion.input_classification – Operator-declared LIVA art. 106 prorrata-especial per-input use classification (
InputClassification):EXCLUSIVELY_DEDUCTIBLE(regla 1.ª, deducted in full),EXCLUSIVELY_NON_DEDUCTIBLE(regla 2.ª, no deduction), orCOMMON(regla 3.ª, deducted at the general percentage). Meaningful only for a purchase row in a bucket whose prorrata register regime is especial; the regime-aware aggregation routes the deducible cuota by this classification.Nonefor rows that are not under especial or carry no per-input use declaration.prorrata_sector_id – Operator-declared LIVA arts. 9.1.c / 101 differentiated sector this input belongs to. References a
sector_iddeclared in the bucket’s prorrata register sector definitions; the sector-aware aggregation applies THAT sector’s provisional percentage to the row’s deducible cuota.Nonemeans common-use (usable across sectors), apportioned by the art. 104.Dos common percentage in a sectorized bucket; in a non-sectorized bucketNoneis the whole-entity default (today’s behaviour), so an unsectored taxpayer is unaffected.purchase_invoice_evidence_id – Canonical purchase-invoice evidence reference attached to the row.
attachment_ids – Supplementary attachment references.
created_by – Actor that first created the manual row when known.
source_command – Backend/CLI command source that created the row.
created_event_id – Bucket event id for the create event when available.
evidence_provenance – Actor/source lineage for attached evidence.
edit_lineage – Durable edit chain for manual corrections.
lifecycle_state – Current active/archive/stash/split state.
lifecycle_lineage – Durable lifecycle transition chain.
split_lineage – Optional
SplitLineagerecording this row’s role within an N-way split cohort.Nonefor transactions that have never been split.notes – Free-text notes.
import_fingerprint – Stable cross-format dedup fingerprint stamped at import time (see
derive_import_fingerprint()) and carried verbatim through every later edit so re-imports of the same statement — or the same movements in a different file format — are recognised as already present.Nonefor hand-entered rows that never came from an import.classified_at – Timezone-aware timestamp of the active decision (
Nonewhen never classified).classified_by – Classifier source string for the active decision.
classification_reason – Free-text reason for the active decision.
classification_confidence – Optional confidence in
[0, 1]for the active decision.classification_history – Tuple of historical
ClassificationHistoryEntryrecords, oldest first.iva_category – Explicit IVA category override. When set the aggregation layer uses this value in place of the rate-kind-derived domestic category, enabling non-domestic categories (intra-community, export, non-subject) to be expressed without a synthetic rate.
Nonefor transactions where the standard domestic rate derivation is sufficient.exemption_article – Optional Ley 37/1992 Art. 20 sub-article discriminator. Valid only when
iva_categoryisIvaCategory.DOMESTIC_EXEMPT;Nonepreserves the broad exempt category with no sub-article distinction.counterparty_eu_member_state – ISO 3166-1 alpha-2 EU member state of the counterparty. Required by the aggregation gate when
iva_categoryisIvaCategory.INTRA_COMMUNITY_SUPPLY; rejected when the category isIvaCategory.EXPORT_THIRD_COUNTRY_ZERO_RATED.Noneotherwise.cash_accounting_treatment – Independent criterio-de-caja axis. It never replaces
iva_category: the operation remains domestic/export/intracom/etc. and this field only records whether the taxpayer’s special regime or a supplier’s special regime changes IVA timing.cash_accounting_operation_date – Art. 75 general-devengo operation date for cash-accounting informational reporting. Required whenever
cash_accounting_treatmentis notNONEso the aggregator never silently reuses a bank movement date as the legal devengo projection.cash_accounting_payment_evidence – Total or partial collection/payment events that settle affected base/cuota under LIVA arts. 163 terdecies / quinquiesdecies.
fx_rate – ECB reference rate applied at import time to convert
raw.amountfromraw.currencyto EUR. The rate is expressed as a multiplier:raw.amount * fx_rate = value_in_eur.Nonewhen the native currency is EUR or when the rate was unavailable at import time.value_in_eur – Pre-converted EUR-equivalent of
raw.amountcomputed at import time asraw.amount * fx_rate, rounded to two decimal places. Aggregation layers use this field in place ofraw.amountfor non-EUR transactions, making casilla sums deterministic and independent of rate changes after the import date.Nonewhen the native currency is EUR or when no rate was available.source_jurisdiction – ISO 3166-1 alpha-2 uppercase code identifying the regulatory source jurisdiction of the income or expense (
"ES"for Spanish-source, foreign two-letter codes for foreign-source). Drives the IRNR scope filter (non-resident profiles only emit Spanish-source rows into AEAT bases) and the Art. 93 LIRPF Beckham filter (impatriado IRPF base excludes foreign-source rows).Nonerecords an explicitly unknown jurisdiction.created_at – UTC-aware timestamp stamped once at construction and carried verbatim through every later edit.
modified_at – UTC-aware timestamp re-stamped on every mutating edit.
- Parameters:
raw (RawTransaction)
transaction_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=64, max_length=64, pattern=^[0-9a-f]{64}$, ascii_only=None)])
direction (TransactionDirection)
business_classification (BusinessClassification)
business_pct (Decimal | None)
invoice_id (str | None)
category_id (str | None)
taxable_base (Decimal | None)
iva_rate (Decimal | None)
iva_amount (Decimal | None)
recargo_amount (Decimal | None)
irpf_category (str | None)
usage_ratio_id (str | None)
prorrata_reference (str | None)
art_104_tres_exclusion (Art104TresExclusion | None)
input_classification (InputClassification | None)
prorrata_sector_id (str | None)
purchase_invoice_evidence_id (str | None)
created_by (str | None)
source_command (str | None)
created_event_id (str | None)
evidence_provenance (tuple[TransactionEvidenceProvenanceEntry, ...])
edit_lineage (tuple[TransactionEditLineageEntry, ...])
lifecycle_state (TransactionLifecycleState)
lifecycle_lineage (tuple[TransactionLifecycleLineageEntry, ...])
split_lineage (SplitLineage | None)
notes (str)
import_fingerprint (str | None)
classified_at (datetime | None)
classified_by (str)
classification_reason (str)
classification_confidence (Decimal | None)
classification_history (tuple[ClassificationHistoryEntry, ...])
iva_category (IvaCategory | None)
exemption_article (IvaExemptionArticle | None)
counterparty_eu_member_state (EUMemberState | None)
cash_accounting_treatment (IvaCashAccountingTreatment)
cash_accounting_operation_date (date | None)
cash_accounting_payment_evidence (tuple[IvaCashAccountingPaymentEvidence, ...])
fx_rate (Decimal | None)
value_in_eur (Decimal | None)
rate_source (str | None)
rate_date (str | None)
source_jurisdiction (str | None)
group_label (str | None)
created_at (datetime)
modified_at (datetime)
- raw: RawTransaction¶
- transaction_id: TransactionId¶
- direction: TransactionDirection¶
- business_classification: BusinessClassification¶
- business_pct: Decimal | None¶
- invoice_id: str | None¶
- category_id: str | None¶
- taxable_base: Decimal | None¶
- iva_rate: Decimal | None¶
- iva_amount: Decimal | None¶
- recargo_amount: Decimal | None¶
- irpf_category: str | None¶
- usage_ratio_id: str | None¶
- prorrata_reference: str | None¶
- art_104_tres_exclusion: Art104TresExclusion | None¶
- input_classification: InputClassification | None¶
- prorrata_sector_id: str | None¶
- purchase_invoice_evidence_id: str | None¶
- attachment_ids: tuple[str, ...]¶
- created_by: str | None¶
- source_command: str | None¶
- created_event_id: str | None¶
- evidence_provenance: tuple[TransactionEvidenceProvenanceEntry, ...]¶
- edit_lineage: tuple[TransactionEditLineageEntry, ...]¶
- lifecycle_state: TransactionLifecycleState¶
- lifecycle_lineage: tuple[TransactionLifecycleLineageEntry, ...]¶
- split_lineage: SplitLineage | None¶
- notes: str¶
- import_fingerprint: str | None¶
- classified_at: datetime | None¶
- classified_by: str¶
- classification_reason: str¶
- classification_confidence: Decimal | None¶
- classification_history: tuple[ClassificationHistoryEntry, ...]¶
- iva_category: IvaCategory | None¶
- exemption_article: IvaExemptionArticle | None¶
- counterparty_eu_member_state: EUMemberState | None¶
- cash_accounting_treatment: IvaCashAccountingTreatment¶
- cash_accounting_operation_date: date | None¶
- cash_accounting_payment_evidence: tuple[IvaCashAccountingPaymentEvidence, ...]¶
- fx_rate: Decimal | None¶
- value_in_eur: Decimal | None¶
- rate_source: str | None¶
- rate_date: str | None¶
- source_jurisdiction: str | None¶
- group_label: str | None¶
- created_at: datetime¶
- modified_at: datetime¶
- class BucketTransactionRef(**data)[source]¶
Bases:
BaseModelA transaction identifier qualified by its owning profile bucket.
- Parameters:
bucket_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=1, max_length=128, pattern=None, ascii_only=None)])
transaction_id (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=None, min_length=64, max_length=64, pattern=^[0-9a-f]{64}$, ascii_only=None)])
- bucket_id: BucketId¶
- transaction_id: TransactionId¶
- class TransactionCatalogue(**data)[source]¶
Bases:
BaseModelImmutable catalogue keyed by
transaction_id.transactionsis a frozentypes.MappingProxyTypefrom stable transaction id toTransaction, built viafrom_transactions()or by passing a mapping / iterable tomodel_validate.- Parameters:
transactions (Mapping[str, Transaction])
- transactions: Mapping[str, Transaction]¶
- classmethod from_transactions(transactions)[source]¶
Build a catalogue from an iterable of transactions.
- Parameters:
transactions (
Iterable[Transaction|Mapping[str,object]]) – Transactions or transaction payloads to load.- Return type:
Self- Returns:
A validated immutable transaction catalogue.
- get(transaction_id)[source]¶
Fetch one transaction by ID if present.
- Parameters:
transaction_id (
str) – Stable transaction identifier.- Return type:
- Returns:
The matching
Transaction, orNonewhen absent.
- values()[source]¶
Iterate over catalogue
Transactionrecords.- Return type:
- class OutOfWindowTransactionStub(**data)[source]¶
Bases:
BaseModelA catalogue transaction outside a requested date window, undecrypted.
Carries ONLY the two plaintext, non-sensitive facts a period-scoped aggregator needs to report the transaction as excluded –
transaction_idand itsfiling_date– never any decrypted field (amount, category, counterparty, direction, business classification). This is the O2 period-first partition contract (2026-07-05-ledger-latency-budget-adr): an out-of-window row is diagnosed from the plaintext date-index fact alone, without paying the decrypt-and-validate cost, and without leaking anything the index itself does not already carry.- transaction_id: str¶
- filing_date: date¶
- class OutOfWindowTransactionSummary(**data)[source]¶
Bases:
BaseModelCompact diagnostics-only summary for out-of-window catalogue rows.
Carries only the facts authorized by the 2026-07-06 diagnostic-summary amendment to the latency ADR: excluded-row count and the filing-date span covered by those rows. It never carries decrypted transaction facts.
- count: int¶
- min_filing_date: date¶
- max_filing_date: date¶
- classmethod from_stubs(stubs)[source]¶
Build a summary from row-level plaintext stubs, or
Nonewhen empty.- Return type:
Optional[Self]- Parameters:
stubs (Iterable[OutOfWindowTransactionStub])
- class LedgerDatePartition(**data)[source]¶
Bases:
BaseModelA ledger catalogue split into an in-window and an out-of-window half.
in_windowis a real, fully decryptedTransactionCataloguescoped to[start, end]– every regulated classifier gate runs over it unchanged.out_of_windowis the plaintext-only remainder (OutOfWindowTransactionStubrows): transactions the catalogue holds outside the window, reported without decryption so a caller can still surface a period-exclusion diagnostic for them.out_of_window_summaryis the compact diagnostics-channel replacement: count plus filing-date span, with no decrypted fields and no row-level allocation requirement. During the migration, callers may see either the row-level stubs, the summary, or both.index_completerecords whether the partition was served from a complete plaintext date index (True) or from a full-scan fallback after a completeness-gate mismatch (False– seeledger-participation-index-is-derived-rebuildable): both cases return an identical partition shape, so a caller cannot observe which path served it except through this flag and through latency.- Parameters:
in_window (TransactionCatalogue)
out_of_window (tuple[OutOfWindowTransactionStub, ...])
out_of_window_summary (OutOfWindowTransactionSummary | None)
index_complete (bool)
- in_window: TransactionCatalogue¶
- out_of_window: tuple[OutOfWindowTransactionStub, ...]¶
- out_of_window_summary: OutOfWindowTransactionSummary | None¶
- index_complete: bool¶