Closed form of the constant-instalment recurrence, C*i / (1 - (1+i)^-n), with i the taux mensuel proportionnel (taux nominal / 12) rather than the taux actuariel equivalent, which is what French lenders use for the schedule. A zero rate is a real case, not a degenerate one: the PTZ is a 0 % loan and the denominator collapses to zero, which rust_decimal's Div panics on rather than erroring. The zero branch computes C/n, the limit of the formula as i tends to zero, and offre_c_ptz covers it. Both known-answer tests compare against generated fixtures whose values came from a real amortisation table, not from this implementation. The expect() is justified by the bounds from the previous commit: capital and rate cap the value well below i64::MAX cents, and rescale(2) makes the scale exact, so both EurosError arms are unreachable. CLAUDE.md is amended in the same commit to permit expect() only in this provable case, since the rule change exists for this call site. Requires rust_decimal's maths feature for powu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
4.4 KiB
Markdown
94 lines
4.4 KiB
Markdown
# CLAUDE.md
|
|
|
|
## Project
|
|
|
|
Web app that answers one question with data: for a given property in France, is buying
|
|
financially better than renting and investing the same cash? It outputs an amortisation
|
|
schedule, total ownership costs, and breakeven curves under several market hypotheses.
|
|
|
|
This is a financial tool. A wrong number is worse than a slow response, an ugly page, or a
|
|
missing feature. Correctness and traceable sources come first.
|
|
|
|
I am learning Rust through this project. See "Working with me" below.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
cargo run -p immo-web # start the server on :3000
|
|
cargo watch -x 'run -p immo-web' # live reload during development
|
|
cargo test --workspace # all tests
|
|
cargo test -p immo-core # domain tests only, fast
|
|
cargo fmt --all
|
|
cargo clippy --workspace --all-targets -- -D warnings
|
|
```
|
|
|
|
Run `cargo clippy` and `cargo test -p immo-core` before telling me a change is finished.
|
|
|
|
## Architecture boundary
|
|
|
|
Two crates. The boundary is a rule, not a description.
|
|
|
|
`crates/immo-core` holds all financial logic as pure functions over plain structs.
|
|
|
|
- No `tokio`, no `axum`, no `reqwest`, no filesystem, no network access.
|
|
- Must stay compilable to `wasm32-unknown-unknown`. After adding any dependency, verify with
|
|
`cargo check -p immo-core --target wasm32-unknown-unknown`.
|
|
- Time is a parameter. Never call `Utc::now()` or read the system clock inside `immo-core`.
|
|
|
|
`crates/immo-web` holds HTTP, HTML rendering, and input parsing. It contains no financial
|
|
arithmetic.
|
|
|
|
If a computation is tempting to write inline in a handler, it belongs in `immo-core`.
|
|
|
|
## Stack, pinned
|
|
|
|
- axum 0.8. Path parameters use `{param}`, **not** `:param` — that is 0.7 syntax and will not compile.
|
|
- `#[async_trait]` is not needed on `FromRequest` / `FromRequestParts` in 0.8. Do not add it.
|
|
- askama for templates, tower-http for static files and compression, serde, rust_decimal.
|
|
- Server-rendered HTML. No npm, no bundler, no JS framework. Charts are a CDN `<script>` tag fed
|
|
by a JSON endpoint.
|
|
|
|
Before writing axum code from memory, check the version in `Cargo.toml` and verify the API against
|
|
docs.rs for that exact version.
|
|
|
|
## Domain rules
|
|
|
|
- Money is `rust_decimal::Decimal` or integer cents. Never `f64`. Never `as` casts between numeric
|
|
types in financial code.
|
|
- Rates are stored as annual nominal and converted explicitly. Name variables so the period is
|
|
unambiguous (`taeg_annual`, `monthly_rate`), never a bare `rate`.
|
|
- Keep French domain vocabulary in type and field names: `FraisDeNotaire`, `TaxeFonciere`,
|
|
`IndiceReferenceLoyers`, `LoyerDeReferenceMajore`, `ZoneTendue`, `Taeg`. Do not translate them.
|
|
- Every hardcoded assumption lives in `crates/immo-core/src/assumptions.rs` with a `///` comment
|
|
giving its source and the date it was checked. Nothing hardcoded anywhere else.
|
|
- If you do not know a real figure — a barème, a tax rate, a fee schedule, an IRL value — say so and
|
|
leave a clearly named placeholder. Do not invent plausible-looking French tax numbers.
|
|
- TAEG and taux nominal are different things. Distinguish them explicitly wherever both appear.
|
|
|
|
## Error handling and tests
|
|
|
|
- `thiserror` for typed errors in `immo-core`; `anyhow` only at the `immo-web` boundary.
|
|
- No `unwrap()` or `panic!()` outside tests and `main`. `expect()` can only be used for cases where the Result will provably be `Ok()`
|
|
- Every `core` function producing a number the user sees has a known-answer test, with the worked
|
|
example stated in the test name or a comment above it.
|
|
- Test the amortisation schedule against hand-checked values, never against the implementation's
|
|
own output.
|
|
|
|
## Working with me
|
|
|
|
I know traits, `Result`, and the borrow checker. I do not know the ecosystem well, and learning it
|
|
is half the point of this project.
|
|
|
|
- When you use a non-obvious idiom — extractors, `Layer`, lifetimes on a struct, `impl Trait` in
|
|
return position — explain why in one or two sentences, in chat rather than as a code comment.
|
|
- If my request implies an unidiomatic design, say so before implementing it.
|
|
- Prefer the simple version over the clever one, and tell me when you deliberately chose the
|
|
simple one.
|
|
|
|
## Do not
|
|
|
|
- Add a dependency without asking first, including small ones.
|
|
- Introduce npm, a bundler, TypeScript, or a frontend framework.
|
|
- Write financial logic in `crates/immo-web`.
|
|
- Leave commented-out code, or comments that restate what the code does.
|
|
- Reformat or restructure files you were not asked to touch.
|