"""Canonical typed-answer model and projection slot for the setup flow.
:class:`SetupAnswers` is the authoritative typed-answers model for the wizard
``setup`` flow. Domain modules import it from here, not from the application
wizard layer, so the permitted dependency direction remains domain-to-core
rather than domain-to-application.
This module owns typed answer validation and the core registration slot for the
reverse projection from persisted canonical-token strings. It does not own
prompt rendering, profile persistence, secure storage, deadline scheduling, or
registry semantics. The application wizard registers its concrete projector at
startup with :func:`register_project_answers`; domain consumers call
:func:`project_answers` through this core slot and receive
:class:`ProjectAnswersNotRegisteredError` if startup has not installed it.
The contract mirrors :mod:`aeat.core.wizard_catalogue`: the application layer
declares the ``SETUP_FLOW`` descriptor, while core exposes the stable answer
model and the projection hook. Downstream profile construction, including
``taxpayer_profile_from_mapping``, therefore stays aligned with wizard
canonical-token parsing without importing application modules directly.
Domain taxonomy types (``EntityType``, ``IVARegime``, etc.) are imported lazily
inside validators rather than at module level to break the circular import
path: ``aeat.core.setup_answers`` -> ``aeat.domain.deadlines._models`` ->
``aeat.domain.deadlines.__init__`` -> ``aeat.domain.deadlines._profiles`` ->
``aeat.core.setup_answers``. This mirrors the deferral strategy used in
``aeat.core.resources._repos.*`` and is the established project pattern.
"""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, Protocol, runtime_checkable
from pydantic import BaseModel, Field, field_validator, model_validator
from ..core import STRICT_FROZEN_CONFIG
from .errors import CoreError, ProfileAnswerTypeError
from .external_constants import DEFAULT_OUTPUT_LANGUAGE, OutputLanguage
from .logging import get_logger
_log = get_logger(__name__)
def _parse_optional_bool_token(value: object, *, field_name: str) -> object:
"""Parse a three-state optional wizard boolean token.
Accepted affirmative tokens become ``True``; accepted negative tokens become
``False``. Blank input and ``None`` remain the empty-string sentinel so
profile persistence drops the fact instead of writing a declared false
value. This helper does not parse prompt labels or locale text; it accepts
only canonical yes/no tokens.
"""
if value == "" or value is None:
return ""
if isinstance(value, bool):
return value
if isinstance(value, str):
token = value.strip().lower()
if token == "":
return ""
if token in {"true", "1", "yes", "y", "si", "sí"}:
return True
if token in {"false", "0", "no", "n"}:
return False
raise ValueError(f"{field_name} must be a boolean, blank, or a recognised canonical token")
# ---------------------------------------------------------------------------
# project_answers registration slot
# ---------------------------------------------------------------------------
[docs]
class ProfileRegistrationError(CoreError):
"""Raised when :func:`register_project_answers` is called a second time with a different callable.
A double-registration with the same callable is a safe no-op; a double-registration
with a *different* callable is a programming error that must be surfaced as a
typed, registry-bound exception so callers receive a structured error envelope
rather than a bare :exc:`RuntimeError`.
"""
[docs]
class ProjectAnswersNotRegisteredError(CoreError):
"""Raised when domain code calls :func:`project_answers` before registration."""
def __init__(self) -> None:
"""Initialise with a fixed message directing the caller to register the projection."""
super().__init__(
"project_answers has not been registered. "
"Call register_project_answers() at application startup before "
"any domain module invokes the projection.",
)
[docs]
@runtime_checkable
class ProjectAnswersFn(Protocol):
"""Structural type for the project_answers callable.
Satisfied by ``aeat.application.wizard._persistence.project_answers``.
Domain code depends only on this protocol.
"""
# KWARGS-ANY-RATIONALE-PROFILE-WIZARD-FLOW-CIRCULAR:
# WizardFlow type lives in aeat.application.wizard; importing here would
# create circular dependency.
def __call__(self, flow: Any, values: Mapping[str, str]) -> BaseModel:
"""Project canonical-token values into the typed answers model."""
... # pragma: no cover
_PROJECT_ANSWERS_SLOT: list[ProjectAnswersFn] = []
[docs]
def register_project_answers(fn: ProjectAnswersFn) -> None:
"""Register the concrete project_answers implementation from the application layer.
Call exactly once at application startup (e.g. in
``aeat.application.wizard._persistence`` module body after the function is
defined). A second call with an identical callable is a no-op; a second
call with a different callable raises :class:`ProfileRegistrationError`.
Domain code should depend on :func:`project_answers`, not on the
application-layer implementation object registered here.
"""
if _PROJECT_ANSWERS_SLOT:
if _PROJECT_ANSWERS_SLOT[0] is fn:
return
raise ProfileRegistrationError(
translated_message="core.profile.errors.registration_duplicate_callable",
)
_PROJECT_ANSWERS_SLOT.append(fn)
_log.debug("project_answers registered: %r", fn)
[docs]
def get_project_answers() -> ProjectAnswersFn:
"""Return the registered project_answers implementation.
Returns:
The :class:`ProjectAnswersFn` registered via
:func:`register_project_answers`.
Raises:
ProjectAnswersNotRegisteredError: When the application layer has not yet
called :func:`register_project_answers`.
"""
if not _PROJECT_ANSWERS_SLOT:
raise ProjectAnswersNotRegisteredError()
return _PROJECT_ANSWERS_SLOT[0]
# KWARGS-ANY-RATIONALE-PROFILE-WIZARD-FLOW-CIRCULAR:
# WizardFlow type lives in aeat.application.wizard; importing here would create
# circular dependency.
[docs]
def project_answers(flow: Any, values: Mapping[str, str]) -> BaseModel:
"""Invoke the registered project_answers implementation.
Delegates to the application-layer function registered via
:func:`register_project_answers`. Domain callers import this function from
``aeat.core.setup_answers`` so they never acquire a direct dependency on
``aeat.application.wizard._persistence``.
Args:
flow: The wizard flow descriptor identifying which flow to
project answers for.
values: Mapping of canonical token keys to raw string values
collected from the wizard or read from profile storage. Blank
strings preserve undeclared optional facts for the registered
projector to interpret.
Returns:
A typed answers model instance produced by the registered
implementation.
"""
return get_project_answers()(flow, values)
# ---------------------------------------------------------------------------
# Lazy domain-type accessors
#
# All domain imports are deferred inside validators and accessors below to
# avoid the circular import:
# aeat.core.setup_answers → aeat.domain.deadlines._models
# → aeat.domain.deadlines.__init__ → aeat.domain.deadlines._profiles
# → aeat.core.setup_answers (partially initialised → ImportError)
# ---------------------------------------------------------------------------
# ANY-RETURN-RATIONALE-PROFILE-LAZY-MODULE: returns the
# aeat.domain.deadlines.taxpayer_model module object; a typed return would
# require importing the module at definition time, re-introducing the
# circular import described in the block comment above.
def _m() -> Any:
"""Return the aeat.domain.deadlines.taxpayer_model module (lazy)."""
import importlib
return importlib.import_module("aeat.domain.deadlines.taxpayer_model")
# ANY-RETURN-RATIONALE-PROFILE-LAZY-MODULE: returns the
# aeat.domain.contribuyente module object; typed return would require importing the
# module at definition time, re-introducing the circular import.
def _p() -> Any:
"""Return the aeat.domain.contribuyente module (lazy)."""
import importlib
return importlib.import_module("aeat.domain.contribuyente")
# ANY-RETURN-RATIONALE-PROFILE-LAZY-MODULE: returns the CCAA enum class object;
# typed return would require importing the module at definition time,
# re-introducing the circular import.
def _ccaa() -> Any:
"""Return the CCAA enum class (lazy)."""
import importlib
return importlib.import_module("aeat.domain.contribuyente").CCAA
# ---------------------------------------------------------------------------
# SetupAnswers
# ---------------------------------------------------------------------------
[docs]
class SetupAnswers(BaseModel):
"""Typed answers collected by the ``setup`` flow.
Canonical home is :mod:`aeat.core.setup_answers`. The application wizard layer
imports :class:`SetupAnswers` from here; the domain deadline engine likewise
imports it from here — no layer needs to cross the hexagonal boundary.
The model stores canonical answer tokens and typed taxonomy values for the
setup flow. It is not the persisted profile record and it is not the
deadline-engine taxpayer profile; those are produced downstream by wizard
persistence and deadline profile projection. Where validators allow the
empty string, the value means undeclared/no answer rather than false, zero,
or a default legal fact.
Field annotations use ``Any`` for domain taxonomy union types because those
types are loaded lazily inside validators to prevent a circular import.
That ``Any`` is not a loose schema: validators enforce the same invariants
the original typed annotations carried, reject values outside the declared
enum / blank-string set, and raise
:class:`~aeat.core.errors.ProfileAnswerTypeError`.
"""
model_config = STRICT_FROZEN_CONFIG
# ── profile identity ─────────────────────────────────────────────────
tax_id: str = Field(min_length=1)
name: str = ""
surnames: str = ""
legal_name: str = ""
activity: str = ""
"""Free-text actividad económica / epígrafe IAE description."""
address_postcode: str = ""
"""Optional Spanish postcode for the taxpayer's activity/contact address."""
activity_start_date: str = ""
"""Optional ISO-8601 censo alta date for the economic activity."""
taxation_type: Any = ""
output_language: OutputLanguage = DEFAULT_OUTPUT_LANGUAGE
@field_validator("output_language", mode="before")
@classmethod
def _coerce_output_language(cls, value: object) -> OutputLanguage:
if isinstance(value, OutputLanguage):
return value
if isinstance(value, str):
return OutputLanguage(value)
raise ProfileAnswerTypeError(
translated_message="core.profile.errors.output_language_type",
)
# ── taxpayer type (three-axis taxpayer model) ────────────────────────
entity_type: Any = ""
legal_entity_form: Any = ""
incn_prior_12_months: str = ""
"""Optional INCN as a canonical decimal string."""
new_entity_first_two_profit_periods: Any = ""
"""Optional three-state bool for LIS Art. 29 new-entity rate."""
ley_49_2002_option_declared: Any = ""
"""Optional three-state bool for the Ley 49/2002 Title II option."""
ley_49_2002_option_date: str = ""
"""ISO-8601 date declared for the Ley 49/2002 Title II option."""
ley_49_2002_renunciation_declared: Any = ""
"""Optional three-state bool for Ley 49/2002 Title II renunciation."""
ley_49_2002_renunciation_date: str = ""
"""ISO-8601 date declared for the Ley 49/2002 Title II renunciation."""
irpf_income_categories: str = ""
"""Comma-separated set of IrpfIncomeCategory tokens."""
# ── taxpayer biographic ──────────────────────────────────────────────
taxpayer_sex: Any = ""
taxpayer_marital_status: Any = ""
taxpayer_marriage_date: str = ""
"""ISO-8601 date when the current marriage began."""
taxpayer_birth_date: str = ""
taxpayer_disability_grade: Any = ""
taxpayer_death_date: str = ""
# ── spouse (taxation_type == "2") ────────────────────────────────────
spouse_tax_id: str = ""
spouse_name: str = ""
spouse_surnames: str = ""
spouse_birth_date: str = ""
spouse_sex: Any = ""
spouse_disability_grade: Any = ""
spouse_non_resident_irpf: bool = False
spouse_eu_eea_resident: bool = False
spouse_eu_eea_country: str = ""
# ── family ───────────────────────────────────────────────────────────
family_descendants_eu_eea_deduction: bool = False
family_minor_children_in_unit: bool = False
situacion_familiar: Any = ""
"""Art. 82 LIRPF family situation governing conjunta eligibility."""
unidad_familiar_descendientes_exclusivos: Any = ""
"""Custodia compartida progenitor claiming the monoparental unidad familiar."""
# ── IVA ──────────────────────────────────────────────────────────────
iva_regime: Any = None
iva_roi_enrolled: bool = False
iva_oss_enrolled: bool = False
iva_group_member_enrolled: bool = False
iva_group_dominant_entity_enrolled: bool = False
iva_sii_enrolled: bool = False
iva_redeme_enrolled: bool = False
iva_intracommunity_operations_exceed_50000_eur: bool = False
# ── enrollment ───────────────────────────────────────────────────────
enrollment_large_company: bool = False
enrollment_public_administration_budget_gt_6000000: bool = False
# ── retencion / modelo obligation booleans ───────────────────────────
has_employees: bool = False
pays_professionals_with_retencion: bool = False
professional_income_withholding_ge_70pct: bool = False
art109_activity_income_withholding_ge_70pct: bool = False
pays_rent_with_retencion: bool = False
pays_capital_income_with_retencion: bool = False
modelo_111_no_retenciones_periods: str = ""
irpf_estimation_regime: Any = ""
objective_estimation_modulos_iae_epigraph: str = ""
objective_estimation_modulos_module_1_units: str = ""
objective_estimation_modulos_module_2_units: str = ""
objective_estimation_modulos_module_3_units: str = ""
objective_estimation_modulos_module_4_units: str = ""
objective_estimation_modulos_module_5_units: str = ""
objective_estimation_modulos_module_6_units: str = ""
objective_estimation_modulos_module_7_units: str = ""
irpf_special_regime: Any = ""
"""IRPF special-regime axis. Blank for the general regime."""
irpf_special_regime_start_date: str = ""
"""ISO-8601 opt-in election date for the special regime."""
does_intracomunitario: bool = False
third_party_transactions_above_347_threshold: bool = False
bienes_extranjero_above_threshold: bool = False
monedas_virtuales_extranjero_above_threshold: bool = False
# ── residence ────────────────────────────────────────────────────────
tax_residence_ccaa: Any = None
fiscal_residency: Any = ""
"""Fiscal residency category."""
country_of_fiscal_residence: str = ""
"""ISO 3166-1 alpha-2 code of the country of fiscal residence."""
representante_fiscal_nif: str = ""
"""NIF/NIE of the fiscal representative in Spain."""
representante_fiscal_nombre: str = ""
"""Full name of the fiscal representative in Spain."""
# ── capabilities ─────────────────────────────────────────────────────
cloud_evidence_upload: bool = False
llm_vision: bool = True
google_export: bool = True
# ── notes ────────────────────────────────────────────────────────────
notes: str = ""
# ------------------------------------------------------------------
# Validators — each lazily resolves domain types via _m() / _p()
# ------------------------------------------------------------------
# CAST-RATIONALE-PROFILE-FIELD-VALIDATOR-ANY: pydantic's @field_validator
# with mode="before" requires the validator to return Any; the post-coercion
# value is validated by pydantic against the field's declared type after
# this validator returns. A narrower return type (e.g., IVARegime) is not
# expressible here because IVARegime is resolved lazily via _m() to avoid
# a circular import at module load time.
@field_validator("iva_regime", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_iva_regime(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
iva_regime_cls = _m().IVARegime
if isinstance(value, iva_regime_cls):
return value
if value is None:
return iva_regime_cls.GENERAL
if isinstance(value, str):
return iva_regime_cls(value.upper()) if value else iva_regime_cls.GENERAL
raise ProfileAnswerTypeError("iva_regime must be an IVARegime member or string token")
@field_validator("tax_residence_ccaa", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_tax_residence_ccaa(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
ccaa_cls = _ccaa()
if isinstance(value, ccaa_cls):
return value
if value is None:
return ccaa_cls.MADRID
if isinstance(value, str):
return ccaa_cls(value) if value else ccaa_cls.MADRID
raise ProfileAnswerTypeError("tax_residence_ccaa must be a CCAA member or string token")
@field_validator("entity_type", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_entity_type(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
entity_type_cls = _m().EntityType
if isinstance(value, entity_type_cls):
return value
if isinstance(value, str):
return entity_type_cls(value)
raise ProfileAnswerTypeError("entity_type must be an EntityType member, string token, or blank")
@field_validator("legal_entity_form", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_legal_entity_form(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
legal_entity_form_cls = _m().LegalEntityForm
if isinstance(value, legal_entity_form_cls):
return value
if isinstance(value, str):
return legal_entity_form_cls(value)
raise ProfileAnswerTypeError("legal_entity_form must be a LegalEntityForm member, string token, or blank")
@field_validator("irpf_estimation_regime", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_irpf_estimation_regime(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
irpf_estimation_regime_cls = _m().IrpfEstimationRegime
if isinstance(value, irpf_estimation_regime_cls):
return value
if isinstance(value, str):
return irpf_estimation_regime_cls(value)
raise ProfileAnswerTypeError(
"irpf_estimation_regime must be an IrpfEstimationRegime member, string token, or blank",
)
@field_validator("situacion_familiar", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_situacion_familiar(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
situacion_familiar_cls = _p().SituacionFamiliar
if isinstance(value, situacion_familiar_cls):
return value
if isinstance(value, str):
return situacion_familiar_cls(value)
raise ProfileAnswerTypeError("situacion_familiar must be a SituacionFamiliar member, string token, or blank")
@field_validator("unidad_familiar_descendientes_exclusivos", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_unidad_familiar_descendientes_exclusivos( # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
cls,
value: object,
) -> Any:
if value == "":
return ""
if isinstance(value, bool):
return value
if isinstance(value, str):
if value.lower() == "true":
return True
if value.lower() == "false":
return False
raise ProfileAnswerTypeError(
"unidad_familiar_descendientes_exclusivos must be a bool, 'true', 'false', or blank",
)
@field_validator("irpf_special_regime", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_irpf_special_regime(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
irpf_special_regime_cls = _m().IrpfSpecialRegime
if isinstance(value, irpf_special_regime_cls):
return value
if isinstance(value, str):
return irpf_special_regime_cls(value)
raise ProfileAnswerTypeError("irpf_special_regime must be an IrpfSpecialRegime member, string token, or blank")
@field_validator("fiscal_residency", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_fiscal_residency(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
fiscal_residency_cls = _m().FiscalResidency
if isinstance(value, fiscal_residency_cls):
return value
if isinstance(value, str):
return fiscal_residency_cls(value)
raise ProfileAnswerTypeError("fiscal_residency must be a FiscalResidency member, string token, or blank")
@field_validator("irpf_income_categories")
@classmethod
def _validate_irpf_income_categories(cls, value: str) -> str:
irpf_income_category_cls = _m().IrpfIncomeCategory
tokens = [token.strip() for token in value.split(",") if token.strip()]
for token in tokens:
irpf_income_category_cls(token)
return ",".join(tokens)
@field_validator("taxation_type", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_taxation_type(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
renta_declaracion_type_cls = _p().RentaDeclaracionType
if isinstance(value, renta_declaracion_type_cls):
return value
if isinstance(value, str):
return renta_declaracion_type_cls(value)
raise ProfileAnswerTypeError("taxation_type must be a RentaDeclaracionType member, string token, or blank")
@field_validator("taxpayer_sex", "spouse_sex", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_sex_code(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
sex_code_cls = _p().RentaSexCode
if isinstance(value, sex_code_cls):
return value
if isinstance(value, str):
return sex_code_cls(value)
raise ProfileAnswerTypeError("sex code must be a RentaSexCode member, string token, or blank")
@field_validator("taxpayer_marital_status", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_marital_status(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
marital_status_cls = _p().RentaMaritalStatus
if isinstance(value, marital_status_cls):
return value
if isinstance(value, str):
return marital_status_cls(value)
raise ProfileAnswerTypeError(
"taxpayer_marital_status must be a RentaMaritalStatus member, string token, or blank",
)
@field_validator("taxpayer_marriage_date")
@classmethod
def _validate_taxpayer_marriage_date(cls, value: str) -> str:
from datetime import date
if value == "":
return value
try:
date.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"taxpayer_marriage_date must be an ISO-8601 date (YYYY-MM-DD), got {value!r}") from exc
return value
@field_validator(
"taxpayer_disability_grade",
"spouse_disability_grade",
mode="before",
)
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_disability_grade(cls, value: object) -> Any: # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
if value == "":
return ""
disability_grade_cls = _p().RentaDisabilityGrade
if isinstance(value, disability_grade_cls):
return value
if isinstance(value, str):
return disability_grade_cls(value)
raise ProfileAnswerTypeError("disability grade must be a RentaDisabilityGrade member, string token, or blank")
@field_validator("incn_prior_12_months")
@classmethod
def _validate_incn_prior_12_months(cls, value: str) -> str:
from decimal import Decimal, InvalidOperation
if value == "":
return value
try:
Decimal(value)
except InvalidOperation as exc:
raise ValueError(f"incn_prior_12_months must be a decimal number, got {value!r}") from exc
return value
@field_validator(
"objective_estimation_modulos_module_1_units",
"objective_estimation_modulos_module_2_units",
"objective_estimation_modulos_module_3_units",
"objective_estimation_modulos_module_4_units",
"objective_estimation_modulos_module_5_units",
"objective_estimation_modulos_module_6_units",
"objective_estimation_modulos_module_7_units",
)
@classmethod
def _validate_objective_estimation_modulos_units(cls, value: str) -> str:
from decimal import Decimal, InvalidOperation
if value == "":
return value
try:
Decimal(value)
except InvalidOperation as exc:
raise ValueError(f"objective-estimation modulos units must be decimal numbers, got {value!r}") from exc
return value
@field_validator("new_entity_first_two_profit_periods", mode="before")
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is always
# a typed StrEnum/enum member.
def _parse_new_entity_first_two_profit_periods( # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
cls,
value: object,
) -> Any:
return _parse_optional_bool_token(
value,
field_name="new_entity_first_two_profit_periods",
)
@field_validator(
"ley_49_2002_option_declared",
"ley_49_2002_renunciation_declared",
mode="before",
)
@classmethod
# ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR: Pydantic
# field_validator(mode='before') requires -> Any; actual return is a
# bool or the blank undeclared sentinel.
def _parse_ley_49_2002_optional_bool( # ANY-RETURN-RATIONALE-PROFILE-PYDANTIC-VALIDATOR
cls,
value: object,
) -> Any:
return _parse_optional_bool_token(value, field_name="ley_49_2002_optional_bool")
@field_validator("activity_start_date")
@classmethod
def _validate_activity_start_date(cls, value: str) -> str:
from datetime import date
if value == "":
return value
try:
date.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"activity_start_date must be an ISO-8601 date (YYYY-MM-DD), got {value!r}") from exc
return value
@field_validator("ley_49_2002_option_date", "ley_49_2002_renunciation_date")
@classmethod
def _validate_ley_49_2002_dates(cls, value: str) -> str:
from datetime import date
if value == "":
return value
try:
date.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"Ley 49/2002 dates must be ISO-8601 (YYYY-MM-DD), got {value!r}") from exc
return value
@field_validator("irpf_special_regime_start_date")
@classmethod
def _validate_irpf_special_regime_start_date(cls, value: str) -> str:
from datetime import date
if value == "":
return value
try:
date.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"special_regime_start_date must be an ISO-8601 date (YYYY-MM-DD), got {value!r}") from exc
return value
@model_validator(mode="after")
def _validate_spouse_fields_when_joint(self) -> SetupAnswers:
renta_declaracion_type_cls = _p().RentaDeclaracionType
if self.taxation_type == renta_declaracion_type_cls.JOINT and not self.spouse_tax_id:
raise ValueError("spouse_tax_id is required when taxation_type is joint (taxation_type='2')")
return self
@model_validator(mode="after")
def _validate_eu_eea_country_when_resident(self) -> SetupAnswers:
if self.spouse_eu_eea_resident and not self.spouse_eu_eea_country:
raise ValueError("spouse_eu_eea_country is required when spouse_eu_eea_resident is true")
return self
__all__ = [
"ProfileAnswerTypeError",
"ProjectAnswersFn",
"ProjectAnswersNotRegisteredError",
"SetupAnswers",
"get_project_answers",
"project_answers",
"register_project_answers",
]