feat(gl): add immutable postgres ledger storage

This commit is contained in:
2026-08-14 19:00:45 +03:30
parent b1e0b01598
commit 0b3eaa0c3d
14 changed files with 1139 additions and 1 deletions
+115
View File
@@ -0,0 +1,115 @@
// Package ledger contains GL's framework-independent financial model.
package ledger
import (
"fmt"
"math/big"
"strings"
"unicode"
)
const (
AmountPrecision = 38
AmountScale = 18
)
var amountFactor = new(big.Int).Exp(big.NewInt(10), big.NewInt(AmountScale), nil)
// Amount is an exact fixed-point decimal with PostgreSQL numeric(38,18)
// semantics. Its zero value is a valid zero amount.
type Amount struct {
units big.Int
}
func ParseAmount(value string) (Amount, error) {
if value == "" {
return Amount{}, fmt.Errorf("amount is required")
}
negative := value[0] == '-'
unsigned := value
if negative {
unsigned = value[1:]
}
if unsigned == "" || strings.HasPrefix(unsigned, "+") {
return Amount{}, fmt.Errorf("invalid amount %q", value)
}
parts := strings.Split(unsigned, ".")
if len(parts) > 2 || parts[0] == "" {
return Amount{}, fmt.Errorf("invalid amount %q", value)
}
for _, part := range parts {
if part == "" || strings.IndexFunc(part, func(r rune) bool { return !unicode.IsDigit(r) }) >= 0 {
return Amount{}, fmt.Errorf("invalid amount %q", value)
}
}
if len(parts[0]) > 1 && parts[0][0] == '0' {
return Amount{}, fmt.Errorf("amount must not contain leading zeroes")
}
if len(strings.TrimLeft(parts[0], "0")) > AmountPrecision-AmountScale {
return Amount{}, fmt.Errorf("amount exceeds integer precision %d", AmountPrecision-AmountScale)
}
fraction := ""
if len(parts) == 2 {
fraction = parts[1]
if len(fraction) > AmountScale {
return Amount{}, fmt.Errorf("amount exceeds scale %d", AmountScale)
}
}
digits := strings.TrimLeft(parts[0]+fraction, "0")
if len(digits) > AmountPrecision {
return Amount{}, fmt.Errorf("amount exceeds precision %d", AmountPrecision)
}
whole := new(big.Int)
whole.SetString(parts[0], 10)
units := new(big.Int).Mul(whole, amountFactor)
if fraction != "" {
padded := fraction + strings.Repeat("0", AmountScale-len(fraction))
fractional := new(big.Int)
fractional.SetString(padded, 10)
units.Add(units, fractional)
}
if negative {
units.Neg(units)
}
return Amount{units: *units}, nil
}
func (a Amount) IsZero() bool {
return a.units.Sign() == 0
}
func (a Amount) Add(other Amount) Amount {
var result big.Int
result.Add(&a.units, &other.units)
return Amount{units: result}
}
func (a Amount) Negate() Amount {
var result big.Int
result.Neg(&a.units)
return Amount{units: result}
}
func (a Amount) String() string {
if a.units.Sign() == 0 {
return "0"
}
abs := new(big.Int).Abs(new(big.Int).Set(&a.units))
whole, fraction := new(big.Int), new(big.Int)
whole.QuoRem(abs, amountFactor, fraction)
value := whole.String()
if fraction.Sign() != 0 {
fractionText := fmt.Sprintf("%018s", fraction.String())
value += "." + strings.TrimRight(fractionText, "0")
}
if a.units.Sign() < 0 {
return "-" + value
}
return value
}