Source code for aeat.application.modelo._action_errors

"""Typed exception vocabulary for modelo application actions.

The classes in this module are the stable application-layer errors raised by
modelo work-unit lifecycle, calculation, verification, filing, amendment,
external-import, and workflow-gate services. They all inherit from
:class:`aeat.domain.modelos._errors.ModeloError` so CLI and API error
boundaries can route them through the central error-code registry without
depending on the implementation module that raised them.

Most classes are deliberately thin taxonomy markers whose operator-facing code,
message key, and suggestion live in :mod:`aeat.core.errors.registry`. The richer
contracts are kept here when the exception must preserve domain context without
leaking it into rendered error payloads, as with
:class:`ModeloWorkflowGateError` and its private
:class:`~aeat.application.workflow.WorkflowResult`.

See Also:
    :mod:`aeat.application.modelo`:
        Public package facade for these action errors.
    :mod:`aeat.core.errors.registry`:
        Maps these exception classes to stable error codes and suggestions.
    :mod:`aeat.application.modelo._workflow_gate`:
        Raises :class:`ModeloWorkflowGateError` after persisting an aborted
        workflow run.
    :mod:`aeat.application.modelo._profile_readiness_gate`:
        Raises :class:`ModeloProfileReadinessError` for filing-grade profile
        preflight failures.
"""

from __future__ import annotations

from ...core.errors import CoreNotFoundError
from ...domain.modelos import ModeloError
from ..workflow import WorkflowAbortReason, WorkflowResult

WORKFLOW_GATE_LEGAL_REFS: tuple[str, ...] = (
    "ley-58-2003:art-119",
    "ley-58-2003:art-120",
    "ley-58-2003:art-122",
)
"""Legal anchors attached to workflow-gate refusal observations.

The cross-period clean-state finding tests assert these ids remain present in
the workflow-gate provenance payload. They correspond to the Ley 58/2003
articles that frame declaration, self-assessment, and complementary declaration
flows.
"""


[docs] class WorkUnitNotFoundError(ModeloError, KeyError): """Raised when a work-unit lookup or mutation targets a missing id."""
[docs] class WorkUnitAlreadyDiscardedError(ModeloError): """Raised when discard is invoked on a work unit already discarded."""
[docs] class WorkUnitMutationRefusedError(ModeloError): """Raised when a mutation targets a discarded work unit."""
[docs] class CalculationRevisionNotFoundError(ModeloError, CoreNotFoundError): """Raised when a calculation revision lookup fails."""
[docs] class CalculationRevisionStateError(ModeloError): """Raised when a state transition is requested from an incompatible source state."""
[docs] class ModeloRecordNotFoundError(ModeloError, KeyError): """Raised when a filing record lookup fails."""
[docs] class VerificationReportNotFoundError(ModeloError, KeyError): """Raised when a verification report lookup fails."""
[docs] class AmendmentEvidenceMissingError(ModeloError): """Raised when the modelo-amend path lacks imported official evidence."""
[docs] class AmendmentTargetStateError(ModeloError): """Raised when the modelo-amend path targets a non-current filing record."""
[docs] class AmendmentKindNotPermittedError(ModeloError): """Raised when the requested amendment kind is not legally available for the period. AEAT's amendment mechanism changed over time: the unified autoliquidación rectificativa (LGT art. 120.4, RD 117/2024) replaced the dual complementaria/solicitud-de-rectificación regime (LGT art. 122.2 / art. 120.3) only from the period each modelo's own orden establishes (see :mod:`aeat.core._amendment_kind_regime`). Requesting ``rectificativa`` for a pre-adoption period, or ``complementaria`` for a modelo/period where rectificativa has replaced it as the ordinary correction mechanism, is refused rather than silently accepted or silently downgraded — the accepted kind set for the resolved period is always named in the refusal. """
[docs] class AmendmentComplementariaLiabilityDecreaseError(ModeloError): """Raised when a pre-rectificativa complementaria would decrease liability. Before the autoliquidación rectificativa unification (LGT art. 120.4), a self-filed ``complementaria`` (LGT art. 122.2) can only ever RAISE the taxpayer's own declared tax due (or lower a requested devolución): "los obligados tributarios podrán presentar autoliquidaciones complementarias" when the new autoliquidación yields "un importe a ingresar superior... o una cantidad a devolver inferior". A correction that LOWERS the declared liability is not a complementaria in law; it requires the separate ``solicitud de rectificación`` procedure (LGT art. 120.3, developed by RGAT art. 126-128). Filing a liability-decreasing correction as a complementaria would silently misrepresent which legal procedure the taxpayer used, so it is refused with guidance toward the correct procedure rather than silently accepted. """
[docs] class StoredCalculationDriftError(ModeloError): """Raised when a persisted calculation revision has drifted from its content-addressed id."""
[docs] class ExternalModeloImportError(ModeloError): """Raised when the external-filing import path cannot persist an imported baseline."""
[docs] class ModeloLocalObservationError(ModeloError): """Raised when an operator-supplied local observation cannot be persisted."""
[docs] class ModeloCrossPeriodCleanStateError(ModeloError): """Raised when a filing-grade workflow lacks clean prior-filing proof."""
[docs] class ModeloWorkflowGateError(ModeloError): """Raised when the workflow gate refuses an internal file transition. The constructor stores the live :class:`~aeat.application.workflow.WorkflowResult` on a private attribute and exposes it through :attr:`result`. The rendered error context contains only primitive machine codes (``abort_code`` and ``stage``), which keeps CLI JSON/text payloads stable while allowing telemetry and tests to inspect the full workflow run. See Also: :func:`aeat.application.modelo._workflow_gate.run_revision_workflow_gate`: Persists the workflow run and raises this error for aborted results. :func:`aeat.core.errors.render_error_text`: Renders the primitive context without serialising the live result. """ def __init__(self, result: WorkflowResult) -> None: self._result = result reason = result.aborted_reason.value if result.aborted_reason is not None else "unknown" summary = result.summary.strip() or "the workflow gate aborted this transition" message = summary suggestion: str | None = None if result.aborted_reason is WorkflowAbortReason.NO_PENDING_OBLIGATION: message = None suggestion = "aeat app modelo export <work-unit-id> --output <path>" super().__init__( message, context={ "abort_code": reason, "stage": result.final_stage.value, }, suggestion=suggestion, ) @property def result(self) -> WorkflowResult: """Return the live :class:`~aeat.application.workflow.WorkflowResult` that triggered the abort.""" return self._result
[docs] class AmendmentOverrideCasillaError(ModeloError): """Raised when an amendment override targets an undeclared casilla id."""
[docs] class AmendmentVerificationRefusedError(ModeloError): """Raised when the corrected casilla map fails verification."""
[docs] class CalculationRegistryUnavailableError(ModeloError): """Raised when the registry snapshot for a work unit cannot be resolved."""
[docs] class ModeloAggregationBindingError(ModeloError): """Raised when bucket-derived aggregation bindings conflict with caller input."""
[docs] class ModeloRequiredBindingsMissingError(ModeloError): """Raised when Modelo 202 lifecycle work lacks required calculation bindings."""
[docs] class ModeloProfileReadinessError(ModeloError): """Raised when filing-grade modelo work starts with missing active-profile facts."""
[docs] class CasillaProvenanceMissingError(ModeloError): """Raised when an engine-result casilla has no registry definition."""
[docs] class ModeloApplicabilityFilterError(ModeloError): """Raised when an unknown applicability filter name is encountered."""
[docs] class ModeloRefundElectionNotEligibleError(ModeloError): """Raised when an operator elects a Modelo 303 refund for an ineligible period. A non-REDEME taxpayer may request a negative Modelo 303 result back as a refund (devolución, Tipo de declaración ``D``) only in the last filing period of the year (the annual liquidación, Ley 37/1992 art. 116). Electing ``devolver`` for any earlier period is refused rather than silently downgraded to compensación — a silent downgrade would hide that the operator's refund request was discarded, and a silent upgrade would file a refund the law does not permit for the period. The fix is operator-driven: carry the credit forward (``compensar``), or make the election in the year's last period. """
[docs] class ModeloRefundAccountMissingError(ModeloError): """Raised when a refund-disposition export has no refund account on file. When the determined disposition is a refund (devolución, ``D`` / ``V`` / ``X``) the fichero must carry the cuenta-devolución block AEAT pays into — the IBAN, or the SWIFT-BIC plus foreign-bank block for a non-SEPA account. If the operator's profile carries no refund account (no ``iban``), the export REFUSES rather than emitting an empty or partial DID block: an empty refund block produces a devolución fichero AEAT cannot pay — a silent, defective filing. The fix is operator-driven: configure a refund account on the profile, or carry the credit forward (``compensar``) instead of requesting a refund. This is the no-silent-under-declaration sibling of the election's eligibility refusal. """
[docs] class WorkUnitRevisionDivergenceError(ModeloError): """Raised when the registry's law-determined revision diverges from the work unit's pinned revision. This can only happen when the registry's law-mapping was corrected after the work unit was created (the creation gate now enforces resolver-equality), or for work units persisted before the strengthened creation gate landed. The resolution is to re-create the work unit so its identity reflects the corrected law-determined revision. """
__all__ = [ "WORKFLOW_GATE_LEGAL_REFS", "AmendmentComplementariaLiabilityDecreaseError", "AmendmentEvidenceMissingError", "AmendmentKindNotPermittedError", "AmendmentOverrideCasillaError", "AmendmentTargetStateError", "AmendmentVerificationRefusedError", "CalculationRegistryUnavailableError", "CalculationRevisionNotFoundError", "CalculationRevisionStateError", "CasillaProvenanceMissingError", "ExternalModeloImportError", "ModeloAggregationBindingError", "ModeloApplicabilityFilterError", "ModeloCrossPeriodCleanStateError", "ModeloLocalObservationError", "ModeloProfileReadinessError", "ModeloRecordNotFoundError", "ModeloRefundAccountMissingError", "ModeloRefundElectionNotEligibleError", "ModeloRequiredBindingsMissingError", "ModeloWorkflowGateError", "StoredCalculationDriftError", "VerificationReportNotFoundError", "WorkUnitAlreadyDiscardedError", "WorkUnitMutationRefusedError", "WorkUnitNotFoundError", "WorkUnitRevisionDivergenceError", ]