Skip to content

Make fulus production ready for finance - #11

Merged
khatibomar merged 19 commits into
mainfrom
production-ready
Oct 10, 2026
Merged

khatibomar merged 19 commits into
mainfrom
production-ready

Conversation

@khatibomar

Copy link
Copy Markdown
Owner

Fix Convert across minor units, make rounding explicit, and tighten the currency type constraint.
Move to 128-bit amounts, opaque Rate and Factor types, decimal JSON and NUMERIC SQL, and a separate format package.
Add currency registries, differential tests, CI coverage, benchmark, Scorecard and SLSA release checks, and the fulusproto module.

Convert applied the ratio to minor units, so EUR 1.00 at EUR/JPY 160.25
gave JPY 16,025. The ratio is now read in major units, as markets quote it,
and Convert adjusts for the minor units of each currency.

Signed-off-by: ayn <ayn.khatib@gmail.com>
An unset mode silently truncated. The modes now start at 1, so the zero
value returns ErrInvalidRoundingMode. RoundUnnecessary returns ErrInexact
when the exact result needs rounding.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Money[currency.Currency] compiled and then panicked with a nil dereference.
The new constraint currency.Unit requires ~struct{}, and TestTypeSafety
type-checks that invalid currency types do not compile.

Signed-off-by: ayn <ayn.khatib@gmail.com>
The generator skipped every ISO 4217 entry for a ZZ country, so XAU, XAG,
XPT and XPD were missing. It now skips only XTS and XXX. When ISO 4217 has
no minor units, the generator uses the CLDR digits and says so in the doc
comment, instead of a silent default of 2.

Signed-off-by: ayn <ayn.khatib@gmail.com>
AnyMoney.Add and Sub allocated three big.Int values for each call. They now
use the same checked int64 arithmetic as Money and do not allocate.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Distribute returned plain int64 chunk sizes, Allocate wrapped the parts in
an Allocation with a redundant Total, and Validate took int64 bounds.
Distribution is now generic with Money fields, Allocate is variadic and
returns []Money[T], and Validate takes Money bounds.

Signed-off-by: ayn <ayn.khatib@gmail.com>
Ratio had exported fields, so a negative or zero literal converted without
an error. Rate[Base, Quote] has a validating constructor and adds Invert,
Cross and text encoding. Cross takes Rate[A, B] and Rate[B, C], so the
compiler checks a triangulation. Convert now returns only the Money value.

Signed-off-by: ayn <ayn.khatib@gmail.com>
MulDecimal parsed a string on each call, so a typo failed only at run time
and in the hot path. Factor is parsed one time with ParseFactor, which also
reads percentages, and Percent and Bps build common factors. ParseFactor
and ParseRate reject exponents, because big.Rat can allocate a very large
number for an input such as 1e999999999.

Signed-off-by: ayn <ayn.khatib@gmail.com>

Signed-off-by: ayn <ayn.khatib@gmail.com>
An int64 amount limits a token with 18 minor units to about 9.2 units,
which is not enough for crypto or trading. Money now holds a 128-bit
two's complement integer, with 192-bit intermediate products and a
math/big fallback only for terms that do not fit. Amount is replaced by
Int64, BigInt and NewMoneyFromBigInt. A value is 16 bytes and stays
comparable with ==.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
JSON held the amount in minor units with no scale, so a payload changed its
value if the reader had other minor units, for example after an ISO 4217
change. MarshalJSON now writes {"amount":"10.50","currency":"USD"} and
UnmarshalJSON checks the scale against the minor units.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
Scan read integer text as minor units, so a NUMERIC value of 1050 dollars
loaded as $10.50, and it failed on NUMERIC text such as 10.50. Value and
Scan now use the canonical decimal for NUMERIC columns, with a scale check.
BigintMoney stores minor units in an integer column, so the column type is
always explicit.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
A change of the minor units in the ISO 4217 data changes the value of every
amount that is stored in minor units. A golden file now records the minor
units of each generated currency, a test compares the data with it, and CI
fails if a stable release changes or removes a line.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
String used a global default locale, so log lines and test output changed
with a process-wide setting. String now gives the currency code and the
canonical decimal, for example "USD -1234.50". Format(loc) is the only
localized output.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
The Currency interface required FormatInfo, so every currency, the core
package and each binary that imports it pulled in the CLDR tables. Currency
now needs only Code and MinorUnits, with Numbered and Named as optional
interfaces. The format package holds Money, AnyMoney, Parse, ParseAny and
the Formatter interface for custom data. Parse now also accepts the minus
sign of the locale for a pattern without a negative subpattern.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
Custom currencies could only go into one global registry, so tenants and
tests shared state. Registry is now a type with ByCode, ByNumber, Register
and All, and the package functions use Default(). Register also rejects a
nil currency and negative minor units.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
The property tests checked only identities such as commutativity.
checkMoneyOps compares Add, Sub, Mul, Neg, Abs, Div, MulFactor, Convert and
RoundCash, for every rounding mode, with a math/big reference that does not
share code with the package. checkAllocate does the same for the largest
remainder method. FuzzDifferential runs both with random 128-bit amounts.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
CI now fails if the coverage of the fulus and format packages is below 95%,
and reports benchstat results for each pull request. A Scorecard workflow
publishes the OpenSSF analysis. A release workflow checks that the tag is
signed, creates the GitHub release with a source archive and adds SLSA
level 3 provenance.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
gRPC services exchange money as google.type.Money. fulusproto converts
Money and AnyMoney to and from it without rounding. It is a separate
module, so the fulus module keeps no dependencies.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
The README, the package documentation and the design documents now show
the format package, Rate, Factor, the decimal JSON and SQL forms, and the
fulusproto module. The README programs compile and give the shown output.

Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>



Signed-off-by: ayn <ayn.khatib@gmail.com>
@khatibomar
khatibomar merged commit 76f1f13 into main Oct 10, 2026
7 checks passed
@khatibomar
khatibomar deleted the production-ready branch October 10, 2026 01:35
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.

1 participant