"""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",
]