Source code for cadrumo.application.modelo._minimo_descendientes_advisory

"""Generic advisory seams for registry-selected family declarations.

Revision-specific descendant predicates, profile identifiers, legal
references, and bounded-detail policy belong to the selected Modelo 100
registry revision. This module retains generic diagnostic mechanics and the
unrelated childcare advisory helpers.
"""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from datetime import date
from decimal import Decimal
from typing import TYPE_CHECKING, NamedTuple

from ...core.casilla_id import CasillaId
from ...core.modelo import Modelo
from ...domain.calculations.registry.authority import bundled_indexed_authority
from ...domain.calculations.registry.errors import RegistryValidationError
from ...domain.calculations.registry.formula_runtime_ops import resolve_dated_value
from ...domain.calculations.registry.schema import BindingDefinition, ModeloRevision
from ...domain.calculations.registry.schema_base import DateAxis
from ...domain.contribuyente.descendant import DescendantInfo
from ...domain.contribuyente.descendant_facts import descendant_list_from_facts
from ...domain.contribuyente.family_fact_context import FamilyFactResolutionContext
from ...domain.user_profile.errors import ProfileNotFoundError
from ..aggregation.source_mesh import CalculationSourceDiagnostic
from .semantic_role_resolution import casilla_id_for_unambiguous_revision_semantic_role

if TYPE_CHECKING:
    from ...domain.calculations.registry.authority import PinnedAuthorityOperation
    from .work_profile import ModeloWorkProfile

__all__ = [
    "collect_descendientes_count_desync_diagnostics",
    "collect_guarderia_madre_meses_undeclared_diagnostics",
    "collect_guarderia_spend_shape_diagnostics",
    "collect_minimo_descendientes_dependencia_diagnostics",
    "collect_minimo_descendientes_entry_date_missing_diagnostics",
    "collect_minimo_descendientes_prorrata_inferred_diagnostics",
    "collect_minimo_descendientes_rentas_undeclared_diagnostics",
    "collect_minimo_descendientes_undeclared_diagnostics",
]

_GUARDERIA_SHAPE_SOURCE_KIND = "guarderia_spend_needs_monthly_detail"
_SEGUNDO_CICLO_SOURCE_KIND = "guarderia_segundo_ciclo_month_undeclared"
_COTIZACIONES_FACT_KEY = "renta_family.cotizaciones_ss_madre_2024"
_COTIZACIONES_CEILING_SOURCE_KIND = "guarderia_cotizaciones_ceiling_unbounded"
_GUARDERIA_MADRE_MESES_SOURCE_KIND = "guarderia_madre_meses_undeclared"
_MINIMO_ESTATAL_SEMANTIC_ROLE = "irpf_minimo_descendientes_estatal"
_DESCENDANT_FACT_PREFIX = "renta_family.descendiente."
_DESCENDANTS_COUNT_PATH = "renta_family.descendientes_count"
_UNDECLARED_SOURCE_KIND = "minimo_descendientes_undeclared"
_RENTAS_UNDECLARED_SOURCE_KIND = "minimo_descendientes_rentas_undeclared"

#: The Art. 81.2 guardería increase (Modelo 100 casilla 0613).
_INCREMENTO_GUARDERIA_SEMANTIC_ROLE = "irpf_incremento_maternidad_guarderia"


class _GuarderiaContext(NamedTuple):
    """The shared source facts an Art. 81.3 advisory reads."""

    casilla_id: CasillaId
    filing_year: int
    descendants: tuple[DescendantInfo, ...]
    facts: dict[str, str]
    family_context: FamilyFactResolutionContext
    registry_scope: _RegistryScope


class _RegistryScope(NamedTuple):
    """The typed registry declarations selected for one calculation scope."""

    revision: ModeloRevision
    bindings: tuple[BindingDefinition, ...]
    filing_year: int
    period_token: str


def _registry_minimo_descendientes_diagnostics(
    revision: ModeloRevision,
    *,
    modelo: str,
    filing_year: int,
    period_token: str,
) -> tuple[CalculationSourceDiagnostic, ...]:
    """Resolve the selected revision's typed verification declarations.

    The advisory predicates themselves remain generic mechanics. Their
    revision-specific profile, formula and legal/source declarations are read
    from the scope selected by the calculation, so a latest-revision lookup
    cannot accidentally author a historical filing.
    """
    scope = _selected_registry_scope(
        revision,
        modelo=modelo,
        filing_year=filing_year,
        period_token=period_token,
    )
    if scope is None:
        return ()
    _validate_binding_report(scope)
    return ()


def _registry_minimo_descendientes_count_diagnostics(
    revision: ModeloRevision,
    *,
    modelo: str,
    filing_year: int,
    period_token: str,
) -> tuple[CalculationSourceDiagnostic, ...]:
    """Resolve the selected revision before checking count consistency."""
    scope = _selected_registry_scope(
        revision,
        modelo=modelo,
        filing_year=filing_year,
        period_token=period_token,
    )
    if scope is None:
        return ()
    _validate_binding_report(scope)
    return ()


def _selected_registry_scope(
    revision: ModeloRevision,
    *,
    modelo: str,
    filing_year: int,
    period_token: str,
) -> _RegistryScope | None:
    """Select one validated registry revision using the live filing scope."""
    if modelo != Modelo("100").value:
        return None
    if type(filing_year) is not int or filing_year <= 0 or not period_token.strip():
        raise RegistryValidationError("descendant advisory registry scope is incomplete")
    return _RegistryScope(
        revision=revision,
        bindings=tuple(revision.bindings),
        filing_year=filing_year,
        period_token=period_token,
    )


def _validate_binding_report(scope: _RegistryScope) -> None:
    """Require the query report to carry every typed, grounded binding row."""
    declared_ids = {str(binding.id) for binding in scope.revision.bindings}
    reported_ids = {str(binding.id) for binding in scope.bindings}
    if declared_ids != reported_ids:
        raise RegistryValidationError(
            "selected descendant advisory binding report does not match the revision declarations",
        )
    if any(not binding.legal_refs or not binding.source_refs for binding in scope.bindings):
        raise RegistryValidationError(
            "selected descendant advisory binding report contains an ungrounded declaration",
        )


def _registry_named_descendant_limit(scope: _RegistryScope) -> int:
    """Read the message cardinality from the selected registry revision."""
    candidates = tuple(
        parameter
        for parameter in scope.revision.parameters
        if str(parameter.data_type) == "integer" and parameter.unit == "diagnostic_items"
    )
    if len(candidates) != 1:
        raise RegistryValidationError(
            "selected descendant advisory revision must declare exactly one integer diagnostic_items parameter",
        )
    selected = resolve_dated_value(
        candidates[0],
        {DateAxis.FILING_PERIOD.value: date(scope.filing_year, 12, 31)},
    )
    if selected.date_axis is not DateAxis.FILING_PERIOD:
        raise RegistryValidationError(
            "selected descendant advisory cardinality must use the filing_period axis",
        )
    value = selected.value
    if value != value.to_integral_value() or value <= 0:
        raise RegistryValidationError(
            "selected descendant advisory cardinality must be a positive integer",
        )
    return int(value)


def _family_fact_context(
    filing_year: int,
    *,
    operation: PinnedAuthorityOperation | None = None,
) -> FamilyFactResolutionContext:
    """Compose the advisory's explicit family fact coordinates."""
    if operation is None:
        with bundled_indexed_authority().operation() as indexed_operation:
            return _family_fact_context(filing_year, operation=indexed_operation)
    coordinate = date(filing_year, 12, 31)
    return FamilyFactResolutionContext(
        authority=operation,
        filing_period=coordinate,
        devengo_date=coordinate,
    )


[docs] def collect_minimo_descendientes_undeclared_diagnostics( revision: ModeloRevision, casilla_values: Mapping[CasillaId, Decimal], *, modelo: str, filing_year: int, period_token: str, bucket_id: str, operation: PinnedAuthorityOperation, profile: ModeloWorkProfile | None = None, ) -> tuple[CalculationSourceDiagnostic, ...]: """Disclose the ambiguous zero for the selected :class:`ModeloRevision`.""" scope = _selected_registry_scope( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, ) if scope is None: return () _validate_binding_report(scope) casilla_id = casilla_id_for_unambiguous_revision_semantic_role( revision, _MINIMO_ESTATAL_SEMANTIC_ROLE, modelo_id=modelo, ) if casilla_id is None or casilla_values.get(casilla_id, Decimal("0")) != 0: return () facts = _profile_fact_strings(bucket_id, operation=operation, profile=profile) if facts is None: return () if any(key.startswith(_DESCENDANT_FACT_PREFIX) or key == _DESCENDANTS_COUNT_PATH for key in facts): return () return ( CalculationSourceDiagnostic( reason="source_issue", source_kind=_UNDECLARED_SOURCE_KIND, message=( f"casilla {casilla_id!r} (mínimo por descendientes, parte estatal) resolved to zero and " "the active profile declares no descendant facts" ), remedy="Declare the family situation with `descendiente add --descendiente NACIMIENTO=YYYY-MM-DD`.", casilla_id=casilla_id, ), )
def _profile_fact_strings( bucket_id: str, *, operation: PinnedAuthorityOperation, profile: ModeloWorkProfile | None, ) -> dict[str, str] | None: """Return every non-null profile fact as a ``{path: str-value}`` map, or ``None``.""" if profile is not None: record = profile.record else: from ..user_profile.profile_record_repository import ProfileRecordRepository try: record = ProfileRecordRepository.for_current_session( bucket_id, profile_decode_context=operation.profile_decode_context(), ).load(bucket_id) except ProfileNotFoundError: return None return {fact.path: str(fact.value) for fact in record.facts if fact.value is not None}
[docs] def collect_minimo_descendientes_prorrata_inferred_diagnostics( revision: ModeloRevision, casilla_values: Mapping[CasillaId, Decimal], *, modelo: str, filing_year: int, period_token: str, bucket_id: str, ) -> tuple[CalculationSourceDiagnostic, ...]: """Delegate inferred-proration verification declarations to the registry. Core types: :class:`~cadrumo.domain.calculations.registry.schema.ModeloRevision`. """ del casilla_values, bucket_id return _registry_minimo_descendientes_diagnostics( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, )
def _name_indices(indices: list[int], scope: _RegistryScope) -> str: """Render generic descendant labels using the registry-selected cardinality.""" limit = _registry_named_descendant_limit(scope) shown = ", ".join(f"descendant[{index}]" for index in indices[:limit]) remainder = len(indices) - limit return f"{shown} and {remainder} more" if remainder > 0 else shown def _guarderia_shape_advisory( indices: list[int], casilla_id: CasillaId, scope: _RegistryScope, ) -> CalculationSourceDiagnostic: return CalculationSourceDiagnostic( reason="source_issue", source_kind=_GUARDERIA_SHAPE_SOURCE_KIND, message=( f"casilla {casilla_id!r} counts no guardería spend for {_name_indices(indices, scope)}: the " "child turns three in this period, so Art. 81.2 LIRPF admits only spend after the " "birthday and an annual total cannot be split across it" ), remedy=( "Restate it month by month with `descendiente add --descendiente " "GASTOS_GUARDERIA_MENSUAL=MM:N;MM-MM:N`. The eligible months are the ones your centre " "reported, so your certificate is the authority." ), casilla_id=casilla_id, ) def _cotizaciones_ceiling_is_unbounded( descendants: Sequence[DescendantInfo], facts: Mapping[str, str], filing_year: int, *, context: FamilyFactResolutionContext, ) -> bool: """Whether an unbounded cotizaciones ceiling can change this filing's outcome. Reads the household cotizaciones fact directly rather than through a profile object, because this collector already holds the fact strings and rebuilding a profile to ask one question would be a second reader of the same data. Silent unless a figure is actually declared: with none the ceiling binds at zero and the increment is already nil for a reason the operator can see, so the advisory would add nothing but noise. """ raw = facts.get(_COTIZACIONES_FACT_KEY, "").strip() if not raw: return False try: declared = int(raw) except ValueError: return False if declared <= 0: return False return any( descendant.convive_con_contribuyente and descendant.age_at_year_end(filing_year) == context.integer("lirpf-art-58-under-three-maximum-age") and bool(descendant.gastos_guarderia_mensuales) for descendant in descendants ) def _segundo_ciclo_month_advisory( indices: list[int], casilla_id: CasillaId, scope: _RegistryScope, ) -> CalculationSourceDiagnostic: """The turning-three window is withheld until the operator declares the month.""" return CalculationSourceDiagnostic( reason="source_issue", source_kind=_SEGUNDO_CICLO_SOURCE_KIND, message=( f"casilla {casilla_id!r} counts no guardería spend for {_name_indices(indices, scope)}: the " "child turns three in this period, so Art. 81.2 LIRPF admits spend only up to the " "month before the second cycle of educación infantil may begin, and that month is " "not declared" ), remedy=( "Declare it with `descendiente add --descendiente SEGUNDO_CICLO_INFANTIL_INICIO_MES=MM`. " "The month is the one the second cycle may start for this child, which your región " "and centre determine; your centre reports it on the modelo 233." ), casilla_id=casilla_id, ) def _cotizaciones_ceiling_advisory(casilla_id: CasillaId) -> CalculationSourceDiagnostic: """The cotizaciones limb is bounded by the same month, and is NOT applied here. Disclosed rather than computed: the declared figure is a household annual total while the ceiling is per child, and AEAT states no rule for apportioning one across several. Computing one would invent the arithmetic this advisory exists to keep out of the engine. """ return CalculationSourceDiagnostic( reason="source_issue", source_kind=_COTIZACIONES_CEILING_SOURCE_KIND, message=( f"casilla {casilla_id!r} caps the guardería increment with the declared cotizaciones " "total as supplied. For a child turning three, Art. 81 counts only the cotizaciones " "devengadas up to the month before the second cycle may begin, and this application " "does not apply that bound: the figure is a household annual total while the ceiling " "is per child" ), remedy=( "Supply the already-bounded figure — the cotizaciones devengadas up to the month " "before the second cycle may begin — rather than the full annual total, if the two " "differ for this filing." ), casilla_id=casilla_id, ) def _guarderia_madre_meses_advisory( indices: list[int], casilla_id: CasillaId, scope: _RegistryScope, ) -> CalculationSourceDiagnostic: return CalculationSourceDiagnostic( reason="source_issue", source_kind=_GUARDERIA_MADRE_MESES_SOURCE_KIND, message=( f"casilla {casilla_id!r} is zero despite declared guardería spend for " f"{_name_indices(indices, scope)}: Art. 81.2 LIRPF raises the maternidad deducción, so it " "needs the months the mother met the Art. 81.1 requirement, and none are on record" ), remedy=( "Declare them with `descendiente add --descendiente MESES_TRABAJO=MM-MM`, or leave " "the zero if she met it in no month of this period." ), casilla_id=casilla_id, )
[docs] def collect_minimo_descendientes_rentas_undeclared_diagnostics( revision: ModeloRevision, casilla_values: Mapping[CasillaId, Decimal], *, modelo: str, filing_year: int, period_token: str, bucket_id: str, operation: PinnedAuthorityOperation, profile: ModeloWorkProfile | None = None, ) -> tuple[CalculationSourceDiagnostic, ...]: """Disclose an absent rentas figure for the selected :class:`ModeloRevision`.""" scope = _selected_registry_scope( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, ) if scope is None: return () _validate_binding_report(scope) casilla_id = casilla_id_for_unambiguous_revision_semantic_role( revision, _MINIMO_ESTATAL_SEMANTIC_ROLE, modelo_id=modelo, ) if casilla_id is None or casilla_values.get(casilla_id, Decimal("0")) == 0: return () facts = _profile_fact_strings(bucket_id, operation=operation, profile=profile) if facts is None: return () context = _family_fact_context(filing_year, operation=operation) undeclared = [ index for index, descendant in enumerate(descendant_list_from_facts(facts)) if descendant.rentas_anuales_euros is None and descendant.meets_non_income_conditions(filing_year, context=context) ] if not undeclared: return () return ( CalculationSourceDiagnostic( reason="source_issue", source_kind=_RENTAS_UNDECLARED_SOURCE_KIND, message=( f"casilla {casilla_id!r} claims a mínimo por descendientes for " f"{_name_indices(undeclared, scope)} with no annual-rentas figure on record" ), remedy="Declare the figure with `descendiente add --descendiente RENTAS=N`; RENTAS=0 is explicit.", casilla_id=casilla_id, ), )
[docs] def collect_minimo_descendientes_entry_date_missing_diagnostics( revision: ModeloRevision, casilla_values: Mapping[CasillaId, Decimal], *, modelo: str, filing_year: int, period_token: str, bucket_id: str, ) -> tuple[CalculationSourceDiagnostic, ...]: """Delegate entry-date verification declarations to the registry. Core types: :class:`~cadrumo.domain.calculations.registry.schema.ModeloRevision`. """ del casilla_values, bucket_id return _registry_minimo_descendientes_diagnostics( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, )
[docs] def collect_guarderia_spend_shape_diagnostics( revision: ModeloRevision, casilla_values: Mapping[CasillaId, Decimal], *, modelo: str, filing_year: int, period_token: str, bucket_id: str, operation: PinnedAuthorityOperation, profile: ModeloWorkProfile | None = None, ) -> tuple[CalculationSourceDiagnostic, ...]: """Advise when a declared guardería figure contributes nothing because of its SHAPE. Art. 81.2 extends the increase into the period the child turns three, but only for spend "incurridos con posterioridad al cumplimiento de dicha edad". An ANNUAL total spans that birthday and cannot be apportioned across it, so a child in that period whose spend is on record only as an annual figure contributes zero. That is the one state where a taxpayer declared real spend, sees it stored, and receives nothing — and nothing about the computed value says why. It is not a withheld window: the operator can replace the annual figure with the month-by-month detail their childcare centre already certified, and the months on that certificate are exactly the eligible ones, because the centre determines them and reports them to the authority on its own informative return. So the advisory points at the document that settles the question rather than at a rule this application cannot state (`no-silent-under-declaration`). Args: revision: The :class:`ModeloRevision` being calculated. Its ``valid_to`` supplies the devengo year the turning-three test is anchored to. casilla_values: The computed engine values keyed by :class:`CasillaId`. modelo: The modelo identifier of the filing being calculated. filing_year: Filing year used to select the descendant fact coordinate. period_token: Registry period token used to confirm the selected scope. bucket_id: Bucket whose profile carries the descendant facts. operation: Authority operation supplying the profile and registry reads. profile: The bucket's profile when the caller already loaded it; when omitted, the facts are read from the bucket. Returns: A one-element tuple carrying the advisory, or an empty tuple. """ context = _guarderia_descendants( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, bucket_id=bucket_id, operation=operation, profile=profile, ) if context is None: return () return _guarderia_spend_shape_diagnostics(context)
def _guarderia_descendants( revision: ModeloRevision, *, modelo: str, filing_year: int, period_token: str, bucket_id: str, operation: PinnedAuthorityOperation, profile: ModeloWorkProfile | None, ) -> _GuarderiaContext | None: """Resolve the shared preconditions the two Art. 81.3 collectors below need. Both ask about the same population against the same casilla and the same devengo year, and both are silent for the same three reasons: a different modelo, a revision that fixes no devengo date, or an unreadable profile. Assembled once so the two cannot drift into disagreeing about when they apply. """ if modelo != Modelo("100").value: return None casilla_id = casilla_id_for_unambiguous_revision_semantic_role( revision, _INCREMENTO_GUARDERIA_SEMANTIC_ROLE, modelo_id=modelo, ) if casilla_id is None: return None if revision.valid_to is None: return None registry_scope = _selected_registry_scope( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, ) if registry_scope is None: return None facts = _profile_fact_strings(bucket_id, operation=operation, profile=profile) if facts is None: return None descendant_facts = facts return _GuarderiaContext( casilla_id=casilla_id, filing_year=registry_scope.filing_year, descendants=tuple(descendant_list_from_facts(descendant_facts)), facts=facts, family_context=_family_fact_context(registry_scope.filing_year, operation=operation), registry_scope=registry_scope, ) def _guarderia_spend_shape_diagnostics( context: _GuarderiaContext, ) -> tuple[CalculationSourceDiagnostic, ...]: """Classify every declared guardería shape issue in stable diagnostic order.""" affected = [ index for index, descendant in enumerate(context.descendants) if descendant.guarderia_needs_monthly_detail(context.filing_year, context=context.family_context) ] needs_month = [ index for index, descendant in enumerate(context.descendants) if descendant.guarderia_needs_segundo_ciclo_month(context.filing_year, context=context.family_context) ] diagnostics: list[CalculationSourceDiagnostic] = [] if affected: diagnostics.append(_guarderia_shape_advisory(affected, context.casilla_id, context.registry_scope)) if needs_month: diagnostics.append(_segundo_ciclo_month_advisory(needs_month, context.casilla_id, context.registry_scope)) if _cotizaciones_ceiling_is_unbounded( context.descendants, context.facts, context.filing_year, context=context.family_context, ): diagnostics.append(_cotizaciones_ceiling_advisory(context.casilla_id)) return tuple(diagnostics)
[docs] def collect_guarderia_madre_meses_undeclared_diagnostics( revision: ModeloRevision, casilla_values: Mapping[CasillaId, Decimal], *, modelo: str, filing_year: int, period_token: str, bucket_id: str, operation: PinnedAuthorityOperation, profile: ModeloWorkProfile | None = None, ) -> tuple[CalculationSourceDiagnostic, ...]: """Advise when declared guardería spend yields nothing for want of the mother's months. Art. 81.2 increases the maternidad deducción, so it is available only where the Art. 81.1 requirement is met, and Art. 81.3 prorates it by the months both hold at once. A filer who never recorded the mother's qualifying months carries a zero on that side, and zero months of overlap is zero increase. The arithmetic is right and the outcome is still a trap, because the record cannot tell a mother who declared no qualifying months from one who was never asked: the field simply defaults to zero. So a taxpayer can declare real nursery spend, watch it stored and listed back, and receive nothing, with no computed value able to say which of the two happened (`no-silent-under-declaration`). Fires only where spend is actually on record for a child the article admits. A filer with no guardería spend at all is not in this state, and telling them about the mother's months would be noise. Args: revision: The :class:`ModeloRevision` being calculated; its ``valid_to`` supplies the devengo year. casilla_values: Computed engine values keyed by :class:`CasillaId`. The advisory is scoped to the zero, since a positive increase means the months were declared. modelo: The modelo identifier of the filing being calculated. filing_year: Filing year used to select the descendant fact coordinate. period_token: Registry period token used to confirm the selected scope. bucket_id: Bucket whose profile carries the descendant facts. operation: Authority operation supplying the profile and registry reads. profile: The bucket's profile when the caller already loaded it; when omitted, the facts are read from the bucket. Returns: A one-element tuple carrying the advisory, or an empty tuple. """ context = _guarderia_descendants( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, bucket_id=bucket_id, operation=operation, profile=profile, ) if context is None: return () if casilla_values.get(context.casilla_id, Decimal("0")) != 0: return () affected = [ index for index, descendant in enumerate(context.descendants) if not descendant.meses_madre_trabajo and descendant.guarderia_qualifying_meses(context.filing_year, context=context.family_context) > 0 and descendant.guarderia_contributing_spend(context.filing_year, context=context.family_context) > 0 ] if not affected: return () return (_guarderia_madre_meses_advisory(affected, context.casilla_id, context.registry_scope),)
[docs] def collect_minimo_descendientes_dependencia_diagnostics( revision: ModeloRevision, casilla_values: Mapping[CasillaId, Decimal], *, modelo: str, filing_year: int, period_token: str, bucket_id: str, ) -> tuple[CalculationSourceDiagnostic, ...]: """Delegate dependency verification declarations to the registry. Core types: :class:`~cadrumo.domain.calculations.registry.schema.ModeloRevision`. """ del casilla_values, bucket_id return _registry_minimo_descendientes_diagnostics( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, )
[docs] def collect_descendientes_count_desync_diagnostics( revision: ModeloRevision, *, modelo: str, filing_year: int, period_token: str, bucket_id: str, ) -> tuple[CalculationSourceDiagnostic, ...]: """Delegate descendant-count verification declarations to the registry. Core types: :class:`~cadrumo.domain.calculations.registry.schema.ModeloRevision`. """ del bucket_id return _registry_minimo_descendientes_count_diagnostics( revision, modelo=modelo, filing_year=filing_year, period_token=period_token, )