carlos / platform Public

Plan: per-line tax codes, the mechanics (seven tasks; Task 0 pins today's bytes before any change)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e4d2110e58ae660d5c7c366c50a1e20f46fa53bc parent 1db5bcf
1 file changed, +49 −0
  • docs/superpowers/plans/2026-10-03-per-line-tax-codes-plan.md +49 −0

CI cancelled — superseded by a newer push — run details

diff --git a/docs/superpowers/plans/2026-10-03-per-line-tax-codes-plan.md b/docs/superpowers/plans/2026-10-03-per-line-tax-codes-plan.md
new file mode 100644
index 000000000..433a5e4cb
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-03-per-line-tax-codes-plan.md
@@ -0,0 +1,49 @@
+# Plan: per-line tax codes, the mechanics (design revision 3)
+
+Date: 2026-10-03. Builds `docs/superpowers/specs/2026-10-03-per-line-tax-codes-design.md` on the branch `design-per-line-tax-codes` (renamed in spirit: the design and its build share the branch). Every task is test first, keeps the hold (`errBillingTaxCodeMixed`) in place, and leaves every one-code calculation, request and operation byte-identical. Decision 16 and 16a to 16d are Paul's; nothing here activates anything.
+
+## Task 0. Pin today's bytes before any change
+
+In `internal/console` and `internal/plans`, a test file `billingtaxshape_fixtures_test.go` with literal JSON of today's frozen calculations and their digests, request hashes and operation kinds for: a legacy version 1 month; a price-book version 2 month with one plan; a one-code month with two plans (Personal to Community); a quantity change inside a month; a partly covered month; a waived month; a fully covered one-code month with non-zero usage and no `Days`. The literals are produced once by a throwaway test on the unchanged code and committed as text, never regenerated. Tests assert that pricing those inputs reproduces the literal bytes. This task changes no production code and is its own commit.
+
+## Task 1. The two fields, in `internal/plans`
+
+- `OverageLine.TaxCode string \`json:"tax_code,omitempty"\``; `Meter` and `AmountCents` keep their untagged names.
+- `PlanSpan{From, To time.Time; Plan string}` and `MonthAllowance.Spans []PlanSpan \`json:"spans,omitempty"\``.
+- `AllowanceForMonth` returns the unmerged paid intervals, half-open, one per counted assignment, in `Spans`; a new exported helper `(MonthAllowance).WithoutSpans()` (or the console simply clears the field) so the console drops them for a one-code month.
+- Tests: Task 0's fixtures still byte-identical when `Spans` is cleared and no line carries a code; spans for a continuous change, a quantity change (two spans, same plan), a lapse, a change at the month's first instant.
+
+## Task 2. The shape rule and the line identity, in `internal/console`
+
+- `wellFormed`: the tax shape (one-code / two-code / waived branch; `txcd_` syntax wherever a code appears; a non-waived calculation with no lines refused).
+- The entry guard keeps account, month and `wellFormed`; drops the separate top-level predicate.
+- `lineParams(line)`: the line's code or the calculation's; refuse both or neither.
+- `billingstripe.InvoiceItemParams.TaxCodeMetadata string \`json:",omitempty"\`` sent as `metadata[tax_code]` when set; `InvoiceItem`/`InvoiceLine` metadata already decoded.
+- Operation kind: `line-<meter>` for a line without a code, `line-<meter>-<code>` for one with; the POST check, the recovery match and the draft validation require `metadata[tax_code]` equal to the frozen line's for a line that carries one.
+- Tests: Task 0's request hashes and kinds unchanged; the shape refusals (both levels, neither, one line lacking, malformed code, non-waived with no lines, waived branch accepted); the fake provider stores and returns `tax_code` and `metadata[tax_code]`; corruption of the metadata in the POST answer and in the draft listing each refused; equal-amount lines distinguished.
+
+## Task 3. Attribution and apportionment, behind the hold
+
+- `eligibleDays(statement, allowance)`: today's rule for a partly covered month (`billingUsageOnPaidDays` unchanged), the whole month as the one interval when `Covered` is nil.
+- `usageByCode(statement, spans, codeOf func(plan) string, rule)`: per (code, statement meter) float sums over eligible days in date order (16a's proposal as the one rule body: the span containing 00:00 UTC of the day); one `billingBaseUnits` per sum; transfer meters added per code with the overflow check; returns the four axes' weights per code. `Days` absent: waived when every billed meter is zero, else `errBillingDays`.
+- `apportion(amountMinor, weights map[code]int64) []share`: floor shares, largest remainder, byte-order ties, zero shares omitted, zero weight sum with a positive amount an error.
+- `taxCodesForLines(...)`: returns `errBillingTaxCodeMixed` for two codes (today's behaviour); two unexported bodies, `splitByGrossUsage` and `lastSpansCode`, exist and are tested but are not called from `freeze`.
+- Tests: the attribution and apportionment lists in the design, including the fractional transfer boundary with exact shares, the shuffled-map determinism, and the end-to-end two-code calculation produced through a test seam (not through `freeze`), with its JSON shape, digest and lines.
+
+## Task 4. The lifecycle and the older reader
+
+- A test seam lets a test freeze a two-code calculation (the body chosen explicitly); the invoice run then creates two lines for one axis, with equal and unequal amounts; in-window replay and expired-window recovery for each split line, a paginated draft listing; a one-code month's journal unchanged.
+- `billingoldreader_test.go`: the old `wellFormed` and entry guard copied verbatim with a note of the commit they come from; a two-code record makes the old flow file `billing_invoice_record_mismatch`, write nothing to the provider and leave the bytes unchanged; the new reader blocked until the review is resolved; an unrelated second review keeps the month held after that.
+- A mixed history that is waived, and one emitting one code only, through both readers.
+
+## Task 5. Records, at the merge
+
+On the launch branch: `ENABLEMENT.md` gains the activation rule (older invoice workers stopped before `taxCodesForLines` is switched; no rollback past activation; the generic rollback note does not apply after it); `OUTSTANDING.md` item 17 points at the build and the hold; `READINESS.md` names the branch. The design's "Not in this design" stays true.
+
+## Task 6. Mutants and dispositions
+
+In a git-less tree copy: the shape rule's each clause; the three metadata checks; the `omitempty` on each new field (a one-code fixture changes bytes); the floor-before-add order; the tie order; the zero-weight hold; the eligibility-before-attribution order; the whole-month interval. Each with its killing test logged. Dispositions per review round in `DISPOSITIONS-per-line-tax-codes.md`.
+
+## Reviews
+
+Each task's commit gets the full `internal/console` and `internal/plans` run (`-timeout 30m`, `GOFLAGS=-buildvcs=false`); the branch gets the gate alone on its exact tip, Opus (AGENTS' production reviewer) and Astra on the whole branch, rounds until both accept; the merge into the launch branch is a merge commit after that and CI.