Skip to content

Reference

Money

Reference for go-sequent's types.Money: decimal amounts with ISO currency codes, JSON string encoding, arithmetic, rounding, and the float64 ban with AssertNoFloatMoney and BanFloat64Money.

Money

Package: types

types.Money is a decimal amount plus a currency code. It is the only money type the framework uses. float64 is banned for money.

Why decimal

Binary floating point cannot represent 0.1 exactly, and errors build up across thousands of ledger entries. go-sequent stores exact decimal amounts. It does not use integer minor units as the framework representation: amounts keep the precision you give them, and the rounding policy is yours.

The decimal arithmetic comes from shopspring/decimal.

The type

type Money struct {
    Amount   decimal.Decimal `json:"amount"`
    Currency string          `json:"currency"`
}

Construct it from strings:

m, err := types.NewMoney("100.00", "CNY")
m2 := types.MustMoney("25.50", "CNY") // panics on bad input; for fixtures and tests
z := types.Zero("CNY")

NewMoney trims whitespace, upper-cases the currency, and returns an error for an empty currency or an invalid amount.

Operations

Method Notes
Add(other), Sub(other) Return (Money, error). Error if currencies differ.
Mul(factor decimal.Decimal) Multiply by a decimal factor.
Round(places int32) Round to a number of decimal places.
Cmp(other), Equal(other) Compare. Cmp errors on a currency mismatch.
IsNegative(), IsPositive(), IsZero() Sign checks.
String() For example 100.00 CNY.
opening := types.MustMoney("100.00", "CNY")
next, err := opening.Sub(types.MustMoney("30.00", "CNY"))
if err != nil { /* currency mismatch */ }
if next.IsNegative() { /* insufficient funds */ }
rounded := next.Round(2)

Mixed currencies never combine silently. Your aggregate should check currencies before applying events, as the bank Account does.

JSON

Amount is written as a decimal string, never a JSON number:

{"amount": "100.00", "currency": "CNY"}

This keeps JSON parsers from turning the amount into a float on the way through. On decode, quoted strings are preferred. A bare number that parses as a decimal without an exponent is accepted, but discouraged. Always send strings from your clients. A missing or empty currency is an error.

Rounding and history

go-sequent does not impose accounting rules. Rounding policy belongs to your domain. Store the exact amounts in events, and never recompute historical money from floats. If a rule changes, new events follow the new rule and old events stay as they were.

Banning float64

Two helpers walk a value and fail when an exported float64 field has a money-like name (Amount, Balance, Money, Credit, Debit, Price, Fee, Total):

// In a test:
if err := testkit.BanFloat64Money(&bank.Deposit{}); err != nil { t.Fatal(err) }

// At startup:
if err := types.AssertNoFloatMoney(&bank.Deposit{}); err != nil { log.Fatal(err) }

Run one over every command and event type. The check is by field name, so it catches Amount float64 but cannot know that a field called Cents holds money. Treat it as a safety net and a code review rule.

type bad struct{ Amount float64 }
testkit.BanFloat64Money(bad{Amount: 1.23}) // returns an error

Docs track the go-sequent library. When APIs drift, the source wins.