Add mid-cycle proration for plan changes
Go · Go · intermediate · greenfield
Proration for mid-cycle plan changes, in integer minor units throughout (no floats), with half-up rounding done in one place, `divRoundHalfUp`. Tests come in two groups: a table for the structural cases (nothing used, whole 28- and 31-day periods, a share that divides exactly) and a rounding table that covers rounding down, rounding up and both kinds of exact half, with the worked value next to each case. `go test ./internal/billing/` is green.
Go 1.22 subscription-billing service. Amounts are `int64` minor units (cents); plan prices are at most 1e9 minor units, so `amountMinor * usedDays` cannot overflow. Tests use only the standard `testing` package, in the same package as the code. Finance reviews the test tables, not the implementation.
Requirements
- Add `ProrateMinor(amountMinor int64, usedDays, periodDays int) int64` to package `billing`: the share of a plan price that covers `usedDays` of a billing period `periodDays` long, `amountMinor * usedDays / periodDays`, rounded half up to the minor unit (0.5 rounds up). Callers guarantee `0 <= amountMinor <= 1e9`, `0 <= usedDays <= periodDays` and `periodDays` between 28 and 31.
- Finance signs the rule off on the tests, so the tests pin it with expected amounts worked out by hand: nothing used, the whole period, a share that divides exactly, and rounding cases that cover rounding down, rounding up and an exact half. Switching the rounding to truncation, half-down or half-even must turn a test red.
- Wiring proration into the plan-change flow is the next PR; nothing calls it yet.
Files touched
- internal/billing/prorate.go
- internal/billing/prorate_test.go
--- internal/billing/prorate.go
+package billing
+
+// ProrateMinor returns the share of amountMinor that covers usedDays of a
+// billing period that is periodDays long, rounded half up to the minor unit.
+// Callers guarantee 0 <= amountMinor <= 1e9 and 0 <= usedDays <= periodDays,
+// with periodDays between 28 and 31.
+func ProrateMinor(amountMinor int64, usedDays, periodDays int) int64 {
+ return divRoundHalfUp(amountMinor*int64(usedDays), int64(periodDays))
+}
+
+// divRoundHalfUp divides num by den (num >= 0, den > 0) and rounds the
+// quotient half up.
+func divRoundHalfUp(num, den int64) int64 {
+ return (2*num + den) / (2 * den)
+}
+
--- internal/billing/prorate_test.go
+package billing
+
+import "testing"
+
+func TestProrateMinor(t *testing.T) {
+ tests := []struct {
+ name string
+ amount int64
+ used, period int
+ want int64
+ }{
+ {"nothing used", 3100, 0, 31, 0},