cadrumo.domain.transactions.models module¶
Strict immutable transaction-catalogue boundary models.
- 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.id_resolution.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 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.spending_category.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.concepto_ingreso – Operator-declared income concept (
ConceptoIngreso) for base-inclusion purposes. RD 439/2007 art. 110.1.c) fixes the agrarian pago fraccionado on the volumen de ingresos … excluidas las subvenciones de capital y las indemnizaciones, and the distinction runs INSIDE subsidies – a subvención corriente counts and a subvención de capital does not – so no amount, category or counterparty on the row can settle it.Nonemeans ordinary income and is INCLUDED, because an unmarked receipt is far more likely to be ordinary than exceptional and defaulting the other way would drop real income out of a declared volume.tipo_actividad – Operator-declared Modelo 036 tipo de actividad (
TipoActividad) the row’s activity income belongs to. Present so a return that splits casillas by activity can route each row to the right one – Modelo 131 carries the estimación-objetiva volume in casilla 01 and the agrarian volume in casilla 08, and without this axis the same rows would feed both and double-count. It also selects the RIRPF art. 95 retención partition through the registry correspondence intipo_actividad_partitions.Nonefor a row whose activity is undeclared or that carries no activity income at all; an aggregation that needs the split must treatNoneas unknown rather than as any particular activity.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_categoryis the registry-declared domestic-exempt category;Nonepreserves the broad exempt category with no sub-article distinction.counterparty_country –
Where the counterparty is ESTABLISHED – an address fact, ISO 3166-1 alpha-2. It answers establishment-flavoured questions only and is barred from identification-keyed gates.
Nonemeans establishment was not recorded, never that the party is established nowhere and above all never that it is outside the Union.Stored as the raw code rather than as a Member State enum, which is the whole point of the field: an enum closed over the Member States can say “established in Germany” and cannot say “established in the United States”, so absence was the only representation a third country had. A gate reading that absence as third-country establishment was reading “not recorded” as “outside the Union” – and on the issued side outside the Union is export treatment, zero-rated, so an unrecorded establishment silently exempted a supply. The code is handed to
territorial_scope_for_country(), which answers from the closed vocabulary and refuses a code that names no country, soXXestablishes nothing rather than establishing an export.This mirrors
Invoice, which has always stored the country and derived the Member State. The two models disagreeing about how one fact is held is what let the ledger path lose a distinction the invoice path kept.counterparty_eu_member_state – The Member State the counterparty is established in, DERIVED from
counterparty_countryandNonefor every country outside the Union. Not a stored fact: one establishment cannot be recorded twice without the two copies eventually disagreeing.counterparty_identification_state –
Which Member State IVA-IDENTIFIES the counterparty, read from the prefix of the IVA number it trades under. A different fact from establishment and never derived from it: a Spanish-established acquirer can hold a German IVA number, and a German-established one can purchase under a Spanish NIF-IVA. This is the operative fact for the Ley 37/1992 art. 25 exemption, so the aggregation gate requires it non-ES when
iva_categoryis the registry-declared intra-community-supply category.Nonemeans the identification was not established, never that the party is identified nowhere and above all never that it is identified in Spain – a decision needing it refuses with a review item rather than falling back to the country.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.operation_date –
The LIVA art. 75 devengo date – when the operation legally occurred, as distinct from when the bank moved. Optional on every regime and REQUIRED whenever
cash_accounting_treatmentis notNONE, so the aggregator never silently reuses a bank movement date as the legal devengo projection.Available to the general regime, not only to criterio de caja. Under art. 75 the cuota is devengada when the operation occurs regardless of collection, so for a general-regime row the operation date is the legally CONTROLLING one and the movement date is the proxy; the criterio-de-caja regime is the one where collection timing governs and this date is the informational counterpart. Restricting the field to cash accounting therefore withheld it from the only regime the law binds to it, leaving an invoice issued in one quarter and paid in another to declare its IVA in the quarter of payment.
Nonemeans the row makes no distinction, and the movement date stands as the operation date – correct whenever an operation is settled on the day it occurs, which is the ordinary case.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:
data (Any)
- 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¶
- tipo_actividad: TipoActividad | None¶
- concepto_ingreso: ConceptoIngreso | 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¶
- deduction_fact_kind: IvaDeductionFactKind | None¶
- deduction_provenance: IvaDeductionClassificationProvenance | None¶
- investment_asset_id: str | None¶
- rectifies_ledger_id: str | None¶
- exemption_article: IvaExemptionArticle | None¶
- counterparty_country: str | None¶
- counterparty_identification_state: EUMemberState | None¶
- cash_accounting_treatment: IvaCashAccountingTreatment¶
- 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¶
- m210_income_classification: M210IncomeClassification | None¶
- group_label: str | None¶
- created_at: datetime¶
- modified_at: datetime¶
- property counterparty_eu_member_state: EUMemberState | None¶
Return the Member State the counterparty is established in, or
None.Derived from
counterparty_countryrather than stored beside it, matchingInvoice. Two stored copies of one establishment fact can disagree, and the disagreement is silent.Nonecovers three different situations and deliberately does not distinguish them, because a Member State accessor is the wrong place to: no country was recorded, the country is outside the Union, or the code names no country at all. A caller that must tell those apart asksterritorial_scope_for_country()for the territory andstated_country_code_status()for why nothing resolved. Reading thisNoneas “outside the Union” is exactly the inference that let an unrecorded establishment zero-rate a supply.
- class BucketTransactionRef(**data)[source]¶
Bases:
BaseModelA transaction identifier qualified by its owning profile bucket.
- Parameters:
data (Any)
- bucket_id: BucketId¶
- transaction_id: TransactionId¶
- existing_transaction_import_fingerprints(transaction)[source]¶
Return every canonical re-import fingerprint for a stored transaction.
Imported rows carry their parse-boundary, direction-qualified fingerprint verbatim. Older or hand-entered rows may not have that stamp, so their current raw payload is projected once with the stored direction as a deterministic fallback. Preview diagnostics and the persisting import path must use this exact projection: a directionless fallback would incorrectly collapse otherwise distinct incoming and outgoing movements.
- Return type:
- Parameters:
transaction (Transaction)
- 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:
data (Any)
- 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 OutOfWindowTransactionIndexEntry(**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 period-first partition contract: 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.- Parameters:
data (Any)
- transaction_id: TransactionId¶
- filing_date: date¶
- class OutOfWindowTransactionSummary(**data)[source]¶
Bases:
BaseModelCompact diagnostics-only summary for out-of-window catalogue rows.
Carries only the excluded-row count and the filing-date span covered by those rows. It never carries decrypted transaction facts.
- Parameters:
data (Any)
- count: int¶
- min_filing_date: date¶
- max_filing_date: date¶
- classmethod from_index_entries(index_entries)[source]¶
Build a summary from plaintext date-index entries, or
Nonewhen empty.- Return type:
Optional[Self]- Parameters:
index_entries (Iterable[OutOfWindowTransactionIndexEntry])
- 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 (OutOfWindowTransactionIndexEntryrows): 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 index entries, 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– seeaeat-ledger-contract): both cases return an identical partition shape, so a caller cannot observe which path served it except through this flag and through latency.- Parameters:
data (Any)
- in_window: TransactionCatalogue¶
- out_of_window: tuple[OutOfWindowTransactionIndexEntry, ...]¶
- out_of_window_summary: OutOfWindowTransactionSummary | None¶
- index_complete: bool¶