176 lines
5.1 KiB
Go
176 lines
5.1 KiB
Go
package tax
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strconv"
|
|
|
|
"git.k6n.net/mats/platform/money"
|
|
)
|
|
|
|
// Price is a monetary amount in minor units (money.Cents) with its VAT broken
|
|
// down by rate. IncVat is the gross total; VatRates maps each tax rate — basis
|
|
// points of a percent, 2500 = 25%, 1250 = 12.5% (the single platform scale,
|
|
// matching OrderLine.tax_rate) — to its VAT amount in minor units.
|
|
//
|
|
// All arithmetic is integer (money.Cents is int64-backed); VAT is only ever
|
|
// computed by Compute (ex-VAT-first), always on a real total. Aggregation
|
|
// (Add/SumPrices) only sums integers, so totals never drift from the sum of
|
|
// their parts. The JSON wire shape is unchanged — Cents marshals as int64.
|
|
type Price struct {
|
|
IncVat money.Cents `json:"incVat"`
|
|
VatRates map[int]money.Cents `json:"vat,omitempty"`
|
|
}
|
|
|
|
// NewPrice returns a new zero Price.
|
|
func NewPrice() *Price {
|
|
return &Price{
|
|
IncVat: 0,
|
|
VatRates: make(map[int]money.Cents),
|
|
}
|
|
}
|
|
|
|
// NewPriceFromIncVat builds a Price from a gross total and a tax rate in basis
|
|
// points (2500 = 25%, 1250 = 12.5%). VAT is computed once via Compute, so
|
|
// exVat + vat == incVat exactly.
|
|
func NewPriceFromIncVat(incVat money.Cents, rateBp int) *Price {
|
|
return &Price{
|
|
IncVat: incVat,
|
|
VatRates: map[int]money.Cents{
|
|
rateBp: money.Cents(Compute(incVat.Int64(), rateBp)),
|
|
},
|
|
}
|
|
}
|
|
|
|
// ValueExVat returns the price excluding VAT.
|
|
func (p *Price) ValueExVat() money.Cents {
|
|
exVat := p.IncVat
|
|
for _, amount := range p.VatRates {
|
|
exVat -= amount
|
|
}
|
|
return exVat
|
|
}
|
|
|
|
// TotalVat returns the total VAT amount.
|
|
func (p *Price) TotalVat() money.Cents {
|
|
total := money.Cents(0)
|
|
for _, amount := range p.VatRates {
|
|
total += amount
|
|
}
|
|
return total
|
|
}
|
|
|
|
// MultiplyPrice scales a Price to a quantity and returns a new Price.
|
|
//
|
|
// For a single-rate price (a line item) it recomputes the VAT on the *scaled
|
|
// total* via NewPriceFromIncVat — not by multiplying the per-unit VAT — so the
|
|
// result matches a payment provider that re-derives tax from the line total, and
|
|
// VAT rounding never accumulates per unit. VAT is only ever computed on a real
|
|
// total.
|
|
//
|
|
// A zero-rate price scales its (untaxed) IncVat. A multi-rate price (which a line
|
|
// item never is — totals are built by Add, never Multiply) falls back to scaling
|
|
// each component, since IncVat can't be split across rates.
|
|
func MultiplyPrice(p Price, qty int64) *Price {
|
|
if len(p.VatRates) == 1 {
|
|
for rate := range p.VatRates {
|
|
return NewPriceFromIncVat(p.IncVat.Mul(qty), rate)
|
|
}
|
|
}
|
|
ret := &Price{
|
|
IncVat: p.IncVat.Mul(qty),
|
|
VatRates: make(map[int]money.Cents, len(p.VatRates)),
|
|
}
|
|
for rate, amount := range p.VatRates {
|
|
ret.VatRates[rate] = amount.Mul(qty)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// Multiply scales the Price by a quantity in place (see MultiplyPrice).
|
|
func (p *Price) Multiply(qty int64) {
|
|
res := MultiplyPrice(*p, qty)
|
|
p.IncVat = res.IncVat
|
|
p.VatRates = res.VatRates
|
|
}
|
|
|
|
// MarshalJSON serializes Price as a wire-friendly object with exVat, incVat, and
|
|
// vat (rate-keyed by raw percent as a string, e.g. "25"). Cents marshals as
|
|
// int64, so the wire bytes are byte-identical to the pre-money.Cents shape.
|
|
func (p Price) MarshalJSON() ([]byte, error) {
|
|
exVat := p.ValueExVat()
|
|
var vat map[string]money.Cents
|
|
if len(p.VatRates) > 0 {
|
|
vat = make(map[string]money.Cents, len(p.VatRates))
|
|
for rate, amount := range p.VatRates {
|
|
vat[strconv.Itoa(rate)] = amount
|
|
}
|
|
}
|
|
type wire struct {
|
|
ExVat money.Cents `json:"exVat"`
|
|
IncVat money.Cents `json:"incVat"`
|
|
Vat map[string]money.Cents `json:"vat,omitempty"`
|
|
}
|
|
return json.Marshal(wire{ExVat: exVat, IncVat: p.IncVat, Vat: vat})
|
|
}
|
|
|
|
// UnmarshalJSON deserializes Price from its wire format.
|
|
func (p *Price) UnmarshalJSON(data []byte) error {
|
|
type wire struct {
|
|
ExVat money.Cents `json:"exVat"`
|
|
IncVat money.Cents `json:"incVat"`
|
|
Vat map[string]money.Cents `json:"vat,omitempty"`
|
|
}
|
|
var w wire
|
|
if err := json.Unmarshal(data, &w); err != nil {
|
|
return err
|
|
}
|
|
|
|
p.IncVat = w.IncVat
|
|
p.VatRates = make(map[int]money.Cents, len(w.Vat))
|
|
for rateStr, amount := range w.Vat {
|
|
// Tolerate legacy float-style keys ("2500", "1250") by truncating to the
|
|
// basis point integer convention.
|
|
rate, err := strconv.Atoi(rateStr)
|
|
if err != nil {
|
|
f, ferr := strconv.ParseFloat(rateStr, 64)
|
|
if ferr != nil {
|
|
return err
|
|
}
|
|
rate = int(f)
|
|
}
|
|
p.VatRates[rate] = amount
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Add adds another Price's values to this one.
|
|
func (p *Price) Add(price Price) {
|
|
p.IncVat += price.IncVat
|
|
for rate, amount := range price.VatRates {
|
|
p.VatRates[rate] += amount
|
|
}
|
|
}
|
|
|
|
// Subtract subtracts another Price's values from this one.
|
|
func (p *Price) Subtract(price Price) {
|
|
p.IncVat -= price.IncVat
|
|
for rate, amount := range price.VatRates {
|
|
p.VatRates[rate] -= amount
|
|
}
|
|
}
|
|
|
|
// SumPrices aggregates multiple Prices into one.
|
|
func SumPrices(prices ...Price) *Price {
|
|
aggregated := NewPrice()
|
|
for _, price := range prices {
|
|
aggregated.IncVat += price.IncVat
|
|
for rate, amount := range price.VatRates {
|
|
aggregated.VatRates[rate] += amount
|
|
}
|
|
}
|
|
if len(aggregated.VatRates) == 0 {
|
|
aggregated.VatRates = nil
|
|
}
|
|
return aggregated
|
|
}
|