Amounts and orders Required
Hold money as integer minor units everywhere
Hold every amount as an integer count of the currency's minor unit, with its ISO 4217 code, in code, database, provider calls and stored events.
How to do it
The API rule money sets the wire format, and exact types sets the column type. Inside the code, use one money type that carries amount and currency, as in value objects. Refuse to add amounts in two currencies. Look up each currency's number of decimals, since JPY has none and KWD has three. Some providers differ from ISO 4217 for a few currencies, so convert through one tested function. Format for display only at the edge, in the reader's locale.
Example
class Money {
constructor(minor, currency) {
if (!Number.isSafeInteger(minor)) throw new TypeError('Amount must be a whole number of minor units')
Object.assign(this, { minor, currency })
Object.freeze(this)
}
plus(other) {
if (other.currency !== this.currency) throw new Error(`Cannot add ${other.currency} to ${this.currency}`)
return new Money(this.minor + other.minor, this.currency)
}
}
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
minor: int
currency: str
def __add__(self, other: "Money") -> "Money":
if other.currency != self.currency:
raise ValueError(f"Cannot add {other.currency} to {self.currency}")
return Money(self.minor + other.minor, self.currency)
How to check
A unit test adds SEK to EUR and expects an error. Search the payment code for parseFloat, float( and Number( applied to amounts. Nothing is found.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers payments playbook: https://agentwranglers.se/playbook/payments/amounts-and-orders/money-in-minor-units.