Files
zeekayandHanzo Dev 92461c82b6 a notification says the amount a person sent, not the one that is stored
Everything is held in minor units, and the transaction notice printed the
stored number: a $250.00 transfer told the customer it was 25000, and a
whole bitcoin told them 1000000. Rendering is now done where the number of
decimal places is already known, beside DecimalsFor, in integer arithmetic
— money put through a float is money rounded by whatever the float held.
Yen has no minor unit and must not grow a decimal point; a negative amount
keeps its sign, since truncation toward zero makes both halves negative
and a naive join prints a loss as a gain.

The two senders looked up account, owner and address separately and logged
separately. That is one lookup and one send, so it is written once. What
each says is now a function of the record alone, which is what makes it
checkable without sending anything — and the KYC test was silently
skipping over a collection the harness never created, so it had never run
at all.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-27 07:33:38 -07:00

85 lines
2.5 KiB
Go

package collections
import "testing"
// An amount is stored in minor units everywhere, so rendering it is where the
// unit has to be undone. Getting it wrong is not a rounding difference — it is
// a factor of a hundred, or of a million for crypto, on a number a customer
// reads as their own money.
func TestFormatUndoesTheMinorUnit(t *testing.T) {
for _, tc := range []struct {
minor int64
cur string
want string
}{
// Fiat: two places.
{25000, "USD", "250.00"},
{1, "USD", "0.01"},
{0, "USD", "0.00"},
{100, "EUR", "1.00"},
{123456789, "GBP", "1234567.89"},
// Yen has no minor unit, so it must not grow a decimal point.
{25000, "JPY", "25000"},
{0, "JPY", "0"},
// Crypto: six places, and a whole unit is a million of them.
{1000000, "BTC", "1.000000"},
{1, "BTC", "0.000001"},
{12500000, "LUX", "12.500000"},
// The currency is read case-insensitively, like everywhere else.
{25000, "usd", "250.00"},
{1000000, "btc", "1.000000"},
// An unknown currency falls to two places rather than refusing —
// a notification is not the place to discover an unsupported code.
{25000, "ZZZ", "250.00"},
} {
if got := Format(tc.minor, tc.cur); got != tc.want {
t.Errorf("Format(%d, %q) = %q, want %q", tc.minor, tc.cur, got, tc.want)
}
}
}
// A negative amount is a refund or a reversal, and its sign is the whole
// meaning. Integer division truncates toward zero, so both halves of the number
// come back negative and a naive join prints -0.50 as 0.50 — a loss read as a
// gain.
func TestFormatKeepsTheSignOfALoss(t *testing.T) {
for _, tc := range []struct {
minor int64
cur string
want string
}{
{-25000, "USD", "-250.00"},
{-50, "USD", "-0.50"},
{-1, "USD", "-0.01"},
{-1, "BTC", "-0.000001"},
{-25000, "JPY", "-25000"},
{-100, "USD", "-1.00"},
} {
if got := Format(tc.minor, tc.cur); got != tc.want {
t.Errorf("Format(%d, %q) = %q, want %q", tc.minor, tc.cur, got, tc.want)
}
}
}
// The extremes hold too: the arithmetic is integer, so nothing here is a float
// wide enough to lose a cent in.
func TestFormatHoldsAtTheEdges(t *testing.T) {
for _, tc := range []struct {
minor int64
cur string
want string
}{
{9223372036854775807, "USD", "92233720368547758.07"},
{-9223372036854775808, "USD", "-92233720368547758.08"},
{9223372036854775807, "JPY", "9223372036854775807"},
} {
if got := Format(tc.minor, tc.cur); got != tc.want {
t.Errorf("Format(%d, %q) = %q, want %q", tc.minor, tc.cur, got, tc.want)
}
}
}