cadrumo.domain.calculations.registry.applicability module

Registry-grounded modelo-applicability derivation from the taxpayer model.

The overview surfaces (explain / calendar / agenda / backlog) used to treat every profile as an autónomo en estimación directa: the DeadlineEngine produces an obligation for every modelo with a registered deadline window, and no layer asked which kind of taxpayer this is. A pure landlord was told Modelo 130 was overdue.

This module is the derivation layer: each modelo’s applicable verdict is DERIVED from the three-axis TaxpayerProfile model (entity type, IRPF income categories, estimation regime) through a registry-grounded rule table. The autónomo-by-default assumption is removed.

Four verdicts are possible:

  • ApplicabilityVerdict.APPLICABLE — the taxpayer model triggers this modelo.

  • ApplicabilityVerdict.NOT_APPLICABLE — the taxpayer model positively excludes this modelo (a landlord has no Modelo 130; an S.L. has no Modelo 100).

  • ApplicabilityVerdict.ATTRIBUTION_PASS_THROUGH — the profile is an attribution entity (comunidad de bienes, sociedad civil sin objeto mercantil) and the modelo asked about is a cuota self-assessment (the IRPF Modelo 100 / 130 or the IS Modelo 200 / 202). An attribution entity runs no IS and no IRPF cuota of its own — the régimen de atribución de rentas (LIRPF Title X Section 2) attributes the income to the members, who file the substantive tax. The honest answer to “what is my cuota” is “none — taxed in the members’ returns”. This is structurally distinct from a plain NOT_APPLICABLE: a salaried-only natural person is positively excluded from Modelo 200 because they file a different cuota (Modelo 100); an attribution entity files no cuota at all.

  • ApplicabilityVerdict.INCOMPLETE — the taxpayer model is undeclared (no entity_type and, for a natural person, no income categories), or the entity form is recognised-but-unsupported. The engine refuses to guess: it never reports a confident wrong obligation, and it never runs an IRPF cuota for a company or an IS cuota for an attribution entity.

The entity-type axis selects the income-tax route: a legal entity routes to the IS path (Modelo 200 / 202), a natural person to the IRPF path (Modelo 100 / 130), and an attribution entity to member pass-through for cuota self-assessments. IVA and payer-fact modelos are then decided by their own declared profile facts (IVA regime, withholding-payer facts, trade thresholds). The engine routing contract does not treat pass-through income taxation as an exemption from non-income-tax obligations.

Canonical applicability authority — modelo level. MODELO_APPLICABILITY_RULES is the single canonical source for modelo-level applicability. Any question of the form “does this taxpayer ever owe this modelo?” is answered here. Code that derives applicability verdicts MUST read from this table; it MUST NOT re-implement the logic in another module or maintain a parallel copy of the rules dict.

Relation to ``applicability_conditions`` on ``ModeloDeadlineWindow``. ModeloDeadlineWindow carries a applicability_conditions mapping that governs window-level scheduling — which specific deadline window applies for a profile within the set of applicable windows (e.g. Modelo 202 uses different modality windows for the April / October / December instalments, and some windows filter by entity_size). These conditions are COMPLEMENTARY to the modelo-level rules, not replacements: applicability_conditions operates after the modelo-level gate confirms the modelo applies at all; it never overrides the modelo-level verdict. Adding a condition to a deadline window does not affect the ApplicabilityVerdict returned by derive_modelo_applicability().

Every rule carries legal_refs — scoped registry citation keys in the law-slug:art-N form (e.g. ley-35-2006:art-99) that resolve against src/cadrumo/_data/registry/aeat/legal/*.toml — per .claude/rules/aeat-calculation-grounding.md: applicability is regulatory data and must be registry-grounded, and every typed-ID reference must point at an existing registry entity. The seed table below covers the core modelo set an ordinary taxpayer encounters — the IRPF Renta and pago-fraccionado modelos (100 / 130 / 131), the corporate IS modelos (200 / 202), the IVA modelos (303 / 390), the retención modelos and their annual companions (111 / 190, 115 / 180), the operaciones modelos (349 / 347), and the attribution-entity informational Modelo 184. Per-entity / per-regime expansion to the remaining registered modelos is intentionally deferred; a modelo with no seed rule is reported INCOMPLETE with a rationale that says so.

class ApplicabilityVerdict(*values)[source]

Bases: StrEnum

Whether a modelo applies to a taxpayer, derived from its model.

Variables:
  • APPLICABLE – The declared taxpayer model triggers this modelo.

  • NOT_APPLICABLE – The declared taxpayer model positively excludes this modelo (e.g. a landlord has no Modelo 130 obligation; a sociedad limitada files no Modelo 100).

  • ATTRIBUTION_PASS_THROUGH – The profile is an attribution entity and the modelo is a cuota self-assessment (Modelo 100 / 130 / 200 / 202). The entity runs no IS and no IRPF cuota of its own — the régimen de atribución de rentas (LIRPF Title X Section 2) attributes the income to the members, who file the substantive tax. The honest answer to “what is my cuota” is “none — the income is taxed in the members’ returns”. Distinct from NOT_APPLICABLE, which means the taxpayer files a different cuota.

  • INCOMPLETE – The taxpayer model is not declared in enough detail to decide, or the entity form is recognised-but-unsupported. The engine refuses to guess — the operator must declare their taxpayer type first.

APPLICABLE
NOT_APPLICABLE
ATTRIBUTION_PASS_THROUGH
INCOMPLETE
class ModeloApplicability(**data)[source]

Bases: BaseModel

The derived applicability of one modelo for one taxpayer profile.

Variables:
  • modelo – The AEAT modelo identifier.

  • verdict – The ApplicabilityVerdict derived from the taxpayer model.

  • reason – Operator-facing prose explaining the verdict. An INCOMPLETE verdict carries one of two distinct rationales: the “declare your taxpayer type first” guidance when the taxpayer model is undeclared, or a “no rule derived yet” notice when the modelo has no seed rule (the latter is not a statement about the operator’s profile).

  • legal_refs – Scoped registry citation keys (law-slug:art-N) grounding the rule, each resolvable against the registry legal/*.toml tables. Always at least one entry — applicability is regulatory data and must be grounded (.claude/rules/aeat-calculation-grounding.md). For an INCOMPLETE verdict the refs ground the concept being asked about (the LIRPF / LIS taxpayer definitions) so the operator still sees a citation.

Parameters:

data (Any)

modelo: ModeloId
verdict: ApplicabilityVerdict
reason: _OperatorReason
legal_refs: tuple[LegalRefId, ...]
failure: RegistryFailureClassification | None

Domain facts for a boundary to project when applicability is incomplete.

property applicable: bool

Return whether the modelo positively applies.

Only ApplicabilityVerdict.APPLICABLE is a confident yes. NOT_APPLICABLE, ATTRIBUTION_PASS_THROUGH and INCOMPLETE all yield False — the operative views must not surface an obligation the engine cannot positively justify. An attribution entity owes no cuota self-assessment, so a pass-through verdict is not an applicable obligation.

class ModeloApplicabilityRule(**data)[source]

Bases: BaseModel

A single registry-grounded modelo-applicability rule.

A rule answers, for one modelo, the question “does the declared taxpayer model trigger this modelo?”. The predicate is expressed as closed sets over the three taxpayer axes; evaluation never invents legal behaviour beyond what the seed table grounds.

Variables:
  • modelo – The AEAT modelo identifier the rule decides.

  • applicable_entity_types – The EntityType values the modelo applies to. A taxpayer whose entity_type is outside this set gets ApplicabilityVerdict.NOT_APPLICABLE.

  • required_income_categories – For a natural person, the IRPF income categories of which at least one must be declared for the modelo to apply. Empty means the modelo does not gate on income category (it applies to every natural person whose entity_type matches). Non-empty means a natural person without any of these categories gets NOT_APPLICABLE — this is the gate that excludes Modelo 130 for a pure landlord.

  • required_estimation_regimes – The IRPF estimation regimes the modelo applies to. Empty means the modelo does not gate on the estimation regime. Non-empty means a natural person whose irpf_estimation_regime is outside the set gets NOT_APPLICABLE. An undeclared regime resolves to the direct-estimation default: estimación directa is the default IRPF method (LIRPF art. 16; RIRPF art. 32 makes módulos opt-in), so an actividad-económica autónomo who has not explicitly elected módulos owes Modelo 130. This is the axis that splits Modelo 130 (estimación directa) from Modelo 131 (estimación objetiva): the two are mutually exclusive on the regime.

  • applicable_fiscal_residencies – The fiscal residency categories the modelo applies to. Empty means the modelo does not gate on fiscal residency. An undeclared residency is kept on the resident-IRPF default path described by TaxpayerProfile; a declared residency outside this set is a positive exclusion.

  • applicable_iva_regimes – The IVA regimes that positively keep a modelo in scope. Empty means the modelo does not gate on IVA regime. Non-empty means a profile outside those regimes gets NOT_APPLICABLE. This lets Modelo 303 / 390 be driven by the declared IVA obligation instead of borrowing the natural person’s IRPF income-category axis for legal and attribution entities.

  • required_payer_fact – The PayerFactValue the modelo’s applicability depends on, or None when the modelo does not gate on a payer fact. When set, a profile that positively declares the fact gets APPLICABLE; a profile that does not gets INCOMPLETE — the underlying boolean has no tri-state, so the engine cannot positively justify a NOT_APPLICABLE (see PayerFactValue).

  • applicable_reason – Operator-facing prose for the APPLICABLE verdict.

  • not_applicable_reason – Operator-facing prose for the NOT_APPLICABLE verdict.

  • cuota_bearing – True when the modelo is a cuota self-assessment (the IRPF Modelo 100 / 130 or the IS Modelo 200 / 202). A cuota-bearing modelo asked of an attribution entity yields an ApplicabilityVerdict.ATTRIBUTION_PASS_THROUGH verdict rather than a plain NOT_APPLICABLE: the entity runs no cuota of its own, the income is taxed in the members’ returns. An informational modelo (Modelo 184) is not cuota-bearing — it stays a plain NOT_APPLICABLE for the entity types its applicable_entity_types excludes.

  • legal_refs – Scoped registry citation keys (law-slug:art-N) grounding the rule, each resolvable against the registry legal/*.toml tables.

Parameters:

data (Any)

modelo: ModeloId
applicable_entity_types: frozenset[EntityType]
required_income_categories: frozenset[IrpfIncomeCategory]
required_estimation_regimes: frozenset[IrpfEstimationRegime]
applicable_fiscal_residencies: frozenset[FiscalResidency]
applicable_iva_regimes: frozenset[IVARegime]
required_payer_fact: PayerFactValue | None
applicable_reason: _OperatorReason
not_applicable_reason: _OperatorReason
cuota_bearing: bool
legal_refs: tuple[LegalRefId, ...]
evaluate(profile)[source]

Derive the ModeloApplicability for profile.

Returns an INCOMPLETE verdict when the taxpayer model is not declared in enough detail to decide; an ATTRIBUTION_PASS_THROUGH verdict when the modelo is a cuota self-assessment asked of an attribution entity; otherwise an APPLICABLE / NOT_APPLICABLE verdict derived from the entity-type, income-category, estimation-regime, and payer-fact axes.

Parameters:

profile (TaxpayerProfile) – The TaxpayerProfile to evaluate against this rule.

Return type:

ModeloApplicability

hydrate_applicability_rule(modelo, fragment)[source]

Hydrate a registry-authored applicability fragment into the runtime rule.

The loader boundary for the applicability schema family: every free-form TOML string on fragment is resolved here to its typed registry token (or PayerFactValue), never left as a raw string for a downstream branch to compare against. An unknown token raises RegistryValidationError naming the offending rule and the underlying enum-coercion error, mirroring the coercion-boundary shape RevisionReviewStatusField and its siblings already use.

Parameters:
Return type:

ModeloApplicabilityRule

Returns:

The equivalent ModeloApplicabilityRule.

Raises:

RegistryValidationError – A field names a token not declared by the selected facts registry.

MODELO_APPLICABILITY_RULES: Mapping[str, ModeloApplicabilityRule]

Seed modelo-applicability rules — core persona coverage.

A modelo absent from this table has no derived rule yet: its applicability is reported ApplicabilityVerdict.INCOMPLETE with a rationale naming the deferred expansion, never a confident guess.

REGISTRY_RESOLVED_APPLICABILITY_MODELOS: frozenset[Modelo]

Modelo ids whose applicability rule is authored in the registry rather than declared as a Python literal in MODELO_APPLICABILITY_RULES below. 303 and 390 stay literal – their authoring trees are owned by the export-fragment-generator-authority campaign, not unplaced by omission.

Membership is reached two ways, and the difference matters when reading a rule’s provenance. Most entries arrived by MIGRATION: every revision of each was hydration-verified equal to the literal it replaces, through the real loader, before it was added here and the literal deleted in the same commit. Modelo 840 arrived by AUTHORING instead – it never had a literal to be verified against, and its rule is grounded directly on TRLRHL arts. 78, 82 and 90 with real-profile verdicts asserted in test_modelo_840_applicability. A migrated entry’s guarantee is equivalence; an authored entry’s is its citations and its tests, and no count is stated here because a tally of either goes stale on the next entry.

This is the single declaration of the mixed-surface state MODELO_APPLICABILITY_RULES is now in: these modelos resolve from the registry, 303 and 390 still resolve from the literal table. The literal table retires outright once the export-fragment campaign closes those two trees and they are migrated the same way; this module then stops authoring applicability data at all – it only reads it.

resolve_applicability_rule_from_authority(authority, modelo)[source]

Resolve one modelo’s applicability rule from an already-loaded authority.

Reads the UNVALIDATED ModeloDefinition (authority.modelo(...)), not validate_modelo/snapshot: applicability derivation is a pervasive, taxpayer-facing read on every profile view, and coupling its availability to full business-rule validation or the review-status filing gate would make an unrelated validation defect elsewhere in the tree break every taxpayer’s applicability answer. The fragment was already validated once, when the registry authority was built and published.

This is a deliberate asymmetry with RegistrySnapshot, which carries a same-shaped projection for every OTHER schema family: applicability answers “is this modelo due, and to whom” – the floor rung of the authority-grade ladder, scheduling reach, not filing authority. RegistrySnapshot is a filing-context projection one rung up. Resolving applicability without filing-grade review is correct per that ladder, not a gate dodged; coupling it to snapshot construction would wrongly tie a floor-rung fact to filing authority it does not need.

Applicability content is uniform across a modelo’s declared revisions today (the migrator authors the identical rule into every one), so any revision carrying the family answers the question – the first one found is used.

Split out from _resolve_registry_applicability_rule() so the real logic takes its authority as a parameter and is testable against a scratch ValidatedRegistryAuthority without touching the bundled tree or monkeypatching anything; the production wrapper leases the indexed generation only when a caller has not already supplied an operation.

Raises:

RegistryValidationError – No declared revision carries an applicability rule, or the rule fails to hydrate.

Return type:

ModeloApplicabilityRule

Parameters:
resolve_applicability_rule_from_operation(operation, modelo)[source]

Resolve one modelo’s applicability rule from a generation-pinned directory.

Return type:

ModeloApplicabilityRule

Parameters:
iter_modelo_applicability_rules()[source]

Return every registry-resolved or seed-literal ModeloApplicabilityRule.

The returned tuple is ordered by modelo id for deterministic audits and tests. Callers receive rule objects, not the mutable module-level dictionary, so the registry rule table remains read-only from the public API.

Return type:

tuple[ModeloApplicabilityRule, ...]

modelo_requires_iva_regime(modelo)[source]

Return whether the modelo’s applicability rule depends on IVA regime.

Calendar completeness is a consumer of the same applicability predicate that determines whether a taxpayer can have the modelo obligation. Keep that classification here, where registry-resolved and seed rules already meet, rather than maintaining a second calendar or core-constants set.

Return type:

bool

Parameters:

modelo (str)

taxpayer_model_is_declared(profile)[source]

Return whether the profile carries a usable taxpayer model.

The taxpayer model is “declared” when the operator has set an entity_type and — for a natural person — at least one IRPF income category. Without these, modelo applicability cannot be derived: the engine must report INCOMPLETE rather than assume autónomo. A legal / attribution entity needs no income category; the entity_type alone selects its tax.

Parameters:

profile (TaxpayerProfile) – The TaxpayerProfile to inspect.

Return type:

bool

derive_tax_route(profile)[source]

Return the tax branch profile routes to.

The routing contract: the entity_type axis selects the tax. A legal-entity profile routes to the Impuesto sobre Sociedades (Modelo 200 / 202); a natural person to the IRPF (Modelo 100 / 130 / 303); an attribution entity to the member pass-through. An undeclared entity_type yields TaxRoute.INCOMPLETE — the engine never runs an IRPF cuota for a company or an IS cuota for an attribution entity, and never defaults a tax for a profile that declared none.

Parameters:

profile (TaxpayerProfile) – The TaxpayerProfile whose entity_type axis selects the tax branch.

Return type:

TaxRoute

Returns:

The TaxRoute branch the profile’s entity_type selects, or TaxRoute.INCOMPLETE when entity_type is undeclared.

derive_modelo_applicability(profile, modelo, *, today=None, authority=None, operation=None)[source]

Derive a modelo’s applicability from the taxpayer model.

The verdict is DERIVED from the three-axis TaxpayerProfile model — never assumed. An undeclared taxpayer model yields an explicit ApplicabilityVerdict.INCOMPLETE answer; the engine never reports a confident wrong obligation.

A modelo without a seed rule (the seed covers the core persona set only) is also reported INCOMPLETE so the operator is never told a confident yes/no the registry rules cannot yet justify; the rationale points at the deferred expansion.

Parameters:
  • profile (TaxpayerProfile) – The operator’s three-axis taxpayer model.

  • modelo (str) – The AEAT modelo identifier to decide.

  • today (date | None) – Reference date for the Beckham window check. Defaults to the Europe/Madrid civil date (today_madrid()) when None — the six-year window is a Spanish-calendar boundary. Pass an explicit date in tests so results are deterministic.

  • authority (ValidatedRegistryAuthority | None) – Already-resolved validated authority to reuse. Omitting it preserves the standalone fingerprint-bounded bundled-tree lookup.

  • operation (PinnedAuthorityOperation | None) – Already-pinned indexed operation to reuse for point-loaded applicability facts. Omitting it leases the bundled indexed generation for this call.

Return type:

ModeloApplicability

Returns:

The ModeloApplicability for modelo and profile.

Core types: ValidatedRegistryAuthority.

derive_taxpayer_files_economic_activity(profile)[source]

Whether the taxpayer files actividad-económica pagos fraccionados (130/131).

Reads the TaxpayerProfile income-category declarations. True when the profile declares actividad-económica income; False when it declares income categories that exclude it (a salaried/rental-only filer never files 130/131); None when income categories are undeclared (fail-closed: the 130/131 dependency stays enforced). LIRPF art. 99 / RIRPF art. 109.

Return type:

bool | None

Parameters:

profile (TaxpayerProfile)