Design: per-line tax codes for a month in which the plan changed (OUTSTANDING 17; the mechanics either answer to decision 16 needs, the hold kept until the answer)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+55
−0
docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md+55 −0
CI passed — run details
diff --git a/docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md b/docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md| new file mode 100644 |
| index 000000000..4ce5f94e3 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md |
| @@ -0,0 +1,55 @@ |
| +# Per-line tax codes for a month in which the plan changed |
| + |
| +Date: 2026-10-03. Status: design, revision 1, for review. Answers `OUTSTANDING.md` item 17 and prepares decision 16 (`DECISIONS-FOR-PAUL.md`), which is Paul's; nothing here chooses it. Branch `design-per-line-tax-codes`, off the launch branch at `29ea6676`. |
| + |
| +## What happens today |
| + |
| +An account's month is invoiced from one frozen calculation (`billingCalculation`, `internal/console/billingcalculation.go`): one overage line per billed meter (`plans.InvoiceOverageLines`, `OverageInvoiceFor`), and one product tax code for the whole calculation (`TaxCode`), taken from the plan of each paid assignment that covered the month (`BillingConfig.taxCode(plan)`). Personal and Community share `txcd_10102001` (software, personal use) and Business has `txcd_10102000` (software, business use); these are the approved codes and stay as they are. When the month's covering assignments carry two codes (a change to or from Business inside the month), `freeze` returns `errBillingTaxCodeMixed` and the month holds with the review `billing_invoice_tax_code_mixed`, for a person. Nothing is invoiced for it until a person acts, and today a person has no door that invoices it either. |
| + |
| +Two facts make this a tax-code question and nothing else: |
| + |
| +- allowances are already time-weighted across the paid spans (`plans.AllowanceForMonth` weights each assignment's allowance by its paid interval over the month), so the money of a month with a plan change is already right for the time each plan was in force; |
| +- overage rates and the floor are per currency, not per plan (`PriceTerms.overageRates`; `OverageFor` takes the plan only to check it exists), so the amount of each meter's overage line does not depend on which plan was in force when the usage happened. |
| + |
| +So the only thing missing is which tax code each overage line carries. That is decision 16, with two answers on the table: split each meter's line by span (one line per tax code, the usage of each UTC day attributed to the plan in force at that day's start) or tax the whole month by the plan in force at the month's end. Both keep every amount as it is today. |
| + |
| +## What this design builds, and what it leaves to the decision |
| + |
| +It builds the mechanics either answer needs, with the hold left exactly where it is until the answer comes, and every month that has one tax code frozen byte for byte as it is today. |
| + |
| +1. **A line carries its own tax code.** `plans.OverageLine` gains `TaxCode string \`json:"tax_code,omitempty"\``. For a month with one code the field is empty on every line and the calculation's `tax_code` carries it, as now: the frozen JSON is byte-identical, so every digest already recorded stays valid. For a month with two codes the calculation's `tax_code` is empty and every line carries one. `wellFormed` requires exactly one of the two shapes: a non-empty `tax_code` with no line carrying one, or an empty `tax_code` with every line carrying one. `lineParams` reads the line's code first and the calculation's second, and refuses a line with neither. |
| + |
| +2. **The allowance names its spans.** `plans.MonthAllowance` gains `Spans []PlanSpan \`json:"spans,omitempty"\`` with `From`, `To` and `Plan`, one per paid assignment interval the allowance counted (the intervals `AllowanceForMonth` already computes as `lo` and `paidHi`, before adjacent ones are joined into `Covered`). It is written only when the spans carry more than one tax code (the console decides that; the plans package does not know tax codes, so the console asks for the spans and drops them when they all map to one code), so a single-code month's JSON is unchanged. |
| + |
| +3. **A day's usage is attributed to a span.** A console function takes the statement's `Days` (the five billed meters by UTC day; present on every statement written since the field existed, absent on older ones), the spans, and a rule, and returns each meter's usage per span. The rule this design proposes, for decision 16's split answer, is the one named there: a UTC day's usage follows the plan in force at the day's start (00:00 UTC); a day no span covers is not attributed (its usage is not billed, as today for a partly covered month). A statement without `Days` cannot be split: such a month holds as today. |
| + |
| +4. **A meter's line is apportioned, not re-priced.** Under the split answer each meter's overage amount, computed for the whole month exactly as today, is divided among the meter's tax codes in proportion to the meter's attributed usage per code, in micros, with the whole minor units handed out by the same largest-remainder rule `overageLines` uses, so the lines of one meter sum to the meter's amount and the invoice total is unchanged. A meter whose usage all fell under one code has one line. Under the month-end answer every line carries the last span's code and nothing is divided. |
| + |
| +5. **The hold stays until the decision.** `freeze` keeps returning `errBillingTaxCodeMixed` for a two-code month. The decision is applied by one function, `taxCodesForLines`, which today returns the error for two codes; the split and the month-end answers are two bodies for it, both written and tested behind the error so the chosen one is a one-line switch and a review, not a build. |
| + |
| +Nothing in this touches the price book, the plans' amounts, the retirement protocol, the ledger or the provider calls, other than the tax code on an invoice item which already exists per item. |
| + |
| +## Compatibility |
| + |
| +- An older binary reading a two-code calculation written by this one sees an empty `tax_code` and refuses it in `wellFormed` and `lineParams`: it does not invoice it and it does not corrupt it. Roll order is therefore free. |
| +- A calculation frozen before this change is read exactly as before: its `tax_code` is set, its lines carry none. |
| +- The barrier trigger on `billing_months` reads `$.v` and is not touched; the calculation's version stays 2. |
| +- Nothing is backfilled: a month already held for two codes is frozen when the decision's function first prices it, from the same statement and history, and its review is resolved by that invoice run as any hold is. |
| + |
| +## Tests |
| + |
| +- A single-plan month's frozen JSON and digest are byte-identical before and after (a recorded calculation from the test fixtures compared as bytes). |
| +- A two-code month still holds, with the same review, and its calculation is not frozen. |
| +- The attribution: a change at 00:00 UTC, at 15:00 UTC (the day follows the plan at its start), a day no span covers, a statement without `Days`. |
| +- The apportionment: the lines of each meter sum to the meter's amount, the invoice total is unchanged, a meter with usage under one code only has one line, the largest-remainder hand-out, zero-usage meters. |
| +- `wellFormed` and `lineParams` refuse the mixed shapes (a code at both levels; a code at neither; a two-code calculation with a line missing its code). |
| +- An older binary's view, by a test that strips the line codes from a two-code calculation and shows it is refused rather than invoiced. |
| +- Mutants in a tree copy, each with its killing test named. |
| + |
| +## Not in this design |
| + |
| +Which answer decision 16 takes; any change to amounts; a door that invoices a held month by hand; the box side (statements already carry `Days`); copy on the invoice line (the description stays input-bound until the copy review). |
| + |
| +## Questions for the reviewer |
| + |
| +A. Is there a third answer to decision 16 that the mechanics above would not serve? B. Is the apportionment in proportion to attributed usage the right split of an unchanged amount, or would a per-span overage (each span priced against its own pro-rated allowance) be the honest reading of "the plan in force", and if so what does it do to the floor and to a span with no overage of its own? C. The change day: is "the plan in force at the day's start" defensible, given the box aggregates usage by UTC day? D. Is leaving the frozen JSON byte-identical for single-code months worth the two shapes, or should every calculation carry per-line codes from now on and the digest rule be stated for the two generations? E. Anything an older binary could do with a two-code calculation other than refuse it. |