The fixture struct published borrowed_capital and total_duration_in_months, but
monthly_repayment is not a function of those two. It is a function of the
capital the amortisation actually runs on and the number of amortising
instalments, and both differ from the published fields as soon as there is a
deferral.
On offre B the six months of total deferral capitalise 439 000 EUR into
444 405,27 EUR, amortised over 294 instalments rather than 300. Feeding the
formula with the fields the fixture exposed gave 1 958,39 EUR against the
table's 2 011,86 EUR. The two deferred fixtures were therefore unusable, and
misleading about it: a 53 EUR monthly gap reads like a rounding bug in the
implementation rather than a missing input.
The generator already computed both values and wrote them to the JSON output.
Only render_rust dropped them, so the Rust fixtures were strictly less
informative than the JSON they were generated alongside.
All four fixtures now reproduce their reference instalment to the cent when fed
amortised_capital and amortisation_in_months, including the two deferred offers
that could not be tested at all before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A bare Decimal rate is ambiguous between the fraction 0,0341 and the percentage
3,41, and nothing in the type system separated them. from_fraction and
from_percentage force the call site to say which one it means.
Migrating the call sites showed the ambiguity was already there: several tests
passed Decimal::new(45, 2), which the old code consumed as the fraction 0,45,
that is 45 %, while it reads naturally as 0,45 %. Those tests exercise capital
and duration rather than the rate, so their outcome is unchanged, but the
reading was a coin flip before and is now explicit.
The type deliberately validates nothing. Comparing buying against investing the
same cash needs rates for expected return and inflation, where a negative value
is meaningful and a 99 % ceiling is nonsense. Range stays a LoanOffer rule,
which is where the domain knows what a plausible loan rate is.
LoanOffer::annual_nominal_rate_in_percent is replaced by InterestRate::
as_percentage, so the conversion stays in immo-core instead of immo-web having
to multiply by 100 itself. It normalises: dividing by 100 and multiplying back
does not restore the original scale, so 3,41 % round-tripped through Display as
"3.4100 %".
This fixes the fraction versus percentage axis only. InterestRate carries no
period, so proportional_monthly_interest_rate still returns a bare Decimal and
naming alone separates annual from monthly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Computes the mensualité, échéancier, coût total and TAEG of a French
fixed-instalment loan in Decimal arithmetic, and emits the four anonymised
sample offers as either JSON or a Rust fixtures module.
Conventions (taux mensuel proportionnel, arrondi ROUND_HALF_UP au centime,
dernière échéance ajustée pour solder le CRD) are verified against a real
amortisation table via the `verify` subcommand, and cross-checked against the
ANIL simulator on 2026-08-03.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>