"""DT 12ª LIRPF plan-de-pensiones capital-rescate reducción.
The 40% reducción on the part of a plan-de-pensiones capital rescate attributable
to contributions made on or before 31-12-2006 (LIRPF Disposición Transitoria 12ª).
:func:`compute_dt12_reduccion_plan_pensiones` accepts :class:`~decimal.Decimal`
amounts, applies :data:`~aeat.core.external_constants.DT12_RESCATE_REDUCCION_RATE`,
rounds with :func:`aeat.core.money.round_to_cents`, and raises
:class:`PensionReduccionError` for invalid preconditions.
DT 12ª apartado 4 (added by Ley 26/2014, ``BOE-A-2014-12327``) restricts the whole
transitional régimen — and therefore the 40% reducción — to prestaciones percibidas
within a time window measured from the contingencia year.
:func:`dt12_regime_window_eligibility` is the pure predicate implementing the three
verbatim apartado-4 branches; it is consumed by the calculate-shortcut path to
fact-gate the injection (withhold the reducción when the declared years prove the
window closed), never by forking the core compute.
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from enum import StrEnum
from ...core.external_constants import DT12_RESCATE_REDUCCION_RATE
from ...core.money import round_to_cents
from ._errors import PensionReduccionError
# DT 12ª apartado 4 boundary years (LIRPF, added by Ley 26/2014 art. 1.86).
_DT12_TRANSITIONAL_CONTINGENCIA_FIRST_YEAR = 2011
_DT12_TRANSITIONAL_CONTINGENCIA_LAST_YEAR = 2014
_DT12_GENERAL_WINDOW_FOLLOWING_YEARS = 2 # "o en los dos ejercicios siguientes"
_DT12_TRANSITIONAL_WINDOW_FOLLOWING_YEARS = 8 # "hasta la finalización del octavo ejercicio siguiente"
_DT12_CLIFF_LAST_YEAR = 2018 # "hasta el 31 de diciembre de 2018"
[docs]
def compute_dt12_reduccion_plan_pensiones(
*,
gross_rescate: Decimal,
aportaciones_pre_2007: Decimal,
aportaciones_totales: Decimal,
) -> Decimal:
"""Compute the DT 12ª LIRPF 40% reducción for a plan-de-pensiones capital rescate.
Formula (LIRPF DT 12ª): ``pre_2007 / totales * gross_rescate * 40%``. The result
is rounded to 2 decimal places (money-2 per the registry convention).
Raises:
PensionReduccionError: When ``aportaciones_totales`` is zero or negative
(division-by-zero guard), when any input is negative, or when
``aportaciones_pre_2007`` exceeds ``aportaciones_totales`` (DT 12ª
apartado 2 scopes the reduced part to contributions made up to
31-12-2006, a subset of the total, so its share cannot exceed 1).
"""
if aportaciones_totales <= Decimal(0):
raise PensionReduccionError(
f"aportaciones_totales must be positive; got {aportaciones_totales}",
context={"field": "aportaciones_totales", "value": str(aportaciones_totales)},
)
if gross_rescate < Decimal(0):
raise PensionReduccionError(
f"gross_rescate must be non-negative; got {gross_rescate}",
context={"field": "gross_rescate", "value": str(gross_rescate)},
)
if aportaciones_pre_2007 < Decimal(0):
raise PensionReduccionError(
f"aportaciones_pre_2007 must be non-negative; got {aportaciones_pre_2007}",
context={"field": "aportaciones_pre_2007", "value": str(aportaciones_pre_2007)},
)
if aportaciones_pre_2007 > aportaciones_totales:
raise PensionReduccionError(
"aportaciones_pre_2007 must not exceed aportaciones_totales "
f"(DT 12ª apartado 2: the pre-2007 part is a subset of total contributions); "
f"got pre_2007={aportaciones_pre_2007}, totales={aportaciones_totales}",
context={
"field": "aportaciones_pre_2007",
"value": str(aportaciones_pre_2007),
"aportaciones_totales": str(aportaciones_totales),
},
)
reduccion = (aportaciones_pre_2007 / aportaciones_totales) * gross_rescate * DT12_RESCATE_REDUCCION_RATE
return round_to_cents(reduccion)
[docs]
class Dt12WindowBranch(StrEnum):
"""Which LIRPF DT 12ª apartado-4 branch governs a rescate's eligibility window.
Attributes:
GENERAL: Contingencia in 2015 or later — the general rule: eligible in
the ejercicio the contingencia occurs plus the two following.
TRANSITIONAL_2011_2014: Contingencia in 2011–2014 — eligible through the
end of the eighth following ejercicio.
CLIFF_2010_OR_EARLIER: Contingencia in 2010 or earlier — eligible only
through the 31-12-2018 cliff.
"""
GENERAL = "general"
TRANSITIONAL_2011_2014 = "transitional_2011_2014"
CLIFF_2010_OR_EARLIER = "cliff_2010_or_earlier"
[docs]
@dataclass(frozen=True, slots=True)
class Dt12WindowEligibility:
"""Typed verdict of the LIRPF DT 12ª apartado-4 time-window predicate.
Attributes:
contingencia_year: The year the contingencia (retirement, disability,
death) occurred, as declared by the operator.
rescate_year: The year the prestación is percibida (normally the filing
year).
branch: The :class:`Dt12WindowBranch` the contingencia year selects.
eligible: Whether the rescate falls inside the apartado-4 window and may
therefore apply the 40% reducción.
eligible_through_year: The last ejercicio in which a rescate for this
contingencia year may still apply the régimen.
"""
contingencia_year: int
rescate_year: int
branch: Dt12WindowBranch
eligible: bool
eligible_through_year: int
[docs]
def dt12_regime_window_eligibility(
*,
contingencia_year: int,
rescate_year: int,
) -> Dt12WindowEligibility:
"""Evaluate the LIRPF DT 12ª apartado-4 time-window eligibility (pure predicate).
Apartado 4 (added by Ley 26/2014 art. 1.86, ``BOE-A-2014-12327``) restricts
the transitional régimen — and therefore the 40% reducción — to prestaciones
percibidas within a window measured from the contingencia year:
- Contingencia in 2015 or later (general rule): eligible in the ejercicio the
contingencia occurs "o en los dos ejercicios siguientes", i.e.
``contingencia_year <= rescate_year <= contingencia_year + 2``.
- Contingencia in 2011–2014: eligible "hasta la finalización del octavo
ejercicio siguiente", i.e.
``contingencia_year <= rescate_year <= contingencia_year + 8``.
- Contingencia in 2010 or earlier: eligible "hasta el 31 de diciembre de
2018", i.e. ``contingencia_year <= rescate_year <= 2018``.
A rescate percibida before the contingencia year (``rescate_year <
contingencia_year``) is never eligible: the window opens with the
contingency.
Args:
contingencia_year: The year the contingencia occurred.
rescate_year: The year the prestación is percibida.
Returns:
A :class:`Dt12WindowEligibility` verdict carrying the governing branch,
the eligibility flag, and the last eligible ejercicio.
Raises:
PensionReduccionError: When either year is not a positive four-digit-era
year (a defensive guard against transposed or unset inputs).
"""
for field_name, value in (("contingencia_year", contingencia_year), ("rescate_year", rescate_year)):
if value < 1900 or value > 2200:
raise PensionReduccionError(
f"{field_name} must be a plausible calendar year (1900–2200); got {value}",
context={"field": field_name, "value": str(value)},
)
if contingencia_year <= 2010:
branch = Dt12WindowBranch.CLIFF_2010_OR_EARLIER
eligible_through_year = _DT12_CLIFF_LAST_YEAR
elif _DT12_TRANSITIONAL_CONTINGENCIA_FIRST_YEAR <= contingencia_year <= _DT12_TRANSITIONAL_CONTINGENCIA_LAST_YEAR:
branch = Dt12WindowBranch.TRANSITIONAL_2011_2014
eligible_through_year = contingencia_year + _DT12_TRANSITIONAL_WINDOW_FOLLOWING_YEARS
else:
branch = Dt12WindowBranch.GENERAL
eligible_through_year = contingencia_year + _DT12_GENERAL_WINDOW_FOLLOWING_YEARS
eligible = contingencia_year <= rescate_year <= eligible_through_year
return Dt12WindowEligibility(
contingencia_year=contingencia_year,
rescate_year=rescate_year,
branch=branch,
eligible=eligible,
eligible_through_year=eligible_through_year,
)