carlos / platform Public

Per-line tax codes, Task 3: attribution and apportionment, built behind the hold

taxCodesForLines keeps today's answer: one code is carried on the
calculation, two hold the month (freeze goes through it). Behind that
error: eligibleIntervals (today's paid coverage for a partly covered
month, the whole month otherwise, since a fully covered month's Covered
is nil by design); usageByCode (eligible days first, as today; each day
to the span holding its first instant, 16a's proposal as the one rule
body; per (code, meter) sums in date order, one floor to base units each,
then the two transfer meters added, the month's own order); apportion
(floor, largest remainder, byte-order ties, zero shares omitted, an
amount with no weight an error); splitLinesByGrossUsage and
lastSpansCodeLines, the two bodies decision 16 would choose between,
neither called. checkedStatementDays and billingMeterScale are the
existing validation and scales, shared; Task 0's pinned bytes pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 00f70d0ef263a4fe06feaf95521ae13b6d44763b parent 510bbb7
4 files changed, +556 −37
  • internal/console/billingcalculation.go +52 −32
  • internal/console/billinginvoice.go +9 −5
  • internal/console/billingtaxsplit.go +275 −0
  • internal/console/billingtaxsplit_test.go +220 −0

CI passed — run details

diff --git a/internal/console/billingcalculation.go b/internal/console/billingcalculation.go
index b62682b7c..66fa45662 100644
--- a/internal/console/billingcalculation.go
+++ b/internal/console/billingcalculation.go
@@ -188,14 +188,7 @@ func priceBillingCalculation(statement usage.Statement, history []plans.Assignme
}
var amounts [5]int64
for i, meter := range monthlyBillingMeters {
- scale := int64(1_000_000_000)
- if meter == usage.MeterInstanceHours {
- scale = 3600
- }
- if meter == usage.MeterDurabilityWrites1k {
- scale = 1000
- }
- amounts[i], err = billingBaseUnits(billed[meter], scale)
+ amounts[i], err = billingBaseUnits(billed[meter], billingMeterScale(meter))
if err != nil {
return nil, err
}
@@ -244,34 +237,37 @@ var errBillingDays = errors.New("billing_statement_days_unusable")
// quiet: the invoice pass logs it by name on every attempt.
var errBillingDaysMismatch = errors.New("billing_statement_days_mismatch")
-// billingUsageOnPaidDays returns each billed meter summed over the UTC days
-// that lie wholly inside one paid span, and those days in order.
-//
-// A day counts only when all 24 hours are inside a single covered interval.
-// A day the plan covered only part of (it started, ended or lapsed inside
-// the day) is not counted, because the statement cannot say which of that
-// day's usage fell on which side. A plan that starts exactly at midnight
-// covers that whole day, and it counts. The rounding is toward the
-// customer, by at most two partial days for each continuous paid span, so
-// a month with several spans (lapses) can leave out more than two.
-//
-// Before anything is counted the days must account for the statement: every
-// day inside the month, and each billed meter's days summing to its monthly
-// total. The days are walked in date order, so the sums, and with them
-// whether a statement at the edge of the tolerance is accepted, are the
-// same on every run.
-func billingUsageOnPaidDays(statement usage.Statement, covered []plans.CoveredInterval) (map[string]float64, []string, error) {
- start, err := time.Parse("2006-01", statement.Month)
+// billingMeterScale is the base unit of a billed meter: seconds of an
+// awake hour, thousandths of a thousand writes, nanos of everything else.
+func billingMeterScale(meter string) int64 {
+ switch meter {
+ case usage.MeterInstanceHours:
+ return 3600
+ case usage.MeterDurabilityWrites1k:
+ return 1000
+ }
+ return 1_000_000_000
+}
+
+// checkedStatementDays validates a statement's daily record as a whole
+// and returns the month's bounds: a statement with no days is usable only
+// when the month recorded nothing at all; every date lies inside the
+// month; every value is finite and not negative; each billed meter's days
+// sum to its monthly total within a relative tolerance. The eligibility
+// of days (billingUsageOnPaidDays) and the attribution of days to plans
+// (billingtaxsplit.go) both read the days only after this.
+func checkedStatementDays(statement usage.Statement) (start, end time.Time, err error) {
+ start, err = time.Parse("2006-01", statement.Month)
if err != nil {
- return nil, nil, errBillingDaysMismatch
+ return start, end, errBillingDaysMismatch
}
- end := start.AddDate(0, 1, 0)
+ end = start.AddDate(0, 1, 0)
if statement.Days == nil {
// No day recorded anything. That is a usable statement only when
// the month recorded nothing at all.
for _, meter := range monthlyBillingMeters {
if statement.Meters[meter] != 0 {
- return nil, nil, errBillingDays
+ return start, end, errBillingDays
}
}
}
@@ -284,7 +280,7 @@ func billingUsageOnPaidDays(statement usage.Statement, covered []plans.CoveredIn
for _, date := range dates {
day, perr := time.Parse("2006-01-02", date)
if perr != nil || day.Before(start) || !day.Before(end) {
- return nil, nil, errBillingDaysMismatch
+ return start, end, errBillingDaysMismatch
}
for _, meter := range monthlyBillingMeters {
v, ok := statement.Days[date][meter]
@@ -292,7 +288,7 @@ func billingUsageOnPaidDays(statement usage.Statement, covered []plans.CoveredIn
continue
}
if v < 0 || math.IsNaN(v) || math.IsInf(v, 0) {
- return nil, nil, errBillingMeter
+ return start, end, errBillingMeter
}
sums[meter] += v
}
@@ -300,9 +296,33 @@ func billingUsageOnPaidDays(statement usage.Statement, covered []plans.CoveredIn
for _, meter := range monthlyBillingMeters {
total := statement.Meters[meter]
if diff := math.Abs(sums[meter] - total); diff > 1e-9*math.Max(1, math.Abs(total)) {
- return nil, nil, errBillingDaysMismatch
+ return start, end, errBillingDaysMismatch
}
}
+ return start, end, nil
+}
+
+// billingUsageOnPaidDays returns each billed meter summed over the UTC days
+// that lie wholly inside one paid span, and those days in order.
+//
+// A day counts only when all 24 hours are inside a single covered interval.
+// A day the plan covered only part of (it started, ended or lapsed inside
+// the day) is not counted, because the statement cannot say which of that
+// day's usage fell on which side. A plan that starts exactly at midnight
+// covers that whole day, and it counts. The rounding is toward the
+// customer, by at most two partial days for each continuous paid span, so
+// a month with several spans (lapses) can leave out more than two.
+//
+// Before anything is counted the days must account for the statement: every
+// day inside the month, and each billed meter's days summing to its monthly
+// total. The days are walked in date order, so the sums, and with them
+// whether a statement at the edge of the tolerance is accepted, are the
+// same on every run.
+func billingUsageOnPaidDays(statement usage.Statement, covered []plans.CoveredInterval) (map[string]float64, []string, error) {
+ start, end, err := checkedStatementDays(statement)
+ if err != nil {
+ return nil, nil, err
+ }
out := map[string]float64{}
var counted []string
for day := start; day.Before(end); day = day.AddDate(0, 0, 1) {
diff --git a/internal/console/billinginvoice.go b/internal/console/billinginvoice.go
index 72efcbbb4..bcfd0b54a 100644
--- a/internal/console/billinginvoice.go
+++ b/internal/console/billinginvoice.go
@@ -467,16 +467,20 @@ func (r *billingInvoiceRun) freeze(ctx context.Context) (billingMonthRecord, err
if identity.LiveMode != r.b.cfg.LiveMode {
return billingMonthRecord{}, errBillingModeMismatch
}
- taxCode := ""
+ codes := make([]string, 0, len(calc.Allowance.Assignments))
for _, assignment := range calc.Allowance.Assignments {
code, ok := r.b.cfg.taxCode(assignment.Plan)
if !ok {
return billingMonthRecord{}, errBillingUnavailable
}
- if taxCode != "" && taxCode != code {
- return billingMonthRecord{}, errBillingTaxCodeMixed
- }
- taxCode = code
+ codes = append(codes, code)
+ }
+ // One code, and the calculation carries it as it always has; two, and
+ // the month holds (taxCodesForLines; decision 16 decides what a
+ // two-code month's lines carry, and nothing here guesses).
+ taxCode, err := taxCodesForLines(codes)
+ if err != nil {
+ return billingMonthRecord{}, err
}
calc.TaxCode = taxCode
raw, err := json.Marshal(calc)
diff --git a/internal/console/billingtaxsplit.go b/internal/console/billingtaxsplit.go
new file mode 100644
index 000000000..71300c7a3
--- /dev/null
+++ b/internal/console/billingtaxsplit.go
@@ -0,0 +1,275 @@
+package console
+
+import (
+ "errors"
+ "math/big"
+ "sort"
+ "time"
+
+ "github.com/carlosframework/platform/internal/plans"
+ "github.com/carlosframework/platform/internal/usage"
+)
+
+// Per-line tax codes for a month in which the plan changed
+// (docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md).
+//
+// A month whose paid spans carry two product tax codes is not invoiced
+// today: taxCodesForLines answers errBillingTaxCodeMixed and the month
+// holds for a person. Which code each overage line should then carry is
+// decision 16 (and 16a to 16d) in DECISIONS-FOR-PAUL.md, the owner's. The
+// mechanics either answer needs are here, built and tested behind that
+// error: the attribution of eligible days to the plan in force, the
+// per-code weights in base units, and the apportionment of each axis's
+// unchanged amount. Nothing in this file changes an amount or a one-code
+// month's bytes.
+
+// errBillingTaxApportion means an axis has a positive amount and no
+// attributed usage to divide it by: the usage that produced the charge
+// is not attributed, and the month holds rather than guess.
+var errBillingTaxApportion = errors.New("billing_invoice_tax_code_mismatch")
+
+// taxCodesForLines decides the tax codes of a calculation's lines from the
+// codes of its paid spans: one code, and the calculation carries it as it
+// always has; two or more, and the month holds. The bodies that would
+// answer a two-code month (splitLinesByGrossUsage, lastSpansCodeLines)
+// exist and are tested, and are not called from here until decision 16.
+func taxCodesForLines(codes []string) (string, error) {
+ code := ""
+ for _, c := range codes {
+ if code != "" && c != code {
+ return "", errBillingTaxCodeMixed
+ }
+ code = c
+ }
+ return code, nil
+}
+
+// eligibleIntervals are the intervals of the month whose whole days are
+// billed: the paid coverage of a partly covered month, exactly as
+// billingUsageOnPaidDays reads it, and the whole month otherwise (a fully
+// covered month's Covered is nil by design, which is not "no days").
+func eligibleIntervals(month string, allowance plans.MonthAllowance) ([]plans.CoveredInterval, error) {
+ if allowance.Partial {
+ return allowance.Covered, nil
+ }
+ start, err := time.Parse("2006-01", month)
+ if err != nil {
+ return nil, errBillingDaysMismatch
+ }
+ return []plans.CoveredInterval{{From: start, To: start.AddDate(0, 1, 0)}}, nil
+}
+
+// usageByCode attributes the eligible days' usage to the plan in force and
+// returns, per tax code, the four axes in base units: the allocation
+// weights of a two-code month. The rule for the day is 16a's proposal, the
+// one body there is: a UTC day belongs to the span that contains its first
+// instant. Eligibility comes first and is today's (a day counts only when
+// the whole of it lies inside the eligible intervals), so a lapse at 15:00
+// excludes that day as it does today, and a continuous change at 15:00
+// keeps it. The arithmetic is the month's own order: the days' values
+// are summed per (code, meter) in date order as the statement's floating
+// values, each sum is floored to base units once, then the two transfer
+// meters are added with the overflow check. These are weights, not the
+// month's billed usage, which comes from the whole month's meters as
+// before. A statement without Days is usable only when every billed meter
+// is zero, as billingUsageOnPaidDays has it; such a month has no weights
+// and nothing to divide.
+func usageByCode(statement usage.Statement, intervals []plans.CoveredInterval, spans []plans.PlanSpan, codeOf func(plan string) (string, bool)) (map[string]plans.Resources, error) {
+ start, end, err := checkedStatementDays(statement)
+ if err != nil {
+ return nil, err
+ }
+ if statement.Days == nil {
+ // Nothing was used (the check above), so nothing is attributed.
+ return map[string]plans.Resources{}, nil
+ }
+ sums := map[string]map[string]float64{} // code -> meter -> value
+ for day := start; day.Before(end); day = day.AddDate(0, 0, 1) {
+ next := day.AddDate(0, 0, 1)
+ inside := false
+ for _, iv := range intervals {
+ if !iv.From.After(day) && !iv.To.Before(next) {
+ inside = true
+ break
+ }
+ }
+ if !inside {
+ continue
+ }
+ code, found := "", false
+ for _, sp := range spans {
+ if !sp.From.After(day) && day.Before(sp.To) {
+ c, ok := codeOf(sp.Plan)
+ if !ok {
+ return nil, errBillingUnavailable
+ }
+ code, found = c, true
+ break
+ }
+ }
+ if !found {
+ // An eligible day lies inside the merged coverage, which is
+ // the union of the spans, so its first instant is in one of
+ // them; a day that is not is a history this code cannot read.
+ return nil, errBillingDaysMismatch
+ }
+ if sums[code] == nil {
+ sums[code] = map[string]float64{}
+ }
+ for _, meter := range monthlyBillingMeters {
+ sums[code][meter] += statement.Days[usage.DayOf(day)][meter]
+ }
+ }
+ out := map[string]plans.Resources{}
+ for code, byMeter := range sums {
+ var units [5]int64
+ for i, meter := range monthlyBillingMeters {
+ if units[i], err = billingBaseUnits(byMeter[meter], billingMeterScale(meter)); err != nil {
+ return nil, err
+ }
+ }
+ if units[2] > (1<<63-1)-units[3] {
+ return nil, plans.ErrOverflow
+ }
+ out[code] = plans.Resources{AwakeSeconds: units[0], StorageByteMonths: units[1], TransferBytes: units[2] + units[3], Writes: units[4]}
+ }
+ return out, nil
+}
+
+// axisWeight reads the weight of one axis, named as the lines name it.
+func axisWeight(r plans.Resources, axis string) int64 {
+ switch axis {
+ case "awake_seconds":
+ return r.AwakeSeconds
+ case "storage_byte_months":
+ return r.StorageByteMonths
+ case "transfer_bytes":
+ return r.TransferBytes
+ case "writes":
+ return r.Writes
+ }
+ return 0
+}
+
+// taxShare is one code's share of one axis's amount.
+type taxShare struct {
+ Code string
+ Amount int64
+}
+
+// apportion divides one axis's amount, in minor units, among codes in
+// proportion to their weights, without rounding beyond the axis: each
+// code's share is floored, and the minor units still owed to reach the
+// amount go one at a time to the codes with the largest remainder, ties
+// broken by the code's byte order. A share of zero is omitted. An amount
+// with no weight to divide it by is an error, not a guess; a zero amount
+// has no shares. Two calls with the same inputs give the same shares in
+// the same order whatever order the map was built in.
+func apportion(amount int64, weights map[string]int64) ([]taxShare, error) {
+ if amount < 0 {
+ return nil, errBillingTaxApportion
+ }
+ codes := make([]string, 0, len(weights))
+ var sum int64
+ for code, w := range weights {
+ if w < 0 {
+ return nil, errBillingTaxApportion
+ }
+ if w > 0 {
+ codes = append(codes, code)
+ sum += w
+ if sum < 0 {
+ return nil, plans.ErrOverflow
+ }
+ }
+ }
+ sort.Strings(codes)
+ if amount == 0 {
+ return nil, nil
+ }
+ if sum == 0 {
+ return nil, errBillingTaxApportion
+ }
+ type part struct {
+ code string
+ share int64
+ remainder *big.Int
+ }
+ parts := make([]part, 0, len(codes))
+ var given int64
+ bigAmount, bigSum := big.NewInt(amount), big.NewInt(sum)
+ for _, code := range codes {
+ product := new(big.Int).Mul(bigAmount, big.NewInt(weights[code]))
+ var share, remainder big.Int
+ share.QuoRem(product, bigSum, &remainder)
+ if !share.IsInt64() {
+ return nil, plans.ErrOverflow
+ }
+ parts = append(parts, part{code, share.Int64(), &remainder})
+ given += share.Int64()
+ }
+ extra := amount - given
+ if extra < 0 || extra > int64(len(parts)) {
+ return nil, errBillingTaxApportion
+ }
+ order := make([]int, len(parts))
+ for i := range order {
+ order[i] = i
+ }
+ sort.SliceStable(order, func(i, j int) bool { // parts are in code byte order already; a stable sort keeps ties so
+ return parts[order[i]].remainder.Cmp(parts[order[j]].remainder) > 0
+ })
+ for k := int64(0); k < extra; k++ {
+ parts[order[k]].share++
+ }
+ out := make([]taxShare, 0, len(parts))
+ for _, p := range parts {
+ if p.share > 0 {
+ out = append(out, taxShare{Code: p.code, Amount: p.share})
+ }
+ }
+ return out, nil
+}
+
+// splitLinesByGrossUsage is the body for 16b's proposal: each axis's line,
+// with the amount it has today, is divided among the codes by their
+// attributed usage of that axis, and the result is one line per (axis,
+// code) in axis order then code byte order, each carrying its code. The
+// subtotal is unchanged. Not called from freeze until decision 16.
+func splitLinesByGrossUsage(lines []plans.OverageLine, weights map[string]plans.Resources) ([]plans.OverageLine, error) {
+ out := make([]plans.OverageLine, 0, len(lines))
+ for _, line := range lines {
+ if line.TaxCode != "" {
+ return nil, errBillingTaxApportion
+ }
+ w := map[string]int64{}
+ for code, r := range weights {
+ w[code] = axisWeight(r, line.Meter)
+ }
+ shares, err := apportion(line.AmountCents, w)
+ if err != nil {
+ return nil, err
+ }
+ for _, s := range shares {
+ out = append(out, plans.OverageLine{Meter: line.Meter, AmountCents: s.Amount, TaxCode: s.Code})
+ }
+ }
+ return out, nil
+}
+
+// lastSpansCodeLines is the body for the month-end answer as 16c would
+// settle it: every line carries the one code given, nothing is divided.
+// Not called from freeze until decision 16.
+func lastSpansCodeLines(lines []plans.OverageLine, code string) ([]plans.OverageLine, error) {
+ if !validTaxCode(code) {
+ return nil, errBillingTaxApportion
+ }
+ out := make([]plans.OverageLine, 0, len(lines))
+ for _, line := range lines {
+ if line.TaxCode != "" {
+ return nil, errBillingTaxApportion
+ }
+ out = append(out, plans.OverageLine{Meter: line.Meter, AmountCents: line.AmountCents, TaxCode: code})
+ }
+ return out, nil
+}
diff --git a/internal/console/billingtaxsplit_test.go b/internal/console/billingtaxsplit_test.go
new file mode 100644
index 000000000..91941f33d
--- /dev/null
+++ b/internal/console/billingtaxsplit_test.go
@@ -0,0 +1,220 @@
+package console
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/plans"
+ "github.com/carlosframework/platform/internal/usage"
+)
+
+func taxSplitCodeOf(plan string) (string, bool) {
+ code, ok := taxShapeCodes[plan]
+ return code, ok
+}
+
+func sep(day, hour int) time.Time { return time.Date(2026, 9, day, hour, 0, 0, 0, time.UTC) }
+
+func span(from, to time.Time, plan string) plans.PlanSpan {
+ return plans.PlanSpan{From: from, To: to, Plan: plan}
+}
+
+func iv(from, to time.Time) plans.CoveredInterval { return plans.CoveredInterval{From: from, To: to} }
+
+// A statement with ten awake hours on every day of September.
+func dailyStatement() usage.Statement {
+ st := usage.Statement{V: 1, Account: "bab", Month: "2026-09", Meters: map[string]float64{usage.MeterInstanceHours: 300}, Days: map[string]map[string]float64{}, DaysInMonth: 30, Closed: true}
+ for d := 1; d <= 30; d++ {
+ st.Days[fmt.Sprintf("2026-09-%02d", d)] = map[string]float64{usage.MeterInstanceHours: 10}
+ }
+ return st
+}
+
+// Two codes still hold; one code is carried as it always was.
+func TestTwoTaxCodesStillHoldTheMonth(t *testing.T) {
+ if code, err := taxCodesForLines([]string{"txcd_10102001", "txcd_10102001"}); err != nil || code != "txcd_10102001" {
+ t.Fatalf("one code: %q %v", code, err)
+ }
+ if _, err := taxCodesForLines([]string{"txcd_10102001", "txcd_10102000"}); !errors.Is(err, errBillingTaxCodeMixed) {
+ t.Fatalf("two codes: %v", err)
+ }
+}
+
+// Eligible days first, as today, then each day to the span that holds its
+// first instant.
+func TestADaysUsageFollowsThePlanInForceAtItsStart(t *testing.T) {
+ oct1 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
+ whole := []plans.CoveredInterval{{From: sep(1, 0), To: oct1}}
+ hours := func(h int64) int64 { return h * 3600 }
+ for name, tc := range map[string]struct {
+ intervals []plans.CoveredInterval
+ spans []plans.PlanSpan
+ want map[string]plans.Resources
+ err error
+ }{
+ "a change at 00:00": {whole, []plans.PlanSpan{span(sep(1, 0), sep(16, 0), plans.PlanPersonal), span(sep(16, 0), oct1, plans.PlanBusiness)},
+ map[string]plans.Resources{"txcd_10102001": {AwakeSeconds: hours(150)}, "txcd_10102000": {AwakeSeconds: hours(150)}}, nil},
+ "a continuous change at 15:00 (the day eligible, under the earlier plan)": {whole, []plans.PlanSpan{span(sep(1, 0), sep(16, 15), plans.PlanPersonal), span(sep(16, 15), oct1, plans.PlanBusiness)},
+ map[string]plans.Resources{"txcd_10102001": {AwakeSeconds: hours(160)}, "txcd_10102000": {AwakeSeconds: hours(140)}}, nil},
+ "a lapse at 15:00 (the day excluded, as today)": {[]plans.CoveredInterval{iv(sep(1, 0), sep(16, 15)), iv(sep(17, 0), oct1)}, []plans.PlanSpan{span(sep(1, 0), sep(16, 15), plans.PlanPersonal), span(sep(17, 0), oct1, plans.PlanBusiness)},
+ map[string]plans.Resources{"txcd_10102001": {AwakeSeconds: hours(150)}, "txcd_10102000": {AwakeSeconds: hours(140)}}, nil},
+ "a change at the month's first instant": {whole, []plans.PlanSpan{span(sep(1, 0), oct1, plans.PlanBusiness)},
+ map[string]plans.Resources{"txcd_10102000": {AwakeSeconds: hours(300)}}, nil},
+ "two spans of one code aggregate": {whole, []plans.PlanSpan{span(sep(1, 0), sep(10, 0), plans.PlanPersonal), span(sep(10, 0), sep(20, 0), plans.PlanCommunity), span(sep(20, 0), oct1, plans.PlanBusiness)},
+ map[string]plans.Resources{"txcd_10102001": {AwakeSeconds: hours(190)}, "txcd_10102000": {AwakeSeconds: hours(110)}}, nil},
+ "an eligible day no span holds": {whole, []plans.PlanSpan{span(sep(2, 0), oct1, plans.PlanBusiness)}, nil, errBillingDaysMismatch},
+ "a plan with no code": {whole, []plans.PlanSpan{span(sep(1, 0), oct1, "mystery")}, nil, errBillingUnavailable},
+ } {
+ got, err := usageByCode(dailyStatement(), tc.intervals, tc.spans, taxSplitCodeOf)
+ if !errors.Is(err, tc.err) || (tc.err == nil && !reflect.DeepEqual(got, tc.want)) {
+ t.Errorf("%s: %+v %v", name, got, err)
+ }
+ }
+ // A fully covered month's Covered is nil by design: the whole month is
+ // eligible, not nothing.
+ if ivs, err := eligibleIntervals("2026-09", plans.MonthAllowance{}); err != nil || !reflect.DeepEqual(ivs, whole) {
+ t.Fatalf("a whole month: %+v %v", ivs, err)
+ }
+ if ivs, _ := eligibleIntervals("2026-09", plans.MonthAllowance{Partial: true, Covered: whole[:1]}); !reflect.DeepEqual(ivs, whole[:1]) {
+ t.Fatal("a partly covered month keeps its coverage")
+ }
+ // No days: nothing to attribute when nothing was used, a hold otherwise.
+ empty := usage.Statement{V: 1, Account: "bab", Month: "2026-09", Meters: map[string]float64{}, DaysInMonth: 30, Closed: true}
+ if got, err := usageByCode(empty, whole, []plans.PlanSpan{span(sep(1, 0), oct1, plans.PlanBusiness)}, taxSplitCodeOf); err != nil || len(got) != 0 {
+ t.Fatalf("no days and no usage: %+v %v", got, err)
+ }
+ empty.Meters[usage.MeterInstanceHours] = 5
+ if _, err := usageByCode(empty, whole, []plans.PlanSpan{span(sep(1, 0), oct1, plans.PlanBusiness)}, taxSplitCodeOf); !errors.Is(err, errBillingDays) {
+ t.Fatalf("no days with usage: %v", err)
+ }
+ // Malformed days hold as they do today.
+ bad := dailyStatement()
+ bad.Days["2026-10-01"] = map[string]float64{usage.MeterInstanceHours: 0}
+ if _, err := usageByCode(bad, whole, []plans.PlanSpan{span(sep(1, 0), oct1, plans.PlanBusiness)}, taxSplitCodeOf); !errors.Is(err, errBillingDaysMismatch) {
+ t.Fatalf("a date outside the month: %v", err)
+ }
+ bad = dailyStatement()
+ bad.Days["2026-09-03"][usage.MeterInstanceHours] = math.NaN()
+ if _, err := usageByCode(bad, whole, []plans.PlanSpan{span(sep(1, 0), oct1, plans.PlanBusiness)}, taxSplitCodeOf); !errors.Is(err, errBillingMeter) {
+ t.Fatalf("a value that is not a number: %v", err)
+ }
+ bad = dailyStatement()
+ bad.Meters[usage.MeterInstanceHours] = 299
+ if _, err := usageByCode(bad, whole, []plans.PlanSpan{span(sep(1, 0), oct1, plans.PlanBusiness)}, taxSplitCodeOf); !errors.Is(err, errBillingDaysMismatch) {
+ t.Fatalf("days that do not sum to the month: %v", err)
+ }
+}
+
+// The two transfer meters are floored to bytes each and then added, as the
+// month's own usage is: a fraction in each does not make a byte together.
+// Ordinary egress under one plan and store egress under the other land in
+// the one transfer axis.
+func TestTransferWeightsFloorEachMeterThenAdd(t *testing.T) {
+ oct1 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
+ tiny := math.Ldexp(1, -30) // 2^-30 GB: 0.93 bytes, exact in binary
+ st := usage.Statement{V: 1, Account: "bab", Month: "2026-09", Meters: map[string]float64{usage.MeterEgressGB: tiny + 1, usage.MeterStoreEgressGB: tiny}, DaysInMonth: 30, Closed: true,
+ Days: map[string]map[string]float64{"2026-09-01": {usage.MeterEgressGB: tiny, usage.MeterStoreEgressGB: tiny}, "2026-09-20": {usage.MeterEgressGB: 1}}}
+ got, err := usageByCode(st, []plans.CoveredInterval{{From: sep(1, 0), To: oct1}}, []plans.PlanSpan{span(sep(1, 0), sep(16, 0), plans.PlanPersonal), span(sep(16, 0), oct1, plans.PlanBusiness)}, taxSplitCodeOf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got["txcd_10102001"].TransferBytes != 0 || got["txcd_10102000"].TransferBytes != 1_000_000_000 {
+ t.Fatalf("transfer weights: %+v (two fractions of a byte must not make a byte)", got)
+ }
+}
+
+// An axis's amount is divided by weight: floors, then the largest
+// remainders, ties by the code's byte order; the shares sum to the amount
+// whatever order the weights were given in.
+func TestAnAxisIsApportionedWithoutRoundingBeyondIt(t *testing.T) {
+ for name, tc := range map[string]struct {
+ amount int64
+ weights map[string]int64
+ want []taxShare
+ err error
+ }{
+ "two thirds and a third": {1000, map[string]int64{"txcd_10102001": 576000, "txcd_10102000": 504000}, []taxShare{{"txcd_10102000", 467}, {"txcd_10102001", 533}}, nil},
+ "equal remainders, byte order": {3, map[string]int64{"txcd_b": 1, "txcd_a": 1}, []taxShare{{"txcd_a", 2}, {"txcd_b", 1}}, nil},
+ "all under one code": {7, map[string]int64{"txcd_a": 0, "txcd_b": 9}, []taxShare{{"txcd_b", 7}}, nil},
+ "a tiny share rounds to nothing": {1, map[string]int64{"txcd_a": 1, "txcd_b": 1_000_000}, []taxShare{{"txcd_b", 1}}, nil},
+ "nothing to divide": {0, map[string]int64{"txcd_a": 1}, nil, nil},
+ "an amount with no weight": {5, map[string]int64{"txcd_a": 0}, nil, errBillingTaxApportion},
+ "a negative weight": {5, map[string]int64{"txcd_a": -1}, nil, errBillingTaxApportion},
+ "weights that overflow": {5, map[string]int64{"txcd_a": math.MaxInt64, "txcd_b": 1}, nil, plans.ErrOverflow},
+ } {
+ got, err := apportion(tc.amount, tc.weights)
+ if !errors.Is(err, tc.err) || !reflect.DeepEqual(got, tc.want) {
+ t.Errorf("%s: %+v %v", name, got, err)
+ }
+ }
+ // The same shares whatever the map's insertion order.
+ a := map[string]int64{}
+ b := map[string]int64{}
+ for i := 0; i < 50; i++ {
+ a[fmt.Sprintf("txcd_%02d", i)] = int64(i + 1)
+ }
+ for i := 49; i >= 0; i-- {
+ b[fmt.Sprintf("txcd_%02d", i)] = int64(i + 1)
+ }
+ x, _ := apportion(12345, a)
+ y, _ := apportion(12345, b)
+ var sum int64
+ for _, s := range x {
+ sum += s.Amount
+ }
+ if !reflect.DeepEqual(x, y) || sum != 12345 {
+ t.Fatalf("order-dependent or not conserving: %v %v %d", x, y, sum)
+ }
+}
+
+// The two bodies behind the hold produce the two-code shape with the
+// subtotal unchanged: one line per (axis, code) under the split, every
+// line under one code for the month-end answer. Neither is called from
+// freeze.
+func TestTheTwoCodeBodiesKeepTheSubtotalAndTheShape(t *testing.T) {
+ lines := []plans.OverageLine{{Meter: "awake_seconds", AmountCents: 1000}, {Meter: "writes", AmountCents: 7}}
+ weights := map[string]plans.Resources{"txcd_10102001": {AwakeSeconds: 576000}, "txcd_10102000": {AwakeSeconds: 504000, Writes: 9}}
+ split, err := splitLinesByGrossUsage(lines, weights)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := []plans.OverageLine{{Meter: "awake_seconds", AmountCents: 467, TaxCode: "txcd_10102000"}, {Meter: "awake_seconds", AmountCents: 533, TaxCode: "txcd_10102001"}, {Meter: "writes", AmountCents: 7, TaxCode: "txcd_10102000"}}
+ if !reflect.DeepEqual(split, want) {
+ t.Fatalf("split: %+v", split)
+ }
+ last, err := lastSpansCodeLines(lines, "txcd_10102000")
+ if err != nil || !reflect.DeepEqual(last, []plans.OverageLine{{Meter: "awake_seconds", AmountCents: 1000, TaxCode: "txcd_10102000"}, {Meter: "writes", AmountCents: 7, TaxCode: "txcd_10102000"}}) {
+ t.Fatalf("last: %+v %v", last, err)
+ }
+ for _, body := range [][]plans.OverageLine{split, last} {
+ var sum int64
+ for _, l := range body {
+ sum += l.AmountCents
+ }
+ calc := billingCalculation{V: 2, Lines: body, Total: &plans.Invoice{TotalMinor: 1007}}
+ if sum != 1007 || !calc.taxShapeOK() {
+ t.Fatalf("a body: sum %d shape %v", sum, calc.taxShapeOK())
+ }
+ raw, _ := json.Marshal(calc)
+ var back billingCalculation
+ if json.Unmarshal(raw, &back) != nil || !reflect.DeepEqual(back.Lines, body) {
+ t.Fatalf("round trip: %s", raw)
+ }
+ }
+ // An axis with an amount and no weight holds rather than guesses; a
+ // line that already carries a code is not divided again.
+ if _, err := splitLinesByGrossUsage([]plans.OverageLine{{Meter: "storage_byte_months", AmountCents: 3}}, weights); !errors.Is(err, errBillingTaxApportion) {
+ t.Fatalf("no weight: %v", err)
+ }
+ if _, err := splitLinesByGrossUsage(want, weights); !errors.Is(err, errBillingTaxApportion) {
+ t.Fatalf("already tagged: %v", err)
+ }
+ if _, err := lastSpansCodeLines(lines, "txcd_"); !errors.Is(err, errBillingTaxApportion) {
+ t.Fatalf("a malformed code: %v", err)
+ }
+}