"""Madrid nacimiento/adopción indeterminate-eligibility advisory for verification.
The Madrid nacimiento/adopción deducción autonómica (casilla 1039, DL 1/2010
arts. 4 y 18.1) auto-populates on the calculate path only for the determinable
single/monoparental individual filer
(:func:`~application.modelo.profile_binding.inject_derived_autonomic_deduccion_facts`);
a tributación conjunta declaration or a married/pareja-de-hecho filer is
fail-closed by design because the unidad-familiar 61.860 € límite needs the
spouse's base imponible, which the app does not persist. That fail-closed
branch resolves casilla 1039 to zero with no operator-facing signal that the
entitlement might still exist.
This module implements the advisory: when the filer is a Madrid
resident with at least one nacimiento/adopción-eligible descendant, the unit is
indeterminate (conjunta or married/partnered), and casilla 1039 has resolved to
zero, emit a non-blocking ADVISORY prompting the operator to confirm eligibility
and consider entering the deducción manually. The finding stays advisory
because the unidad-familiar income-limit gate genuinely cannot be evaluated
from data this application holds (``no-silent-under-declaration`` is
symmetric for a deducción: over-claim, not silence, is the hazard the
calculate-path fail-closed default already guards against; this advisory is
the surface half of that same guard).
The advisory reads the casilla-1039 semantic role and the resolved input value
off the verify-time :class:`~domain.calculations.registry.schema.RegistrySnapshot`
for the target modelo revision, the same authority the calculate path resolves
its registry formula against.
See Also:
:func:`~application.modelo.profile_binding.inject_derived_autonomic_deduccion_facts`
The calculate-path injector whose fail-closed branch this advisory
surfaces to the operator.
:func:`~application.modelo.profile_binding.madrid_nacimiento_adopcion_candidate_weighted_count`
Shared candidate-count primitive: evaluates only the per-descendant
window/cohabitation condition, independent of the unit's determinability.
:func:`~application.modelo.verification_actions._collect_revision_verification_findings`
Verification collector that appends this advisory beside the DT 12ª /
art. 20 / art. 52 / Convenio LOB advisories using the same
non-blocking mechanism.
"""
from __future__ import annotations
from collections.abc import Mapping
from datetime import date
from decimal import Decimal
from typing import TYPE_CHECKING
from ...core.casilla_id import CasillaId
from ...domain.calculations.registry.errors import RegistryValidationError
from ...domain.calculations.registry.ids import LegalRefId
from ...domain.calculations.registry.schema import RegistrySnapshot
from ...domain.contribuyente.family_fact_context import FamilyFactResolutionContext
from ...domain.modelos.errors import ModeloError
from ...domain.modelos.verification_report import (
ModeloVerificationFinding,
ModeloVerificationFindingKind,
ModeloVerificationFindingSeverity,
)
from ...domain.modelos.work_unit import WorkUnit
from ...domain.user_profile.errors import ProfileNotFoundError
from ...domain.user_profile.values import UserProfileFactValue
from ..user_profile.profile_record_repository import ProfileRecordRepository
from ..user_profile.projections import profile_fact_index
from .profile_binding import (
is_indeterminate_unidad_familiar,
is_madrid_autonomic_deduccion_filing_year,
is_madrid_resident,
madrid_nacimiento_adopcion_candidate_weighted_count,
)
from .semantic_role_resolution import AmbiguousSemanticRoleCasillaError, casilla_id_for_unique_semantic_role
if TYPE_CHECKING:
from ...domain.calculations.registry.authority import PinnedAuthorityOperation
from .work_profile import ModeloWorkProfile
_MADRID_NACIMIENTO_ADOPCION_SEMANTIC_ROLE = "irpf_deduccion_madrid_nacimiento_adopcion"
def _advisory_legal_refs(snapshot: RegistrySnapshot, casilla_id: CasillaId) -> tuple[LegalRefId, ...]:
"""Project the advisory's grounding from the selected registry formula.
The advisory concerns a computed casilla, so its legal grounding is the
formula's typed provenance rather than a second application-owned tuple.
Refuse an incomplete or out-of-snapshot declaration instead of emitting a
finding whose legal references cannot be checked against the selected
authority.
"""
casilla = next((candidate for candidate in snapshot.revision.casillas if candidate.id == casilla_id), None)
if casilla is None or casilla.formula is None:
raise RegistryValidationError(
f"Madrid nacimiento/adopción advisory casilla {casilla_id!r} has no registry formula provenance",
)
formula = next((candidate for candidate in snapshot.revision.formulas if candidate.id == casilla.formula), None)
if formula is None or not formula.legal_refs:
raise RegistryValidationError(
f"Madrid nacimiento/adopción advisory formula {casilla.formula!r} has no legal provenance",
)
missing = tuple(ref for ref in formula.legal_refs if ref not in snapshot.legal)
if missing:
raise RegistryValidationError(
f"Madrid nacimiento/adopción advisory formula {formula.id!r} has legal refs absent from the selected "
f"authority: {missing!r}",
)
return tuple(formula.legal_refs)
[docs]
def madrid_nacimiento_adopcion_eligibility_advisory_finding(
snapshot: RegistrySnapshot,
casilla_values: Mapping[CasillaId, Decimal],
*,
bucket_id: str,
operation: PinnedAuthorityOperation,
profile: ModeloWorkProfile | None = None,
) -> ModeloVerificationFinding | None:
"""Warn to confirm Madrid nacimiento/adopción eligibility for an indeterminate unit.
Loads the bucket's :class:`~domain.user_profile.values.UserProfileRecord`
directly (the same source the calculate-path injector reads) so the verify
path can see the ``tax_residence.ccaa`` / ``renta_taxpayer.marital_status`` /
``renta_filing.declaration_type`` / ``renta_family.descendiente.*`` facts
that :class:`~domain.deadlines.models.TaxpayerProfile` does not carry.
Fires only when ALL of the following hold: the revision is the 2025 M100
filing year the first-slice registry formula covers; the filer is a Madrid
resident; at least one descendant is inside the nacimiento/adopción
applicability window and cohabits (the candidate weighted count is
strictly positive); the unit is indeterminate (tributación conjunta or a
married/pareja-de-hecho filer — the same condition that makes the
calculate-path injector fail-closed); and casilla 1039 resolved to zero (the
auto-trigger did not fire). A determinate single/monoparental filer whose
entitlement was already auto-populated, or a unit with no eligible
descendant, never fires this advisory.
Returns ``None`` when the bucket carries no profile record, the revision
does not declare the casilla-1039 semantic role, or any of the firing
conditions above is not met.
Core types:
:class:`~cadrumo.domain.calculations.registry.schema.RegistrySnapshot`.
"""
try:
casilla_id = casilla_id_for_unique_semantic_role(snapshot, _MADRID_NACIMIENTO_ADOPCION_SEMANTIC_ROLE)
except AmbiguousSemanticRoleCasillaError as exc:
raise ModeloError(str(exc), context=exc.ambiguity.context()) from exc
if casilla_id is None:
return None
if casilla_values.get(casilla_id, Decimal(0)) != Decimal(0):
# The auto-trigger already populated the casilla; nothing to advise.
return None
fact_index = _load_fact_index(
bucket_id,
operation=operation,
profile=profile,
)
if fact_index is None:
return None
if not is_madrid_resident(fact_index):
return None
if not is_indeterminate_unidad_familiar(fact_index):
return None
coordinate = date(snapshot.filing_year, 12, 31)
family_context = FamilyFactResolutionContext(authority=operation, filing_period=coordinate, devengo_date=coordinate)
if not is_madrid_autonomic_deduccion_filing_year(
snapshot.filing_year,
context=family_context,
operation=operation,
):
return None
weighted_count = madrid_nacimiento_adopcion_candidate_weighted_count(
fact_index,
snapshot.filing_year,
context=family_context,
operation=operation,
)
if weighted_count <= 0:
return None
return ModeloVerificationFinding(
kind=ModeloVerificationFindingKind.ADVISORY,
severity=ModeloVerificationFindingSeverity.WARNING,
casilla_id=casilla_id,
message_locale_key="application.modelo.findings.madrid_nacimiento_adopcion_eligibility_advisory",
message_facts={
"casilla_id": casilla_id,
"weighted_count": weighted_count,
},
legal_refs=_advisory_legal_refs(snapshot, casilla_id),
)
def _load_fact_index(
bucket_id: str,
*,
operation: PinnedAuthorityOperation,
profile: ModeloWorkProfile | None,
) -> dict[str, UserProfileFactValue] | None:
"""Return the bucket's profile fact index, or ``None`` when no profile exists."""
if profile is not None:
return profile_fact_index(profile.record, profile.profile_decode_context.schema)
try:
repository = ProfileRecordRepository.for_current_session(
bucket_id,
profile_decode_context=operation.profile_decode_context(),
)
record = repository.load(bucket_id)
except ProfileNotFoundError:
return None
return profile_fact_index(record, repository.session.profile_decode_context.schema)
[docs]
def madrid_nacimiento_adopcion_advisory_finding_for_work_unit(
snapshot: RegistrySnapshot,
casilla_values: Mapping[CasillaId, Decimal],
*,
work_unit: WorkUnit,
operation: PinnedAuthorityOperation,
profile: ModeloWorkProfile | None = None,
) -> ModeloVerificationFinding | None:
"""Convenience wrapper reading ``bucket_id`` off a :class:`WorkUnit`.
Core types:
:class:`~cadrumo.domain.calculations.registry.schema.RegistrySnapshot`.
"""
return madrid_nacimiento_adopcion_eligibility_advisory_finding(
snapshot,
casilla_values,
bucket_id=work_unit.bucket_id,
operation=operation,
profile=profile,
)
__all__ = [
"madrid_nacimiento_adopcion_advisory_finding_for_work_unit",
"madrid_nacimiento_adopcion_eligibility_advisory_finding",
]