carlos / platform Public

WIP: pin what the void does not do

The void of a wrong-total invoice is remediation. A finalized invoice is open,
and payable from the hosted page, from the finalize until the void lands. Each
limit has a test that first asserts the invoice is open: the process dying
between the two calls, the customer paying in the window (held as paid with a
wrong total, the month not completed), the void timing out, its answer lost,
and the provider refusing it. The plan says so, and that invoice writes stay
disabled until the prevention is proven.

Not gated, not reviewed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 57375a708901079f52bcffd320e9a89f6ba015d8 parent c22e309
3 files changed, +252 −21
  • docs/superpowers/plans/2026-10-01-late-request-fencing.md +18 −2
  • internal/console/billinginvoice.go +52 −19
  • internal/console/billinginvoice_test.go +182 −0
diff --git a/docs/superpowers/plans/2026-10-01-late-request-fencing.md b/docs/superpowers/plans/2026-10-01-late-request-fencing.md
index 0d122db5e..d3a11b679 100644
--- a/docs/superpowers/plans/2026-10-01-late-request-fencing.md
+++ b/docs/superpowers/plans/2026-10-01-late-request-fencing.md
@@ -66,12 +66,28 @@ If `T_idle` turns out shorter than the lease margin (five minutes), the key fenc
- The transport: one connection per request, established before the last check, no reuse, no keepalive, no retry, HTTP/1.1, no proxy. The last check reads the caller's send deadline and the context's deadline on the wall clock, so a suspended sender wakes to a closed window without any caller asking for it. Tests against a server that closes idle connections: a held request is never processed and never re-sent; a request past its deadline is never written; every request has its own connection (`internal/billingstripe/transport.go`, `transport_test.go`).
- The quantity sender passes its lease's margin as the send deadline.
-- The invoice engine voids an open invoice whose subtotal is not the frozen total, in the visit that finds it (`finalizedOrVoided`), and records `billing_invoice_voided_wrong_total`. Test: a duplicate line lands between the line check and the finalize; the invoice ends void, never paid.
+- The invoice engine voids an open invoice whose subtotal is not the frozen total, in the visit that finds it (`finalizedOrVoided`). **This is remediation, not prevention, and it does not close the defect.** See "What the void does not do" below.
- The sandbox test measures the idle limit: a request held six minutes (configurable) on an established connection must not be processed.
Not built: the fake provider forgetting keys after 24 hours in the quantity simulation, with holds longer than that; a review of this branch.
-What this does not yet prove, stated plainly: the provider's idle limit is unmeasured. Until the sandbox run measures it, the argument above is a design with one untested premise, and both paths stay disabled. The void narrows the invoice exposure to the seconds between a wrong finalize and the void in the same visit; it does not remove it by itself, which is why the transport is the fix and the void is the net.
+What this does not yet prove, stated plainly: the provider's idle limit is unmeasured. Until the sandbox run measures it, the argument above is a design with one untested premise, and both paths stay disabled. Invoice writes stay off (billing is off) until the prevention is proven, not until the void exists.
+
+### What the void does not do
+
+A wrong-total invoice that has been finalized is open at the provider from that instant, and an open invoice can be paid by the customer from the hosted page or the portal. The void does not change that it was payable. It shortens how long. Each limit below is pinned by a test that first asserts the invoice is open, so that nobody reads a passing test as "never payable":
+
+| Case | How long the wrong invoice is payable | What the engine does | Test |
+| --- | --- | --- | --- |
+| The void follows the finalize in the same visit | one round trip | voids, records `billing_invoice_voided_wrong_total`, never charges | `TestInvoiceEngineVoidsAFinalizedInvoiceWithAWrongTotal` |
+| The process dies between the finalize and the void | until the next visit to that account | voids then | `TestInvoiceWrongTotalIsPayableUntilTheNextVisitWhenTheProcessDiesBeforeTheVoid` |
+| The customer pays in that window | it was paid | cannot void; records `billing_invoice_paid_wrong_total`; the month is not marked complete; a person refunds | `TestInvoiceWrongTotalPaidByTheCustomerBeforeTheVoidIsHeldNotCompleted` |
+| The void request fails or times out | until a later visit's void lands | retries the void on the next visit | `TestInvoiceWrongTotalWhenTheVoidFails`, first two cases |
+| The provider refuses the void | until a person acts | records a review, never charges, month not complete | `TestInvoiceWrongTotalWhenTheVoidFails`, third case |
+
+So after the void there is still customer-payable exposure in four of five cases, and a customer who pays is owed a refund. Only prevention closes the defect: no line may land on the draft between the line check and the finalize. That rests on the transport and on the provider's unmeasured idle limit.
+
+Not reviewed. This branch has had no independent review at all.
## What stays true
diff --git a/internal/console/billinginvoice.go b/internal/console/billinginvoice.go
index 0e76b47ca..4095910d2 100644
--- a/internal/console/billinginvoice.go
+++ b/internal/console/billinginvoice.go
@@ -1097,20 +1097,29 @@ func (r *billingInvoiceRun) recoverFinalize(ctx context.Context, op billingOpera
}
// finalizedOrVoided verifies an invoice the provider reports as finalized.
-// One failure gets more than a review: an OPEN invoice, the one this month
-// is bound to, whose subtotal is not the frozen total. Finalized, it is
-// payable by the customer from the provider's hosted page whatever this
-// engine decides, and its amount is wrong. It is voided here, in the pass
-// that found it, so it is not left payable while it waits for a person.
+// One failure gets more than a review: the invoice this month is bound to,
+// finalized with a subtotal that is not the frozen total.
//
-// The lines are checked against the frozen calculation immediately before
-// the finalize, so this state needs a line to land on the draft between
-// that check and the finalize. The transport is built so that cannot
-// happen (billingstripe, transport.go). This is the net under that.
+// This is remediation. It is not prevention, and it does not make the
+// wrong invoice one that was never payable. From the instant the provider
+// finalized it until the void lands, the invoice is open, and an open
+// invoice can be paid by the customer from the provider's hosted page
+// whatever this engine decides. That window is at least one round trip. It
+// is as long as this process takes to come back if it dies between the two
+// calls, and as long as the provider keeps failing or refusing the void.
+// What prevents the wrong finalize in the first place is that no line can
+// land on the draft between the line check and the finalize, which rests
+// on the transport (billingstripe, transport.go) and on a provider
+// behaviour that has not been measured. Until it has, invoice writes stay
+// disabled.
//
-// A void that fails in a way that may succeed later leaves the finalize
-// unacknowledged, and the next visit comes back here. A void the provider
-// refuses outright is reported as the open invoice it still is, and held.
+// What this does: voids an OPEN wrong-total invoice in the visit that
+// finds it. If the customer has already paid it, it cannot be voided: that
+// is reported as paid with a wrong total and held for a person (a refund),
+// and the month is not recorded as complete. A void that fails in a way
+// that may succeed later leaves the finalize unacknowledged, and the next
+// visit comes back here. A void the provider refuses outright is followed
+// by a read, so what is recorded is what the invoice is now.
func (r *billingInvoiceRun) finalizedOrVoided(ctx context.Context, inv *billingstripe.Invoice) (string, string, error) {
verr := r.verifyFinalized(inv)
if verr == nil {
@@ -1119,20 +1128,40 @@ func (r *billingInvoiceRun) finalizedOrVoided(ctx context.Context, inv *billings
if inv == nil {
return "", "", verr
}
- if inv.Status != "open" || inv.ID != r.rec.InvoiceID || inv.Customer != r.rec.Customer || inv.Subtotal == r.calc.Invoice.TotalCents {
+ if inv.ID != r.rec.InvoiceID || inv.Customer != r.rec.Customer || inv.Subtotal == r.calc.Invoice.TotalCents {
+ return inv.ID, inv.Status, verr
+ }
+ switch inv.Status {
+ case "paid":
+ return inv.ID, billingInvoicePaidWrongTotal, verr
+ case "open":
+ default:
return inv.ID, inv.Status, verr
}
voided, err := r.b.client.VoidInvoice(ctx, inv.ID, r.operationKey("void", 1))
- if err != nil {
- var stripeErr *billingstripe.Error
- if errors.As(err, &stripeErr) && definitiveRefusal(stripeErr) {
- return inv.ID, inv.Status, verr
- }
+ if err == nil {
+ return voided.ID, voided.Status, verr
+ }
+ var stripeErr *billingstripe.Error
+ if !errors.As(err, &stripeErr) || !definitiveRefusal(stripeErr) {
return "", "", err
}
- return voided.ID, voided.Status, verr
+ // Refused outright. The invoice may have been paid or voided since the
+ // answer this function was given: read it, and report what it is.
+ now, gerr := r.b.client.GetInvoice(ctx, inv.ID)
+ if gerr != nil {
+ return "", "", gerr
+ }
+ if now.Status == "paid" {
+ return now.ID, billingInvoicePaidWrongTotal, verr
+ }
+ return now.ID, now.Status, verr
}
+// billingInvoicePaidWrongTotal is the outcome recorded for a finalize whose
+// invoice was paid with a subtotal that is not the frozen total.
+const billingInvoicePaidWrongTotal = "paid_wrong_total"
+
func (r *billingInvoiceRun) ackFinalized(_, outcome string) (string, string, string) {
switch outcome {
case "open":
@@ -1143,6 +1172,10 @@ func (r *billingInvoiceRun) ackFinalized(_, outcome string) (string, string, str
// Voided by finalizedOrVoided: the month stays where it is, bound
// to an invoice nobody can pay, until a person has looked.
return "", "", "billing_invoice_voided_wrong_total"
+ case billingInvoicePaidWrongTotal:
+ // The customer paid an invoice whose total was wrong. The month is
+ // not complete: a person has to refund and reissue.
+ return "", "", "billing_invoice_paid_wrong_total"
}
return "", "", "billing_invoice_mismatch"
}
diff --git a/internal/console/billinginvoice_test.go b/internal/console/billinginvoice_test.go
index d026ab869..5d14bbd55 100644
--- a/internal/console/billinginvoice_test.go
+++ b/internal/console/billinginvoice_test.go
@@ -1639,3 +1639,185 @@ func TestInvoiceEngineVoidsAFinalizedInvoiceWithAWrongTotal(t *testing.T) {
t.Fatalf("after later visits, mutations %v", mutations)
}
}
+
+// wrongTotalFixture is an invoice run in which a duplicate line lands on
+// the draft between the engine's line check and its finalize.
+func wrongTotalFixture(t *testing.T) *invoiceFixture {
+ t.Helper()
+ ix := newInvoiceFixture(t)
+ landed := false
+ ix.srv.billing.invoice.hooks = &billingInvoiceHooks{beforePost: func(kind string) error {
+ if kind != "finalize" || landed {
+ return nil
+ }
+ landed = true
+ ix.fake.mu.Lock()
+ defer ix.fake.mu.Unlock()
+ for _, inv := range ix.fake.invoices {
+ if inv.status == "draft" && len(inv.lines) > 0 {
+ dup := inv.lines[0]
+ dup.id, dup.item = dup.id+"_late", dup.item+"_late"
+ inv.lines = append(inv.lines, dup)
+ }
+ }
+ return nil
+ }}
+ return ix
+}
+
+func (ix *invoiceFixture) invoiceStatus(t *testing.T) string {
+ t.Helper()
+ ix.fake.mu.Lock()
+ defer ix.fake.mu.Unlock()
+ if len(ix.fake.invoices) != 1 {
+ t.Fatalf("%d invoices at the provider, want one", len(ix.fake.invoices))
+ }
+ for _, inv := range ix.fake.invoices {
+ return inv.status
+ }
+ return ""
+}
+
+func (ix *invoiceFixture) hasReview(t *testing.T, prefix string) bool {
+ t.Helper()
+ for _, reason := range ix.reviews(t) {
+ if strings.HasPrefix(reason, prefix) {
+ return true
+ }
+ }
+ return false
+}
+
+// The void is remediation, and these are its limits, each pinned. In every
+// one of them the wrong-total invoice IS open, and so payable by the
+// customer from the provider's hosted page, for some time. The tests assert
+// that window exists, so nobody reads the void as closing the defect, and
+// then assert what the engine does about it.
+
+// The process dies between the finalize and the void (here: the finalize
+// ran and its answer was lost). Until the next visit the invoice is open.
+// The next visit voids it.
+func TestInvoiceWrongTotalIsPayableUntilTheNextVisitWhenTheProcessDiesBeforeTheVoid(t *testing.T) {
+ ix := wrongTotalFixture(t)
+ ix.fake.loseResponse["finalize"] = true
+ if err := ix.run(t); err == nil {
+ t.Fatal("the visit whose finalize answer was lost reported success")
+ }
+ if got := ix.invoiceStatus(t); got != "open" {
+ t.Fatalf("after the lost finalize the invoice is %q; this test pins that it is open, and payable, until someone comes back", got)
+ }
+ if _, mutations, _ := ix.fake.counts(); mutations["void"] != 0 {
+ t.Fatalf("mutations %v: a void was sent by a process that never saw the finalize answer", mutations)
+ }
+ ix.now = ix.now.Add(time.Minute)
+ _ = ix.run(t)
+ if got := ix.invoiceStatus(t); got != "void" {
+ t.Fatalf("the next visit left the wrong-total invoice %q", got)
+ }
+ if _, mutations, _ := ix.fake.counts(); mutations["pay"] != 0 || mutations["void"] != 1 {
+ t.Fatalf("mutations %v: want one void and no payment", mutations)
+ }
+ if !ix.hasReview(t, "billing_invoice_voided_wrong_total") {
+ t.Fatalf("no review says the invoice was voided: %v", ix.reviews(t))
+ }
+}
+
+// The customer pays the wrong-total invoice in that window, before the
+// engine comes back. It cannot be voided. The engine does not record the
+// month as complete and does not pretend otherwise: it is held for a person
+// as paid with a wrong total.
+func TestInvoiceWrongTotalPaidByTheCustomerBeforeTheVoidIsHeldNotCompleted(t *testing.T) {
+ ix := wrongTotalFixture(t)
+ ix.fake.loseResponse["finalize"] = true
+ if err := ix.run(t); err == nil {
+ t.Fatal("the visit whose finalize answer was lost reported success")
+ }
+ // The customer pays from the hosted page.
+ ix.fake.mu.Lock()
+ for _, inv := range ix.fake.invoices {
+ if inv.status != "open" {
+ ix.fake.mu.Unlock()
+ t.Fatalf("fixture: the invoice is %q, want open", inv.status)
+ }
+ inv.status = "paid"
+ }
+ ix.fake.mu.Unlock()
+ for i := 0; i < 3; i++ {
+ ix.now = ix.now.Add(time.Minute)
+ _ = ix.run(t)
+ }
+ if got := ix.invoiceStatus(t); got != "paid" {
+ t.Fatalf("the invoice is %q", got)
+ }
+ if !ix.hasReview(t, "billing_invoice_paid_wrong_total") {
+ t.Fatalf("a wrong-total invoice the customer paid has no review saying so: %v", ix.reviews(t))
+ }
+ if rec := ix.month(t); rec.State == "complete" {
+ t.Fatalf("the month was recorded complete over an invoice paid with a wrong total: %+v", rec)
+ }
+ if _, mutations, _ := ix.fake.counts(); mutations["pay"] != 0 {
+ t.Fatalf("mutations %v: the engine charged", mutations)
+ }
+}
+
+// The void fails. Three ways, and in each the invoice stays open, and
+// payable, for as long as it takes.
+func TestInvoiceWrongTotalWhenTheVoidFails(t *testing.T) {
+ t.Run("the void request never arrives: the invoice is open until a later visit voids it", func(t *testing.T) {
+ ix := wrongTotalFixture(t)
+ ix.fake.dropRequest["void"] = true
+ if err := ix.run(t); err == nil {
+ t.Fatal("the visit whose void failed reported success")
+ }
+ if got := ix.invoiceStatus(t); got != "open" {
+ t.Fatalf("after the failed void the invoice is %q; this test pins that it is still open", got)
+ }
+ ix.now = ix.now.Add(time.Minute)
+ _ = ix.run(t)
+ if got := ix.invoiceStatus(t); got != "void" {
+ t.Fatalf("the next visit left the invoice %q", got)
+ }
+ if !ix.hasReview(t, "billing_invoice_voided_wrong_total") {
+ t.Fatalf("no review: %v", ix.reviews(t))
+ }
+ })
+ t.Run("the void runs and its answer is lost: the next visit learns it is void", func(t *testing.T) {
+ ix := wrongTotalFixture(t)
+ ix.fake.loseResponse["void"] = true
+ if err := ix.run(t); err == nil {
+ t.Fatal("the visit whose void answer was lost reported success")
+ }
+ ix.now = ix.now.Add(time.Minute)
+ _ = ix.run(t)
+ if got := ix.invoiceStatus(t); got != "void" {
+ t.Fatalf("the invoice is %q", got)
+ }
+ if _, mutations, _ := ix.fake.counts(); mutations["void"] != 1 || mutations["pay"] != 0 {
+ t.Fatalf("mutations %v: want exactly one void and no payment", mutations)
+ }
+ if !ix.hasReview(t, "billing_invoice_voided_wrong_total") {
+ t.Fatalf("no review: %v", ix.reviews(t))
+ }
+ })
+ t.Run("the provider refuses the void: the invoice stays open, held, and never charged by the engine", func(t *testing.T) {
+ ix := wrongTotalFixture(t)
+ ix.fake.refuse["void"] = 400
+ _ = ix.run(t)
+ for i := 0; i < 3; i++ {
+ ix.now = ix.now.Add(time.Hour)
+ _ = ix.run(t)
+ }
+ if got := ix.invoiceStatus(t); got != "open" {
+ t.Fatalf("the invoice is %q; this test pins that a refused void leaves it open and payable until a person acts", got)
+ }
+ if len(ix.reviews(t)) == 0 {
+ t.Fatal("a wrong-total invoice that could not be voided has no review")
+ }
+ if _, mutations, _ := ix.fake.counts(); mutations["pay"] != 0 {
+ t.Fatalf("mutations %v: the engine charged a wrong-total invoice", mutations)
+ }
+ if rec := ix.month(t); rec.State == "complete" {
+ t.Fatalf("the month was recorded complete: %+v", rec)
+ }
+ })
+}