aeat.domain.iva._saturation module

Grounded saturation primitives for the aeat.domain.iva subpackage.

Two reusable primitives that let a caller turn a selected IvaCategory and a transaction gross into the regulated euro substrate (rate, taxable base, IVA amount) without ever guessing a number:

  • resolve_category_rate() maps an IvaCategory to its IvaRateKind and looks the applicable rate up via aeat.domain.iva.lookup_rate(), returning it as a decimal fraction (Decimal("0.21")) wrapped in a typed IvaRateResolution. Domestic general / reduced / super-reduced derive a positive rate; domestic zero and exempt derive 0; every category with no simple derivable positive domestic rate (intra-community, export, reverse-charge, recargo, import, régimen simplificado, no-sujeta, erroneous, unknown) returns a derivable=False resolution carrying an explicit operator-facing reason — never a fabricated rate.

  • split_gross_at_rate() performs the inverse split of a gross at a rate fraction into (taxable_base, iva_amount) quantised with the AEAT-mandated aeat.core.money.round_to_cents() (ROUND_HALF_UP).

The split formula is the canonical inverse of an IVA-inclusive gross: base = round_to_cents(gross / (1 + rate)) and iva = round_to_cents(gross - base). Quantising the base first and deriving the IVA as the remainder guarantees base + iva == gross to the cent regardless of the rounding residual, which is exactly the invariant the aeat.domain.transactions.Transaction model enforces.

Authority: 2026-06-04-llm-ledger-classification-adr. The rate values are grounded in registry/aeat/iva/rates.toml (Spain general 21 / reduced 10 / super-reduced 4 / zero 0; LIVA art. 90/91, year-scoped).

class IvaRateResolution(**data)[source]

Bases: BaseModel

Typed outcome of resolving an IvaCategory to a rate fraction.

A resolution is self-documenting: it never silently substitutes a fabricated rate for a category the system cannot ground. When derivable is True the rate carries the applicable rate as a decimal fraction in [0, 1] (e.g. Decimal("0.21"); Decimal("0") for zero-rated and exempt categories). When derivable is False the rate is None and reason states why the operator must complete the field.

Variables:
  • category – The IvaCategory that was resolved.

  • derivableTrue when a Spanish domestic rate fraction was derived, False when the category has no simple derivable domestic rate and the operator must complete it.

  • rate – The applicable IVA rate as a decimal fraction in [0, 1] when derivable, else None.

  • rate_kind – The IvaRateKind the category maps to when derivable, else None.

  • reason – An operator-facing explanation present only when the rate is not derivable; the empty string otherwise.

Parameters:
category: IvaCategory
derivable: bool
rate: Decimal | None
rate_kind: IvaRateKind | None
reason: str
resolve_category_rate(category, *, on_date)[source]

Resolve an IvaCategory to its Spanish IVA rate fraction.

Maps category to its IvaRateKind and looks the applicable rate up via aeat.domain.iva.lookup_rate() for EUMemberState.ES on on_date, returning the percentage as a decimal fraction (IvaRateRecord.pct / 100). Domestic general / reduced / super-reduced derive a positive fraction; domestic zero and exempt derive Decimal("0"). Every category with no simple derivable positive domestic rate returns a derivable=False resolution carrying an operator-facing reason — the system never guesses a rate for those.

Parameters:
  • category (IvaCategory) – The selected IVA category to resolve.

  • on_date (date) – The effective date used to resolve the registry rate.

Return type:

IvaRateResolution

Returns:

A typed IvaRateResolution. derivable is True with a rate fraction for domestic categories; False with a reason for every non-derivable category.

Raises:

IvaRateNotFoundError – If a domestic category maps to a rate kind that the registry has no record for on on_date (a registry gap, not a category-shape problem).

split_gross_at_rate(gross, rate)[source]

Inverse-split an IVA-inclusive gross into (taxable_base, iva_amount).

Computes the IVA-exclusive base and the IVA charged from an IVA-inclusive gross at rate (a decimal fraction, e.g. Decimal("0.21")), quantising with the AEAT-mandated aeat.core.money.round_to_cents() (ROUND_HALF_UP). The base is quantised first and the IVA is taken as the quantised remainder (gross - base), so taxable_base + iva_amount == gross holds to the cent regardless of the rounding residual.

A rate of 0 (zero-rated or exempt) yields the whole gross as the base and a zero IVA amount.

Parameters:
  • gross (Decimal) – The IVA-inclusive gross amount. Expected non-negative (the caller passes abs(amount) for a signed transaction); a signed value is split as given.

  • rate (Decimal) – The IVA rate as a decimal fraction in [0, 1].

Return type:

tuple[Decimal, Decimal]

Returns:

A (taxable_base, iva_amount) tuple, each quantised to euro cents, whose sum equals round_to_cents(gross) to the cent.