cadrumo.core.aggregation module

Core aggregation taxonomy shared across registry, application, and adapters.

This module owns closed value sets and tiny pydantic carriers only; it does not aggregate ledger rows, calculate casilla values, or perform source resolution. The registry schema imports BindingAggregation and BindingSourceKind to validate TOML authoring input. Application resolvers and adapters import the same BindingSourceKind members so source tokens do not drift through bare strings.

Keep the three axes separate:

class BindingAggregationOp(*values)[source]

Bases: StrEnum

Closed set of aggregation operators a registry binding may declare.

A binding’s aggregation.op selects how the resolver folds the selected source values into the bound casilla value. The members below are the complete set declared on a BindingDefinition.aggregation across the registry authoring tree; relation aggregation (copy/sum on a relation_prefill provider) and formula-expression operators are a separate, unrelated axis and are not modelled here.

Members:
SUM: Add the selected source values (default for the scalar-folding

families: previous-filing, counterpart, invoice, ledger, withholding).

ROWS: Emit one detail row per selected observation rather than folding

to a scalar (default for the detail-record families: related-party, foreign-asset, atribución, refund).

COPY: Carry a single source value through unchanged; refuses when more

than one source value is selected.

COUNT_DISTINCT: Count the distinct operators/perceptores behind the

selected observations (e.g. Modelo 349 operator counts).

PRIOR_PAGOS_FRACCIONADOS: Modelo 130 cumulative pago-fraccionado carry

that nets the prior positive declaration against the running total.

SUM
ROWS
COPY
COUNT_DISTINCT
PRIOR_PAGOS_FRACCIONADOS
class BindingAggregation(**data)[source]

Bases: BaseModel

Typed aggregation rule carried by a registry BindingDefinition.

Placed in core (cross-layer home) because the domain registry schema declares the field and the application/adapter layers read it; the closed BindingAggregationOp set is the only key real binding aggregation mappings carry in the registry authoring tree. The model is strict and frozen, matching the registry schema’s STRICT_FROZEN_CONFIG convention, so an unknown op or a stray extra key is rejected at registry-build validation rather than silently re-parsed at resolve time.

Parameters:

data (Any)

op: BindingAggregationOp
class RelationAggregationOp(*values)[source]

Bases: StrEnum

Closed set of aggregation operators a cross-filing fold may declare.

A relation’s aggregation.op selects how a cross-modelo fold-in folds its matched source filings: COPY carries a single source value through unchanged (the default when a relation declares no aggregation), and SUM adds the matched per-period source values (annual summaries). This is a deliberately separate axis from BindingAggregationOp (which governs BindingDefinition folds and carries the binding-only rows / count_distinct / prior_pagos_fraccionados members); the two are not interchanged. The complete set declared across the registry relation tree is copy and sum.

COPY
SUM
class AggregationCaptureKind(*values)[source]

Bases: StrEnum

Which ingestion path wrote a per-perceptor aggregation observation batch.

CAPTURE PROVENANCE, and a different axis from two neighbours it is easy to confuse with. It is not ObservationSourceKind, which classifies whether a persisted modelo observation is official AEAT filing evidence; and it is not the inner source_kind on a retención observation, which records whether the underlying row came from a ledger transaction or a payable invoice. All three have worn the name source_kind.

That distinction was load-bearing before it was enforceable. The two aggregation stores are exempt from the official-evidence displacement guard precisely BECAUSE no evidence-authority value can reach them, and that exemption rested on the field being a free-form str that happened to carry one token. Nothing refused "aeat_sede_justificante" into either store, and on the day something wrote it the exemption would have become wrong silently. Typing the axis converts the disjointness from an observation about today’s content into a load-time refusal.

One member is not a defect. The set is closed at what the code actually produces; a second ingestion path adds its own member here, deliberately, rather than arriving as an unreviewed string.

AGGREGATE_PULL
class RowSetGroupingKind(*values)[source]

Bases: StrEnum

Canonical row-set source-kind discriminators for detail-record assembly.

Placed in core (cross-layer home) because both the application assembly layer and the domain registry schema reference these values, and domain → application imports are forbidden under the hexagonal contract.

This is the row-assembly grouping axis consumed in the application layer (_row_set_assembly.py), a separate concept from the binding source token enumerated by BindingSourceKind. For the three detail-record families whose grouping member differs from the binding source token, the correspondence is intentional and explicit; see ROW_SET_GROUPING_FOR_BINDING_SOURCE:

  • BindingSourceKind.WITHHOLDING ("withholding") ↔ WITHHOLDING

  • BindingSourceKind.FOREIGN_ASSET ("foreign_asset") ↔ FOREIGN_ASSET

  • BindingSourceKind.RELATED_PARTY_OPERATION ("related_party_operation") ↔ RELATED_PARTY ("related_party")

  • BindingSourceKind.ATRIBUCION_MEMBER ("atribucion_member") ↔ ATRIBUCION ("atribucion")

  • BindingSourceKind.REFUND_OPERATION ("refund_operation") ↔ REFUND ("refund")

  • BindingSourceKind.DONATIVO_DONOR ("donativo_donor") ↔ DONATIVO ("donativo")

WITHHOLDING
RELATED_PARTY
FOREIGN_ASSET
ATRIBUCION
REFUND
DONATIVO
GASTO193
WITHHOLDING296
class CalculationSourceLineageRole(*values)[source]

Bases: StrEnum

A persisted source node’s role in one resolver-produced provenance graph.

PRIMARY

Durable resolved economic object whose resolver owns the binding source.

CONTRIBUTOR

Upstream fact linked to, but never substituting for, a primary object.

class BindingSourceKind(*values)[source]

Bases: StrEnum

The single canonical closed set of binding/source-mesh tokens.

Every BindingDefinition declares exactly one source drawn from the registry-declared subset of this enum. The same enum also carries mesh-only source decisions such as BORRADOR and IVA_WALLET_DECISION, which are resolved before a registry binding is constructed and are parity-accounted as non-registry members. Per-family frozensets (invoice, ledger, counterpart) are derived from this enum rather than hand-maintained, so a new source token is added in exactly one place.

BEHAVIOUR-PRESERVING LIFT: every member’s string VALUE equals the source token that was previously a bare string (or a RowSetGroupingKind member) in the BindingDefinition.source Literal. Those tokens live in registry TOML and may be persisted; a StrEnum serialises to its value, so folding the mixed Literal onto this enum changes the static type without changing any stored or compared string (the modelo-enum-hardening precedent). Do NOT rename a stored token.

This enum is the single canonical source-kind authority across BOTH the registry binding definitions AND the application resolver mesh: the counterpart subset (COUNTERPART_SOURCE_KINDS) is derived from it, and the two grouping members reuse RowSetGroupingKind values so the cross-layer aggregation taxonomy stays consistent; see ROW_SET_GROUPING_FOR_BINDING_SOURCE for the detail-record source-token ↔ grouping-axis mapping.

PROFILE
PREVIOUS_FILING
RELATION_PREFILL
MANUAL_INPUT
DESIGN_CONSTANT
LEDGER_OSS_AGGREGATION
LEDGER_IVA_AGGREGATION
LEDGER_RENTA_GASTOS_ESTIMACION_DIRECTA_AGGREGATION
LEDGER_RENTA_INCOME_AGGREGATION
LEDGER_RENTA_GASTOS_PAGO_FRACCIONADO_AGGREGATION
LEDGER_IMPATRIADO_INCOME_AGGREGATION
LEDGER_IRNR_INCOME_AGGREGATION
RETENCIONES_AGGREGATION
IVA_COMPENSATION_ANNUAL_PARTITION
M303_REGIMEN_SIMPLIFICADO_ANNUAL_SUMMARY
BIENES_INVERSION_REGULARIZACION
INVENTORY
PRORRATA_REGULARIZACION
BORRADOR
IVA_WALLET_DECISION
PAYABLE_INVOICE
COLLECTIBLE_INVOICE
M347_THIRD_PARTY_OPERATION
LEDGER_TRANSACTION
PURCHASE_INVOICE_EVIDENCE
WITHHOLDING
FOREIGN_ASSET
RELATED_PARTY_OPERATION
ATRIBUCION_MEMBER
REFUND_OPERATION
DONATIVO_DONOR
GASTO193_CONTRIBUTOR
WITHHOLDING296
ROW_SET_GROUPING_FOR_BINDING_SOURCE: Final[Mapping[BindingSourceKind, RowSetGroupingKind]]

Explicit detail-record binding-source ↔ row-assembly grouping correspondence.

The binding source token (e.g. "related_party_operation") and the row-assembly RowSetGroupingKind value (e.g. "related_party") are distinct strings for the three families whose source token carries the _operation / _member suffix; this mapping makes the relationship explicit so a reader is not misled into assuming the two axes share a value.

INVOICE_BINDING_SOURCE_KINDS: Final[frozenset[BindingSourceKind]]

Invoice-shaped binding source kinds, derived from BindingSourceKind.

COUNTERPART_SOURCE_KIND_ORDER: Final[tuple[CounterpartSourceKind, ...]]

Operator-facing display order for COUNTERPART_SOURCE_KINDS.

This is the order the --kind help text and alias table present to an operator (documented in docs/how-to/review-queue.md), so it is authoritative and MUST NOT be reordered incidentally. COUNTERPART_SOURCE_KINDS derives its membership from this tuple so the two can never drift apart.

counterpart_source_kind(value)[source]

Return value narrowed to the counterpart source-kind subset.

Parameters:

value (object) – A BindingSourceKind member or its stored string value.

Return type:

TypeAliasType

Returns:

The same source kind narrowed to CounterpartSourceKind for counterpart aggregation inputs.

Raises:

ValueError – When value is not a known BindingSourceKind, or is known but outside the counterpart subset.

LEDGER_BINDING_SOURCE_KINDS: Final[frozenset[BindingSourceKind]]

Ledger-aggregation binding source kinds (all seven), derived from the enum.

Every binding whose source is a member reads its values from the bucket-scoped ledger (transaction-classified IVA / OSS aggregation, Renta first-slice income and estimación directa gastos aggregation, the M130 pago-fraccionado gastos cumulative aggregation, or the M151 impatriado base aggregation, M151 impatriado income, or M210 explicit IRNR income). The ledger_ namespace derives the set directly from the canonical enum; ledger_transaction remains a counterpart source rather than a ledger aggregation. Cross-domain consumers route through this frozenset so the registry stays the single source of truth for ledger readiness.

OBSERVATION_BACKED_BINDING_SOURCE_KINDS: Final[frozenset[BindingSourceKind]]

Binding source kinds whose value is read back from a prior-filed observation.

Both name a figure the taxpayer already declared — a direct same-modelo carry, or a cross-modelo fold-in materialised into a relation-prefill slot — rather than a value this application derives. Consumers that must not treat such a value as independent evidence, or that must not compare it against the observation store it came from, route through this frozenset.

MEMBERSHIP IS SEMANTIC, NOT LEXICAL, and this is the one per-family collection that cannot be derived by construction the way LEDGER_BINDING_SOURCE_KINDS is from the ledger_ namespace: the two members share no naming property, only the behaviour of reading the observation store. So adding a source kind here is a judgement, and the question to ask is whether its resolver reads a prior-filed observation — not whether its token looks like these two. That is stated rather than papered over, because a hand-listed set that claims to be derived is worse than one that admits it is not.

DO NOT WIDEN THIS TO THE CARRY-FORWARD OVERRIDE TIER. The caller-override precedence ladder declares a superset that additionally carries the IVA compensación annual partition and the prorrata regularización. That is a different concept — which values an operator override may displace — and folding it in here silently widens two registry validation guards.

class BindingTypedEnumKind(*values)[source]

Bases: StrEnum

The closed set of substrate enum-class names a binding value bridges.

A BindingDefinition whose value bridges a closed-membership substrate axis declares typed_enum = one of these members. Each value is the NAME of the closed enum class a consumer routes the binding value through:

  • CENSO_EVENT_KIND ("censo_event_kind") — Modelo 036 censo status.

  • CCAA ("CCAA") — Modelo 100 autonomic-community tax residence.

  • ESTIMACION_DIRECTA_MODALIDAD ("EstimacionDirectaModalidad") — Modelo 100 estimación-directa modality.

  • LEGAL_ENTITY_FORM ("LegalEntityForm") — Modelo 200 legal form.

  • RENTAL_REDUCTION_ART_23_2_TIER ("RentalReductionArt232Tier") — Modelo 100 reducción por arrendamiento de vivienda tier (LIRPF Art. 23.2).

BEHAVIOUR-PRESERVING LIFT: every member’s string VALUE equals the annotation token that was previously a bare str in BindingDefinition.typed_enum. Those tokens live in registry TOML and flow through operator-facing surfaces (bindings list table, the ModeloBindingQueryRow projection, the borrador resolver, the Sheets-pull router); a StrEnum serialises to its value, so narrowing the field from str | None to this enum changes the static type without changing any stored, compared, or emitted string (the modelo-enum-hardening precedent). Do NOT rename a stored token.

Declared in core as a closed value set per the architecture contract; the loader hydrates the registry TOML’s raw token to its member at the schema boundary (see _coerce_typed_enum()). It is the closed-set annotation on the binding, distinct from the engine input_channel (how a formula consumes the value); a binding may carry a typed_enum yet still be a numeric decimal channel.

CENSO_EVENT_KIND
CCAA
ESTIMACION_DIRECTA_MODALIDAD
LEGAL_ENTITY_FORM
RENTAL_REDUCTION_ART_23_2_TIER
class LedgerIncomeGrounding(*values)[source]

Bases: StrEnum

Whether a ledger income row’s contribution rests on declared invoice substrate.

An actividad-económica income row reaches a modelo income casilla by one of two structurally different routes, and the difference is a LEGAL one, not a presentation detail:

  • SUBSTRATE_DECLARED — the row carries an explicit taxable_base (the IVA-exclusive base imponible from its invoice). Every income fact reads a real declared figure.

  • CASH_FALLBACK — no base is declared, so the only measure available is the raw bank-credited amount. That figure is net of any retención practicada and may be IVA-inclusive, so it is neither the ingresos íntegros the return asks for nor a base: the ingresos_integros_sum fact folds the cash in (mis-measuring in a direction that depends on the invoice), while taxable_base_sum contributes nothing for the row.

Declared in core as a closed value set per the architecture contract, so the domain registry protocol and the application aggregation pipeline key on ONE marker rather than each re-deriving the distinction from taxable_base_amount is None. Consumers must branch on the member, never on field-nullness: the marker is the fact.

SUBSTRATE_DECLARED
CASH_FALLBACK
class InvoiceDevengoRank(*values)[source]

Bases: StrEnum

Which source produced the LIVA art. 75 devengo date an invoice files under.

Period attribution – which quarter a cuota is declared in, which filing year an annual reconciliation sees – resolves on the art. 75 devengo date. That date is when the operation occurred, and the invoice date appears nowhere in art. 75.Uno: a B2B invoice may be issued up to the fifteenth of the following month while still belonging to the earlier period, so the issue date is wrong at exactly the month and quarter boundaries where attribution changes.

A fallback chain therefore cannot hand back a bare date. The two ranks below are not two spellings of one answer: one is a fact the taxpayer recorded, the other is a substitute that is right most of the time and wrong precisely where it matters. Returning them undifferentiated would let a consumer treat the substitute as the fact, which is the mistake the marker exists to prevent.

Declared in core as a closed value set per the architecture contract, beside LedgerIncomeGrounding, which answers the parallel question for the income measure. Consumers must branch on the member, never on operation_date is None: the marker is the fact.

D10’s third named rank – the bank movement date – is NOT a member here. An invoice always carries an issue date, so the invoice-side chain terminates at ISSUE_DATE_PROXY and can never reach a movement date; the movement-date substitution belongs to the ledger-transaction side, whose dates have their own owners in cadrumo.domain.transactions.dates. Declaring a member no producer on this axis can emit would be dead capacity wearing the shape of coverage.

OPERATION_DATE_DECLARED

The invoice records its own devengo-relevant date and it was used.

Covers both art. 75 clauses: the general-regime operation date (art. 75.Uno) and a pago anticipado’s collection date (art. 75.Dos). Which clause supplied it is a separate axis, recorded on the invoice’s own operation_date_role; this marker answers only whether the date is a declared fact or a substitute.

ISSUE_DATE_PROXY

No devengo date was recorded, so the issue date stood in for it.

The attribution is a best available guess, not a legal determination. It agrees with the declared fact for every operation invoiced in its own period and diverges exactly at the boundary cases, so a period whose figures rest on this rank is one an operator should be told about.

class LedgerWithholdingDerivation(*values)[source]

Bases: StrEnum

How a ledger income row’s retención figure was arrived at.

A ledger row never declares its retención – no transaction field records one – so every non-zero figure on this surface is derived, and the reader needs to know from what. Without the marker three structurally different situations are one indistinguishable Decimal("0"): a row that genuinely had nothing withheld, a row whose substrate was too thin to tell, and a row whose arithmetic implied a withholding so large the figure was refused. The last is the dangerous one, because a refused inference and a real zero look identical in the sum.

Declared in core as a closed value set per the architecture contract, beside LedgerIncomeGrounding, which answers the parallel question for the income measure.

NOT_APPLICABLE

The row is not an actividad-económica receipt, so no retención arises.

NO_SUBSTRATE

No base is declared, or the cuota is not determinable from what is.

The withholding is unknown, not zero. The row still contributes its cash to the income measure, so it under-claims the offsetting credit.

NONE_WITHHELD

nothing was withheld.

Type:

Substrate sufficed and the cash covers the invoice

DECLARED_ON_LINKED_INVOICE

Read from the retención the linked sales invoice itself declares.

The strongest source available: the figure the document states, not one reconstructed from what reached the bank. Preferred over every inference below it, because a declared figure beats a derived one.

INFERRED_FROM_DECLARED_CUOTA

Derived as invoice gross minus cash, with the cuota read from the row.

INFERRED_FROM_CATEGORY_ZERO_CUOTA

Derived the same way, with the cuota taken as zero because the declared IVA category makes it zero by law.

This is what lets an IVA-exempt professional service recover its retención: the operation has no cuota to record, so requiring a recorded one excluded precisely the invoices whose withholding matters most.

REFUSED_ABOVE_SUPPORTED_RATE

The inference exceeded the registry maximum supported rate and was dropped.

An implied withholding above the RIRPF art. 95.1 general rate is evidence that the recorded cash is the base without IVA rather than a net-of-retención payment. Emitting the figure anyway would persist a fabricated withholding; capping it at the bound would invent one at exactly the legal maximum. The figure is therefore zero and this marker says why.

class RetencionScheme(value)[source]

Bases: str

Open wire token for a registry-owned retenciones scheme.

The published authority owns membership and meaning. This core primitive validates only the stable token shape and exposes value for existing persistence/sorting call sites; it deliberately does not enumerate scheme values or import the registry authority.

Parameters:

value (str)

Return type:

RetencionScheme

property value: str

Return the canonical wire token.

class WorkIncomeRetencionTreatment(**data)[source]

Bases: BaseModel

Typed result carrier for a registry-resolved withholding treatment.

Parameters:

data (Any)

scheme: RetencionScheme
is_fixed_rate: bool
class RetencionClave(value, *, _registry_validated=False)[source]

Bases: StrictRegistryToken

Opaque Modelo 190/193 clave token projected from registry membership.

The selected withholding catalogue owns the letter vocabulary and its model applicability. Core carries only the typed token boundary; callers must obtain instances through the registry projection.

Parameters:
  • value (str)

  • _registry_validated (bool)

Return type:

Self

class IntracomOperationType(*values)[source]

Bases: StrEnum

Modelo 349 operation-key letters carried by invoice records.

E
H
M
S
T
R
A
ADQUISICION_SERVICIOS
D
C
class TravelAgencyMediationType(value, *, _registry_validated=False)[source]

Bases: StrictRegistryToken

Opaque travel-agency mediation token projected from fact 0133.

RD 1619/2012 disposición adicional cuarta owns the vocabulary and its Modelo 347 consequences. Core carries only the typed token boundary; callers must receive instances through the facts-registry projection.

Parameters:
  • value (str)

  • _registry_validated (bool)

Return type:

Self

class ThirdPartyDeclarationRole(value, *, _registry_validated=False)[source]

Bases: StrictRegistryToken

Opaque third-party declaration-role token projected from fact 0134.

The role vocabulary, legal grounding, and C/D/E role projections belong to the facts registry. Core carries only the validated token boundary; callers must obtain instances through the typed registry projection.

Parameters:
  • value (str)

  • _registry_validated (bool)

Return type:

Self

class ForeignAssetClass(*values)[source]

Bases: StrEnum

Modelo 720 asset classes (clave de tipo de bien).

Source: AEAT Modelo 720 instrucciones. Each clave is declared separately; the declarability gate applies the 50,000 EUR floor to the regulatory obligation block that contains the class after the aggregator runs. Declared in core as a closed value set.

ACCOUNT
SECURITY
COLLECTIVE_INVESTMENT
INSURANCE
REAL_ESTATE
VIRTUAL_CURRENCY