Skip to content

docs(currencies): spec for arbitrary-precision money and fx - #144

Closed
patrykbojczuk wants to merge 7 commits into
developfrom
pb/arbitrary-precision-money-spec
Closed

patrykbojczuk wants to merge 7 commits into
developfrom
pb/arbitrary-precision-money-spec

Conversation

@patrykbojczuk

@patrykbojczuk patrykbojczuk commented Oct 8, 2026 •

Copy link
Copy Markdown

Summary

Spec only: .ai/specs/2026-10-08-arbitrary-precision-money-and-fx.md. No code changes.

  • Unconstrained numeric for exchange rates (exchange_rates.rate, sales_orders.exchange_rate) and about 100 money columns.
  • Exact decimal math (big.js via @open-mercato/shared/lib/decimal). Only division is cut: amounts at the currency precision, FX at 50 decimals, and never fewer than 17 significant digits.
  • Free-form jsonb metadata on exchange rates.
  • Backward compatible: existing number fields stay, with new <field>Exact string siblings.

The implementation lives on pb/exchange-rate-metadata.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@Frshy Frshy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds a design spec with no code changes. It plans how Open Mercato would store and calculate money and exchange rates exactly, with no fixed decimal limit.

🎯 Money and FX rates lose digits

Today money columns have a fixed number of decimals (2 or 4), and exchange rates have 8. Calculations run on JavaScript floats with hard-coded rounding. Crypto currencies such as BTC (8 decimals) and ETH (18) cannot be stored correctly. Very small or inverted exchange rates get cut down to a few significant digits. Exchange rates also have nowhere to keep extra context, such as a provider's table number.

🔧 Unlimited columns, exact decimal math

The spec makes about 100 money columns and both exchange-rate columns unlimited Postgres numeric. All money math moves to exact decimal arithmetic using big.js, wrapped in a new @open-mercato/shared/lib/decimal toolkit. Only division gets rounded: amounts to max(4, currency decimals), FX to 50 decimals. Exchange rates gain a free-form metadata jsonb column.

🔍 Details

  • Compatibility: existing number fields in TS types and API responses stay. Each gets a <field>Exact: string sibling.
  • Validators: they keep number output. Commands read the exact digits from the raw request through withExactAmounts.
  • Old calculation hooks: a "hook bridge" spots hooks that only change the number field and rebuilds the exact value from it.
  • Precision lookup: a new currencyPrecisionService supplies each currency's decimal places. The decimalPlaces cap rises from 8 to 50.
  • Staff exception: staff amounts round to the currency's own decimals (2 for fiat), matching today's round2 in staff/lib/time-tracking/cost.ts.

⚠️ Worth knowing

  • The implementation is on a separate branch, pb/exchange-rate-metadata. This PR only lands the document.
  • Rows written after the change store values unpadded (40 instead of 40.0000), so string comparisons may need updating.
  • Negative half values will round away from zero, which differs from Math.round.

Walkthrough

Arbitrary-precision money and FX spec

A single new spec covering the column changes, the decimal toolkit, the compatibility plan and the risks. Check the sections against each other: the Decisions table and Changelog were updated after implementation, but the TLDR and the Migration & Backward Compatibility table still describe the earlier plan.


Reviewed with cerber 🐕 — drafted by AI, sent by a human.

| DB schema | `numeric(p,s)` → `numeric` on money/FX columns; new nullable `exchange_rates.metadata` | ✓ ADDITIVE (widening) |
| TS types (`sales/lib/types.ts`, tax service, providers, `shared/modules/payment_gateways/types.ts`, staff `TimeRateResolver`) | `<field>Exact: string` added; `number` fields `@deprecated`; resolvers may return `number | string` | ✓ ADDITIVE + deprecation |
| Calculation hooks | Legacy number-only hooks keep working through the hook bridge | ✓ Behavior-preserving |
| Zod validators | Input widened to `number | string`; parsed output for amount fields changes from `number` to `string` | ⚠ Type change of `z.infer` output - documented in UPGRADE_NOTES |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ minor — This row contradicts D8 and the changelog.

The row says validators now output string and that this z.infer change is documented in UPGRADE_NOTES. D8 says output types stay number, and the changelog says validators keep number output. The BC table is what reviewers check under BACKWARD_COMPATIBILITY.md, so it should match what shipped. If output really stays number, change the row to "input widened to number | string; output unchanged" and mark it ADDITIVE.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the row was left over from the first plan. Validator output stays number (D8), so the row now says: parsed output unchanged; inputs that only took JSON numbers (gateway amounts, staff rate override) also accept decimal strings; bounds that mirrored the old column precision are removed. Classified ✓ ADDITIVE, which matches BACKWARD_COMPATIBILITY.md.

@patrykbojczuk

Copy link
Copy Markdown
Author

Re the walkthrough note that the TLDR and the BC table still described the earlier plan: agreed and fixed. Both now match D7/D8 (float copies, validators keep number output). I also fixed other stale bits: the precision service is not cached per request but is scoped to one organization, the hook bridge now also keeps changes made only to the exact field, and the 1000 exponent cap is now documented.

@patrykbojczuk

Copy link
Copy Markdown
Author

Superseded with implementation #145

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants