carlos / platform Public

Per-line tax codes, Task 4: the lifecycle of a two-code record and the older reader

A two-code calculation stored by hand (the seam; freeze still holds such a
month) invoices one line per code: two operations, two items each carrying
its code in tax_code and metadata, the draft validated against both, with
equal and unequal amounts. A lost answer to one line is recovered by the
listing, told from its sibling by its metadata code, paged or not, and
nothing is sent twice. A provider that drops the metadata code is caught
at the POST answer and at the draft (the fake has two knobs for it): the
month holds and nothing is finalized. The older binary's entry guard,
copied as it was, refuses the record and files the record-mismatch
review; that review holds the account for the new reader, the record's
bytes stay, the provider is not written; resolving it frees nothing while
an unrelated review is open, and the month completes once both are
resolved.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 24387f8f620bedbc3ece16072dc3a9c7604ff096 parent 00f70d0
2 files changed, +214 −7
  • internal/console/billinginvoice_test.go +11 −7
  • internal/console/billingtaxlifecycle_test.go +203 −0
diff --git a/internal/console/billinginvoice_test.go b/internal/console/billinginvoice_test.go
index afe7455fd..44813ab0c 100644
--- a/internal/console/billinginvoice_test.go
+++ b/internal/console/billinginvoice_test.go
@@ -44,9 +44,13 @@ type fakeInvoiceStripe struct {
dropRequest map[string]bool // do not apply, answer 502 once
refuse map[string]int // definitive HTTP status once
extraLine bool
- listDuplicate bool
- hasMoreLines bool
- seq int
+ // stripItemMetaTax drops metadata[tax_code] from the answer to a POST;
+ // stripLineMetaTax drops it from the listed lines: a provider that did
+ // not keep what it was sent.
+ stripItemMetaTax, stripLineMetaTax bool
+ listDuplicate bool
+ hasMoreLines bool
+ seq int
// The fake is in whatever currency each request named, as the
// provider is. currencies records what each kind of request named.
// The report* fields make it answer in another currency than it was
@@ -86,9 +90,9 @@ type fakeLine struct {
// lineMetadata is what the provider reports as a line's metadata: the
// meter and the digest always, the tax code when the request carried it.
-func (l fakeLine) lineMetadata() map[string]string {
+func (l fakeLine) lineMetadata(strip bool) map[string]string {
m := map[string]string{"meter": l.meter, "calculation_digest": l.digest}
- if l.metaTax != "" {
+ if l.metaTax != "" && !strip {
m["tax_code"] = l.metaTax
}
return m
@@ -134,7 +138,7 @@ func (f *fakeInvoiceStripe) invoiceJSON(inv *fakeInvoice, allLines bool) map[str
continue
}
lines = append(lines, map[string]any{"id": l.id, "invoice_item": l.item, "type": "invoiceitem", "amount": l.amount, "currency": either(f.reportLineCurrency, l.currency), "discountable": false,
- "metadata": l.lineMetadata(), "period": map[string]int64{"start": 1, "end": 2}})
+ "metadata": l.lineMetadata(f.stripLineMetaTax), "period": map[string]int64{"start": 1, "end": 2}})
}
if f.extraLine && inv.status == "draft" {
lines = append(lines, map[string]any{"id": "il_extra", "invoice_item": "ii_extra", "type": "invoiceitem", "amount": 100, "currency": inv.currency, "discountable": false, "metadata": map[string]string{}})
@@ -247,7 +251,7 @@ func (f *fakeInvoiceStripe) serve(w http.ResponseWriter, r *http.Request) {
line := fakeLine{currency: r.PostForm.Get("currency"), id: "il_" + strconv.Itoa(f.seq), item: "ii_" + strconv.Itoa(f.seq), meter: r.PostForm.Get("metadata[meter]"), digest: r.PostForm.Get("metadata[calculation_digest]"), amount: amount,
taxCode: r.PostForm.Get("tax_code"), metaTax: r.PostForm.Get("metadata[tax_code]")}
inv.lines = append(inv.lines, line)
- return map[string]any{"id": line.item, "invoice": inv.id, "customer": inv.customer, "livemode": false, "amount": amount, "currency": either(f.reportItemCurrency, line.currency), "discountable": r.PostForm.Get("discountable") != "false", "metadata": line.lineMetadata()}
+ return map[string]any{"id": line.item, "invoice": inv.id, "customer": inv.customer, "livemode": false, "amount": amount, "currency": either(f.reportItemCurrency, line.currency), "discountable": r.PostForm.Get("discountable") != "false", "metadata": line.lineMetadata(f.stripItemMetaTax)}
}, key)
case strings.HasSuffix(path, "/finalize"):
inv := f.invoices[strings.TrimSuffix(strings.TrimPrefix(path, "/v1/invoices/"), "/finalize")]
diff --git a/internal/console/billingtaxlifecycle_test.go b/internal/console/billingtaxlifecycle_test.go
new file mode 100644
index 000000000..ce3be802b
--- /dev/null
+++ b/internal/console/billingtaxlifecycle_test.go
@@ -0,0 +1,203 @@
+package console
+
+import (
+ "encoding/json"
+ "errors"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/plans"
+)
+
+// storeTwoCodeMonth freezes, by hand, the calculation a two-code month
+// would carry once decision 16 is taken: the fixture's own month, priced
+// as today, with its one awake line divided between two codes and the
+// calculation's own code empty. It is the seam the lifecycle tests use;
+// freeze itself still holds such a month.
+func storeTwoCodeMonth(t *testing.T, ix *invoiceFixture, personalShare func(total int64) int64) *billingCalculation {
+ t.Helper()
+ calc := ix.expectedCalc(t)
+ if len(calc.Lines) != 1 || calc.Lines[0].Meter != "awake_seconds" {
+ t.Fatalf("the fixture's lines: %+v", calc.Lines)
+ }
+ total := calc.Lines[0].AmountCents
+ personal := personalShare(total)
+ calc.TaxCode = ""
+ calc.Lines = []plans.OverageLine{
+ {Meter: "awake_seconds", AmountCents: total - personal, TaxCode: "txcd_business"},
+ {Meter: "awake_seconds", AmountCents: personal, TaxCode: "txcd_personal"},
+ }
+ if !calc.taxShapeOK() {
+ t.Fatal("the hand-made record is not in the two-code shape")
+ }
+ raw, err := json.Marshal(calc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := ix.st.freezeBillingMonth(ix.acct.ID, invoiceFixtureMonth, raw); err != nil {
+ t.Fatal(err)
+ }
+ return calc
+}
+
+func taxLifecycleFixture(t *testing.T) *invoiceFixture {
+ t.Helper()
+ ix := newInvoiceFixture(t)
+ ix.srv.billing.cfg.TaxCode = ""
+ ix.srv.billing.cfg.TaxCodes = map[string]string{plans.PlanPersonal: "txcd_personal", plans.PlanCommunity: "txcd_personal", plans.PlanBusiness: "txcd_business"}
+ return ix
+}
+
+// Two lines of one meter are two operations, two items, each carrying its
+// own code in tax_code and in metadata, and the draft validates against
+// both; with equal and with unequal amounts.
+func TestATwoCodeMonthInvoicesOneLinePerCode(t *testing.T) {
+ for name, share := range map[string]func(int64) int64{
+ "unequal amounts": func(total int64) int64 { return total * 6 / 10 },
+ "equal amounts": func(total int64) int64 { return total / 2 },
+ } {
+ t.Run(name, func(t *testing.T) {
+ ix := taxLifecycleFixture(t)
+ calc := storeTwoCodeMonth(t, ix, share)
+ if err := ix.run(t); err != nil {
+ t.Fatalf("run: %v", err)
+ }
+ if rec := ix.month(t); rec.State != "complete" {
+ t.Fatalf("month: %+v", rec)
+ }
+ ops := ix.ops(t)
+ for _, line := range calc.Lines {
+ op, ok := ops[lineKind(line)]
+ if !ok || op.State != billingOpAcked {
+ t.Fatalf("operation %s: %+v", lineKind(line), op)
+ }
+ }
+ if _, dup := ops["line-awake_seconds"]; dup {
+ t.Fatal("an operation by meter alone, beside the two by code")
+ }
+ ix.assertMutations(t, map[string]int{"draft": 1, "line-awake_seconds-txcd_business": 1, "line-awake_seconds-txcd_personal": 1, "finalize": 1, "pay": 1})
+ inv := ix.fake.invoices[ix.fake.order[0]]
+ if len(inv.lines) != 2 {
+ t.Fatalf("lines at the provider: %+v", inv.lines)
+ }
+ for _, l := range inv.lines {
+ if l.taxCode != l.metaTax || (l.metaTax != "txcd_business" && l.metaTax != "txcd_personal") {
+ t.Fatalf("a line's codes: %+v", l)
+ }
+ }
+ })
+ }
+}
+
+// A lost answer to one of the two lines is recovered by the listing, the
+// line told from its sibling by its metadata code, through a paginated
+// listing too; nothing is sent twice.
+func TestATwoCodeLinesLostAnswerIsRecoveredByItsCode(t *testing.T) {
+ for name, paged := range map[string]bool{"one page": false, "paged listing": true} {
+ t.Run(name, func(t *testing.T) {
+ ix := taxLifecycleFixture(t)
+ storeTwoCodeMonth(t, ix, func(total int64) int64 { return total / 2 })
+ ix.fake.hasMoreLines = paged
+ ix.fake.loseResponse["line-awake_seconds-txcd_personal"] = true
+ if err := ix.run(t); !errors.Is(err, errBillingPending) {
+ t.Fatalf("lost line answer: %v", err)
+ }
+ ix.now = ix.now.Add(2 * time.Hour)
+ if err := ix.run(t); err != nil {
+ t.Fatalf("recovery: %v", err)
+ }
+ if rec := ix.month(t); rec.State != "complete" {
+ t.Fatalf("month: %+v", rec)
+ }
+ ix.assertMutations(t, map[string]int{"draft": 1, "line-awake_seconds-txcd_business": 1, "line-awake_seconds-txcd_personal": 1, "finalize": 1, "pay": 1})
+ })
+ }
+}
+
+// A provider that drops the metadata code is caught at the answer to the
+// POST and again at the draft, equal amounts included: the line cannot be
+// told from its sibling, so the month holds for a person and nothing is
+// finalized.
+func TestAMissingMetadataCodeHoldsTheMonth(t *testing.T) {
+ for name, setup := range map[string]func(*fakeInvoiceStripe){
+ "on the answer to the POST": func(f *fakeInvoiceStripe) { f.stripItemMetaTax = true },
+ "on the listed lines": func(f *fakeInvoiceStripe) { f.stripLineMetaTax = true },
+ } {
+ t.Run(name, func(t *testing.T) {
+ ix := taxLifecycleFixture(t)
+ storeTwoCodeMonth(t, ix, func(total int64) int64 { return total / 2 })
+ setup(ix.fake)
+ err := ix.run(t)
+ if !errors.Is(err, errBillingInvoiceHeld) {
+ t.Fatalf("run: %v", err)
+ }
+ reviews := ix.reviews(t)
+ if len(reviews) != 1 || !strings.HasSuffix(reviews[0], ":"+invoiceFixtureMonth) || !strings.Contains(reviews[0], "mismatch") {
+ t.Fatalf("reviews: %v", reviews)
+ }
+ _, mutations, _ := ix.fake.counts()
+ if mutations["finalize"] != 0 || mutations["pay"] != 0 {
+ t.Fatalf("finalized or paid under a hold: %v", mutations)
+ }
+ })
+ }
+}
+
+// An older binary's entry guard, copied as it was before lines carried
+// codes (29ea6676: wellFormed and a top-level txcd_ check), refuses a
+// two-code record and files the record-mismatch review; that review
+// blocks the new reader until it is resolved, and an unrelated review
+// keeps the month held after that. The record's bytes are untouched and
+// nothing reaches the provider meanwhile.
+func TestAnOlderReaderRefusesATwoCodeRecordAndHoldsTheAccount(t *testing.T) {
+ olderGuardAccepts := func(c *billingCalculation) bool {
+ return c.wellFormed() && strings.HasPrefix(c.TaxCode, "txcd_") && len(c.TaxCode) > 5
+ }
+ ix := taxLifecycleFixture(t)
+ oneCode := ix.expectedCalc(t)
+ oneCode.TaxCode = "txcd_personal"
+ if !olderGuardAccepts(oneCode) {
+ t.Fatal("the older guard refuses a one-code record")
+ }
+ storeTwoCodeMonth(t, ix, func(total int64) int64 { return total / 2 })
+ before := ix.month(t)
+ var frozen billingCalculation
+ if err := json.Unmarshal(before.Calculation, &frozen); err != nil || olderGuardAccepts(&frozen) {
+ t.Fatalf("the older guard accepts a two-code record: %v", err)
+ }
+ if err := ix.st.SetOperator("paul@keymail.dev", true); err != nil {
+ t.Fatal(err)
+ }
+ // What the older binary then does: the review, by the run's own filing.
+ if err := ix.st.fileBillingInvoiceReview(ix.acct.ID, "sub_hist", "billing_invoice_record_mismatch:"+invoiceFixtureMonth, ix.now); err != nil {
+ t.Fatal(err)
+ }
+ if err := ix.st.fileBillingInvoiceReview(ix.acct.ID, "sub_hist", "billing_invoice_tax_code_mixed:2026-08", ix.now); err != nil {
+ t.Fatal(err)
+ }
+ if err := ix.run(t); !errors.Is(err, errBillingReview) {
+ t.Fatalf("the new reader under the older one's review: %v", err)
+ }
+ if _, mutations, _ := ix.fake.counts(); len(mutations) != 0 {
+ t.Fatalf("the provider was written under a hold: %v", mutations)
+ }
+ if after := ix.month(t); string(after.Calculation) != string(before.Calculation) || after.Digest != before.Digest {
+ t.Fatal("the frozen record changed")
+ }
+ if err := ix.st.resolveBillingReview(billingReviewResolution{AccountID: ix.acct.ID, Subscription: "sub_hist", Reason: "billing_invoice_record_mismatch:" + invoiceFixtureMonth, Occurrence: 1, Actor: "paul@keymail.dev", Note: "older_build"}, ix.now); err != nil {
+ t.Fatal(err)
+ }
+ if err := ix.run(t); !errors.Is(err, errBillingReview) {
+ t.Fatalf("with an unrelated review still open: %v", err)
+ }
+ if err := ix.st.resolveBillingReview(billingReviewResolution{AccountID: ix.acct.ID, Subscription: "sub_hist", Reason: "billing_invoice_tax_code_mixed:2026-08", Occurrence: 1, Actor: "paul@keymail.dev", Note: "decided"}, ix.now); err != nil {
+ t.Fatal(err)
+ }
+ if err := ix.run(t); err != nil {
+ t.Fatalf("after both reviews: %v", err)
+ }
+ if rec := ix.month(t); rec.State != "complete" {
+ t.Fatalf("month: %+v", rec)
+ }
+}