Four reference offers with their full amortisation schedules, 960 instalment
lines in total, emitted by scripts/loan_schedule.py --rust. The generator was
verified against a real 2020 Caisse d'Epargne amortisation table and
cross-checked against the ANIL simulator, so the expected values come from
outside this crate rather than from its own output.
Do not hand-edit loan_offer_fixture.rs; regenerate it.
The module is #[cfg(test)] so the fixtures never reach the wasm build. Nothing
consumes them yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Needed by the input-bound tests in the next commit, which construct an amount
one cent above the maximum.
Deliberately untested and deliberately not checked: this addition wraps in
release builds. That is acceptable while the only caller is a test, but it has
to be settled before the amortisation schedule starts summing instalments,
since a wrapped total is a wrong number that looks plausible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Converting a Decimal amount into integer cents can fail two ways: the value
carries sub-cent precision, or it does not fit in an i64 once scaled. Both are
reported through EurosError rather than silently truncating, since a money type
that rounds without being asked is the wrong kind of convenient.
The scale check demands exactly 2 rather than at most 2, so callers have to be
explicit about the precision they are handing over.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The field was called annual_rate_in_percent but nothing multiplied it by 100,
so a 3,41 % loan would have been stored as 3.41 and read as 341 %. Rename it
to annual_nominal_rate, which is what it holds, and add an explicit
annual_nominal_rate_in_percent() accessor for callers that want the display
form. Drop the trailing % from the error message, which was making the same
claim.
The nominal qualifier is there because TAEG will join this struct later and
the two must never be confused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The constructor takes a raw i64 count of cents. Naming the unit at the call
site keeps it distinct from the Decimal-based constructor added next, where
the argument is an amount in euros rather than a cent count.
Also fixes a typo in a test name.
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>
Captures how we work a feature: I guide and review, Marcel writes the code
in crates/, and commits stay atomic. Encodes the domain-grounding rule
(find a real source, reproduce it to the cent, state what it does not
settle), the review checklist, the four-command verification gate
including the wasm target, and the handling of personal source documents
via .git/info/exclude.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LoanOffer::new is the only way in: fields are private and the constructor
rejects a negative rate, a zero duration, and a non-positive capital. A zero
rate is accepted on purpose — the PTZ (prêt à taux zéro) is a real French
instrument.
Add thiserror for the typed error, as required for immo-core. Rate and
duration take no upper bound for now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renders as "123.45 €" — dot separator and a plain space, i.e. American
style. French formatting (narrow no-break space, comma separator) will need
a separate path since Display takes no parameters; deferred until there is
a page to render.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Store amounts as i64 cents rather than a Decimal, so the "scale is 2"
invariant is structural instead of maintained by hand. Both accessors are
total: as_cents is a field read, as_decimal derives the Decimal on demand.
i64 rather than u64 because differences between amounts can be negative.
Add rust_decimal, verified to build for wasm32-unknown-unknown with default
features. Allow clippy::inconsistent_digit_grouping in immo-core so cent
literals can be grouped as euros-then-cents.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two-crate workspace: immo-core for pure financial logic (wasm-compatible,
no async/IO deps) and immo-web for HTTP and rendering. Both crates are
still cargo-new skeletons; no domain code yet.
Pins the toolchain to 1.97 with rustfmt, clippy and the
wasm32-unknown-unknown target so the core crate's wasm constraint is
checkable locally. Cargo.lock is committed since the workspace ships a
binary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>