Skip to content
← All articles

Sep 24, 2026
2 min read

  • Fintech
  • Multi-currency
  • PostgreSQL
  • TypeScript
  • Architecture

In Goma, a customer can pay half in CDF and half in USD. Floats, rounding and moving exchange rates will quietly break your books. Here is the model I use.

In most tutorials, money is a number and a currency symbol. In eastern Congo that model breaks on the first day: prices are quoted in dollars, change is given in francs, and the rate moves during the week.

Building tills and treasuries for products like Hela and Nunua, I ended up with a few rules that I now apply everywhere.

1. Never store money as a float

0.1 + 0.2 is not 0.3 in JavaScript, and that error compounds across thousands of sales. Store integers in the smallest unit:

type Money = {
  amount: bigint | number; // minor units: cents for USD, francs for CDF
  currency: 'USD' | 'CDF';
};

In PostgreSQL that's a bigint (or numeric if you truly need it), never real.

2. A conversion is an event with its own rate

The mistake I see most often: storing a payment in USD and recomputing the CDF value later with today's rate. Six months later, your reports no longer match the receipts customers hold.

Instead, every conversion records the rate that was used:

type Payment = {
  id: string;
  paid: Money;          // what the customer actually handed over
  settles: Money;       // what it counts for on the invoice
  rate: string;         // e.g. "2850" CDF per USD, stored as a decimal string
  rateSource: 'manual' | 'daily';
  at: Date;
};

Reports then sum settles — no conversion at read time, no drift.

3. Decide who absorbs the rounding

Converting 10 USD at 2 850 gives exactly 28 500 CDF. Converting 7.35 USD gives 20 947.5 — and francs have no half. Someone wins or loses that half franc.

Pick a rule, write it down, apply it in one function:

  • round in the customer's favour at the till;
  • keep the difference in an explicit rounding line, so the books still balance.

4. Mixed payments are just several payments

"Half in dollars, the rest in francs" sounds complicated, but it is two Payment rows against the same invoice. The invoice is settled when the sum of settles reaches the total. Change (la monnaie) is a negative payment in the currency you give back.

5. Show both, always

Cashiers think in both currencies. Every total on screen shows the invoice currency and its equivalent at the current rate, with the rate visible. It removes a whole category of disputes at the counter.

Checklist

  • Integers in minor units, typed with their currency
  • Rate stored on every conversion, never recomputed
  • One rounding function, with an explicit rounding line
  • Mixed payments = multiple payment rows
  • Both currencies and the rate visible on screen

None of this is exotic. But skipping any of these rules works fine in the demo — and fails the first time an accountant reconciles a month of real sales.