"""Registry lookup and casilla validation helpers for modelo actions.
The helper layer resolves
:class:`~aeat.domain.calculations.registry.RegistrySnapshot` instances through
:mod:`aeat.application.modelo._registry_resources`, validates
operator/imported casilla maps against the selected
:class:`~aeat.domain.calculations.registry.ModeloRevision`, and refuses
non-canonical printed-number tokens before the calculation engine or
persistence layer sees them.
It also verifies stored :class:`~aeat.domain.modelos.CalculationRevision`
payloads by re-deriving their content-addressed identifiers and checking
:class:`~aeat.domain.calculations.registry.CasillaObservation`/value consistency
before stored payloads are trusted by verification or filing workflows.
See Also:
:func:`aeat.application.modelo._registry_resources.authority_via_resources`
Central registry authority loader used by snapshot-backed guards.
:func:`validate_casilla_input_ids`
Boundary validator for operator-supplied casilla maps.
:func:`assert_revision_content_integrity`
Read-side guard for stored calculation revision drift.
"""
from __future__ import annotations
from collections.abc import Mapping
from decimal import Decimal
from ...core import Period
from ...domain.calculations.registry import (
CasillaId,
InputKind,
ModeloRevision,
RegistrySnapshot,
RegistryValidationError,
VerificationPredicateDefinition,
casilla_noncanonical_reference_targets,
casillas_by_id,
undeclared_casilla_ids,
validated_casilla_id,
)
from ...domain.modelos import (
CalculationRevision,
derive_calculation_revision_id,
)
from ._action_errors import (
AmendmentOverrideCasillaError,
AmendmentVerificationRefusedError,
ExternalModeloImportError,
StoredCalculationDriftError,
)
from ._registry_resources import (
authority_via_resources,
registry_root,
reject_unknown_period_for_revision,
reject_unknown_revision,
)
_NUMERIC_CASILLA_DATA_TYPES: frozenset[str] = frozenset({"decimal", "money", "integer", "ratio"})
[docs]
def reject_incomplete_amendment_casillas(
*,
modelo: str,
filing_year: int,
period: Period,
casilla_values: Mapping[CasillaId, Decimal],
) -> None:
"""Mirror the verify-modelo-revision required-manual gate on amend.
The supplied :class:`~aeat.core.Period` selects the
:class:`~aeat.domain.calculations.registry.RegistrySnapshot` used to read
required manual casillas. Missing required manual casillas raise
:class:`~aeat.application.modelo.AmendmentVerificationRefusedError` before
an amendment can be accepted as complete.
"""
required_optional = required_input_casilla_ids_for_revision(modelo=modelo, filing_year=filing_year, period=period)
if required_optional is None:
raise AmendmentVerificationRefusedError(
f"registry has no snapshot for modelo={modelo!r} filing_year={filing_year} "
f"period={period.registry_token!r}; cannot verify amendment completeness",
translated_message="application.modelo.errors.amendment_verification_refused_no_snapshot",
)
required, _ = required_optional
missing = sorted(casilla_id for casilla_id in required if casilla_id not in casilla_values)
if missing:
raise AmendmentVerificationRefusedError(
f"amendment is incomplete: required casilla id(s) {missing!r} are not present "
f"in the corrected map for modelo={modelo!r} filing_year={filing_year} "
f"period={period.registry_token!r}",
translated_message="application.modelo.errors.amendment_verification_refused_missing_casillas",
)
def _format_noncanonical_casilla_reference(token: str, targets: tuple[CasillaId, ...]) -> str:
rendered_targets = ", ".join(targets)
if len(targets) > 1:
return f"{token!r} is ambiguous; candidate casilla.id values: {rendered_targets}"
return f"{token!r} -> {rendered_targets}"
def _noncanonical_casilla_reference_details(
revision: ModeloRevision,
casilla_ids: tuple[CasillaId, ...],
) -> tuple[dict[CasillaId, tuple[CasillaId, ...]], tuple[CasillaId, ...]]:
noncanonical = {
casilla_id: targets
for casilla_id in casilla_ids
if (targets := casilla_noncanonical_reference_targets(revision, casilla_id))
}
unknown = tuple(casilla_id for casilla_id in casilla_ids if casilla_id not in noncanonical)
return noncanonical, unknown
[docs]
def reject_unknown_override_casillas[CasillaKey](
*,
modelo: str,
filing_year: int,
period: Period,
overrides: Mapping[CasillaKey, Decimal],
) -> dict[CasillaId, Decimal]:
"""Refuse amendment override casillas outside the resolved revision.
Keys are canonicalised as
:class:`~aeat.domain.calculations.registry.CasillaId` values and checked
against the :class:`~aeat.domain.calculations.registry.RegistrySnapshot`
selected by ``modelo``, ``filing_year``, and :class:`~aeat.core.Period`.
Printed-number aliases and ambiguous reused numbers raise
:class:`~aeat.application.modelo.AmendmentOverrideCasillaError` instead of
being projected to a declared casilla.
"""
if not overrides:
return {}
from ...domain.calculations.registry import RegistrySnapshotError
try:
authority = authority_via_resources()
except FileNotFoundError as exc:
raise AmendmentOverrideCasillaError(
translated_message="application.modelo.errors.amendment_registry_root_missing",
context={"registry_root": registry_root()},
) from exc
try:
snapshot = authority.snapshot(modelo, filing_year=filing_year, period=period.registry_token)
except RegistrySnapshotError as exc:
raise AmendmentOverrideCasillaError(
translated_message="application.modelo.errors.amendment_registry_snapshot_unresolved",
context={"modelo": modelo, "filing_year": filing_year, "period": period.registry_token},
) from exc
malformed: list[str] = []
canonical_overrides: dict[CasillaId, Decimal] = {}
for casilla_id, value in overrides.items():
try:
canonical_overrides[validated_casilla_id(casilla_id, surface="amendment override casilla")] = value
except ValueError:
malformed.append(repr(casilla_id))
if malformed:
raise AmendmentOverrideCasillaError(
translated_message="application.modelo.errors.amendment_unknown_casillas",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"casillas": sorted(malformed),
},
)
unknown = undeclared_casilla_ids(snapshot.revision, canonical_overrides)
if unknown:
noncanonical, unknown_only = _noncanonical_casilla_reference_details(snapshot.revision, unknown)
if noncanonical:
details = "; ".join(
_format_noncanonical_casilla_reference(casilla_id, targets)
for casilla_id, targets in sorted(noncanonical.items())
)
raise AmendmentOverrideCasillaError(
f"amendment override casillas must use canonical casilla.id values; "
f"non-canonical reference tokens are not accepted: {details}",
translated_message="application.modelo.errors.amendment_unknown_casillas",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"casillas": sorted(noncanonical),
},
)
raise AmendmentOverrideCasillaError(
translated_message="application.modelo.errors.amendment_unknown_casillas",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"casillas": unknown_only,
},
)
return canonical_overrides
[docs]
def reject_unknown_import_casillas[CasillaKey](
*,
modelo: str,
filing_year: int,
period: Period,
casilla_values: Mapping[CasillaKey, Decimal],
) -> tuple[RegistrySnapshot, dict[CasillaId, Decimal]]:
"""Validate imported casilla ids and return the resolved registry snapshot.
The snapshot is a
:class:`~aeat.domain.calculations.registry.RegistrySnapshot`. The returned
mapping is keyed by canonical
:class:`~aeat.domain.calculations.registry.CasillaId` values declared by the
selected :class:`~aeat.domain.calculations.registry.ModeloRevision`.
Unknown, malformed, and non-canonical printed numbers raise
:class:`~aeat.application.modelo.ExternalModeloImportError` so imported AEAT
values enter observation projection only under registry ids.
"""
from ...domain.calculations.registry import RegistrySnapshotError
try:
authority = authority_via_resources()
except FileNotFoundError as exc:
raise ExternalModeloImportError(
translated_message="application.modelo.errors.external_import_registry_root_missing",
context={"registry_root": registry_root()},
) from exc
try:
snapshot = authority.snapshot(modelo, filing_year=filing_year, period=period.registry_token)
except RegistrySnapshotError as exc:
raise ExternalModeloImportError(
translated_message="application.modelo.errors.external_import_registry_snapshot_unresolved",
context={"modelo": modelo, "filing_year": filing_year, "period": period.registry_token},
) from exc
malformed: list[str] = []
canonical_values: dict[CasillaId, Decimal] = {}
for casilla_id, value in casilla_values.items():
try:
canonical_values[validated_casilla_id(casilla_id, surface="external import casilla")] = value
except ValueError:
malformed.append(repr(casilla_id))
if malformed:
raise ExternalModeloImportError(
translated_message="application.modelo.errors.external_import_unknown_casillas",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"casillas": sorted(malformed),
},
)
unknown = undeclared_casilla_ids(snapshot.revision, canonical_values)
if unknown:
noncanonical, unknown_only = _noncanonical_casilla_reference_details(snapshot.revision, unknown)
if noncanonical:
details = "; ".join(
_format_noncanonical_casilla_reference(casilla_id, targets)
for casilla_id, targets in sorted(noncanonical.items())
)
raise ExternalModeloImportError(
f"external import casillas must use canonical casilla.id values; "
f"non-canonical reference tokens are not accepted: {details}",
translated_message="application.modelo.errors.external_import_unknown_casillas",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"casillas": sorted(noncanonical),
},
)
raise ExternalModeloImportError(
translated_message="application.modelo.errors.external_import_unknown_casillas",
context={
"modelo": modelo,
"filing_year": filing_year,
"period": period.registry_token,
"casillas": unknown_only,
},
)
return snapshot, canonical_values
[docs]
def verification_predicates_for_revision(
*,
modelo: str,
filing_year: int,
period: Period,
) -> tuple[VerificationPredicateDefinition, ...]:
"""Return verification predicate rows for the selected revision.
The rows are
:class:`~aeat.domain.calculations.registry.VerificationPredicateDefinition`
instances. Missing registry roots or unresolved
:class:`~aeat.domain.calculations.registry.RegistrySnapshot` instances
produce an empty tuple so callers can degrade to their existing verification
paths.
"""
from ...domain.calculations.registry import RegistrySnapshotError
try:
authority = authority_via_resources()
except FileNotFoundError:
return ()
try:
snapshot = authority.snapshot(modelo, filing_year=filing_year, period=period.registry_token)
except RegistrySnapshotError:
return ()
return snapshot.revision.verification_predicates
[docs]
def assert_revision_content_integrity(revision: CalculationRevision) -> None:
"""Check revision integrity; raise stored-calculation drift on mismatch.
The supplied :class:`~aeat.domain.modelos.CalculationRevision` is re-hashed
from its persisted inputs, bindings, relations, casilla values, and source
metadata before its provenance observations are compared with
``casilla_values``. This is a defense-in-depth read-side check for raw
storage corruption that bypassed normal model construction; a mismatched
observation envelope raises
:exc:`~aeat.application.modelo.StoredCalculationDriftError` before the
revision is treated as authoritative.
"""
expected = derive_calculation_revision_id(
work_unit_id=revision.work_unit_id,
input_values_by_casilla_id=revision.input_values_by_casilla_id,
binding_overrides=revision.binding_overrides,
row_binding_values=revision.row_binding_values,
relation_overrides=revision.relation_overrides,
casilla_values=revision.casilla_values,
source_transaction_ids=revision.source_transaction_ids,
borrador_snapshot_id=revision.borrador_snapshot_id,
bindings_sourced_from_borrador=revision.bindings_sourced_from_borrador,
detail_rows=revision.detail_rows,
)
if expected != revision.calculation_revision_id:
raise StoredCalculationDriftError(
f"calculation revision {revision.calculation_revision_id!r} content-address mismatch: "
f"stored id does not match re-derived hash of its payload; "
f"the record may have been tampered with or corrupted",
)
for obs in revision.observations:
stored = revision.casilla_values.get(obs.casilla_id)
if stored is None:
raise StoredCalculationDriftError(
f"calculation revision {revision.calculation_revision_id!r} provenance drift: "
f"observation for casilla {obs.casilla_id!r} is present but casilla_values "
f"has no entry for it; the provenance envelope may have been tampered with",
)
if obs.value != stored:
raise StoredCalculationDriftError(
f"calculation revision {revision.calculation_revision_id!r} provenance drift: "
f"observation value for casilla {obs.casilla_id!r} is {obs.value!r} "
f"but casilla_values holds {stored!r}; "
f"the record may have been tampered with or corrupted",
)
__all__ = [
"assert_revision_content_integrity",
"authority_via_resources",
"registry_root",
"reject_incomplete_amendment_casillas",
"reject_unknown_import_casillas",
"reject_unknown_override_casillas",
"reject_unknown_period_for_revision",
"reject_unknown_revision",
"required_input_casilla_ids_for_revision",
"validate_casilla_input_ids",
"verification_predicates_for_revision",
]