feat: currency-aware minor units (proper ISO 4217 scaling) #68

Open
opened 2026-07-06 10:27:58 +01:00 by TimCane · 0 comments
TimCane commented 2026-07-06 10:27:58 +01:00 (Migrated from github.com)

Problem

The system treats a minor unit as hundredths of the major unit everywhere, regardless of currency. ReceiptParser.ToMinor multiplies whole units by 100 unconditionally, and Bill, SplitCalculator, and MaxAmountMinor all follow that convention. This is wrong for currencies whose real minor unit is not 1/100:

  • Zero-decimal (JPY, KRW): 1000 yen should be 1000 minor, not 100000.
  • Three-decimal (BHD, KWD): 1.250 should be 1250 minor, and the parser's \d{2} fraction regex can't even read them.

The frontend money layer was matched to the backend's fixed-100 convention as a stopgap (PR #67) so display and entry are at least self-consistent. That is the right short-term call, but it means a JPY split shows ¥1,000.00 semantics internally and the whole stack is quietly assuming 2-decimal money.

Scope

Make minor-unit scaling currency-aware end to end:

  • ReceiptParser.ToMinor + the money regex handle 0/2/3-decimal currencies from the detected currency.
  • MaxAmountMinor / validation bounds reason in the currency's own scale.
  • Confirm SplitCalculator largest-remainder rounding is correct at each currency's minor unit.
  • Frontend money.ts / moneyInput.ts scale by the currency's ISO fraction digits again (revert the fixed-100 stopgap from #67).
  • Decide the source of truth for per-currency digits (server-provided vs Intl) so display and entry can't drift.
  • Docs: docs/02-domain-model.md#money should state the per-currency minor-unit rule.

Non-goals

No currency conversion / FX. Each session stays in a single currency.

Context

Deferred from the #67 review. See the money-scale fix commit for the stopgap.

## Problem The system treats a minor unit as hundredths of the major unit everywhere, regardless of currency. `ReceiptParser.ToMinor` multiplies whole units by 100 unconditionally, and `Bill`, `SplitCalculator`, and `MaxAmountMinor` all follow that convention. This is wrong for currencies whose real minor unit is not 1/100: - Zero-decimal (JPY, KRW): 1000 yen should be `1000` minor, not `100000`. - Three-decimal (BHD, KWD): 1.250 should be `1250` minor, and the parser's `\d{2}` fraction regex can't even read them. The frontend money layer was matched to the backend's fixed-100 convention as a stopgap (PR #67) so display and entry are at least self-consistent. That is the right short-term call, but it means a JPY split shows `¥1,000.00` semantics internally and the whole stack is quietly assuming 2-decimal money. ## Scope Make minor-unit scaling currency-aware end to end: - [ ] `ReceiptParser.ToMinor` + the money regex handle 0/2/3-decimal currencies from the detected currency. - [ ] `MaxAmountMinor` / validation bounds reason in the currency's own scale. - [ ] Confirm `SplitCalculator` largest-remainder rounding is correct at each currency's minor unit. - [ ] Frontend `money.ts` / `moneyInput.ts` scale by the currency's ISO fraction digits again (revert the fixed-100 stopgap from #67). - [ ] Decide the source of truth for per-currency digits (server-provided vs `Intl`) so display and entry can't drift. - [ ] Docs: `docs/02-domain-model.md#money` should state the per-currency minor-unit rule. ## Non-goals No currency conversion / FX. Each session stays in a single currency. ## Context Deferred from the #67 review. See the money-scale fix commit for the stopgap.
Sign in to join this conversation.
No description provided.