carlos / platform Public

Per-line tax codes, Task 2: the tax shape, a line's own request and operation identity, the three metadata checks

taxShapeOK: one code on the calculation and none on any line, or none on
the calculation and one on every line; a code is txcd_ and more; a
calculation that is not waived has lines. The invoice run's entry guard
keeps its account, month and money checks and asks the shape instead of
the top-level predicate. lineParams takes the line's code (sent as the
item's tax_code and metadata[tax_code]) or the calculation's, never both,
never neither; lineKind is line-<meter> as before, line-<meter>-<code> for
a line with its own. The POST answer, the recovery match and the draft
validation require metadata[tax_code] to be the frozen line's when the
line carries one. InvoiceItemParams.TaxCodeMetadata is omitempty, so an
old request encodes and hashes as before (Task 0's pinned hashes pass),
and the client refuses a metadata code that is not the item's. The fake
provider stores and reports a line's tax code and metadata.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 510bbb7d9a9a65a6dc25df6944683b11aca1d390 parent b04d846
6 files changed, +199 −12
  • internal/billingstripe/invoices.go +12 −0
  • internal/billingstripe/invoices_test.go +26 −0
  • internal/console/billingcalculation.go +33 −0
  • internal/console/billinginvoice.go +31 −9
  • internal/console/billinginvoice_test.go +20 −3
  • internal/console/billingtaxshape_test.go +77 −0

CI cancelled — superseded by a newer push — run details

diff --git a/internal/billingstripe/invoices.go b/internal/billingstripe/invoices.go
index b00b61069..65f75ea43 100644
--- a/internal/billingstripe/invoices.go
+++ b/internal/billingstripe/invoices.go
@@ -266,6 +266,12 @@ func (x *InvoiceItem) UnmarshalJSON(data []byte) error {
type InvoiceItemParams struct {
Customer, Invoice, Meter, TaxCode, Description, CalculationDigest string
AmountCents, PeriodStart, PeriodEnd int64
+ // TaxCodeMetadata, when set, is sent as metadata[tax_code] and must
+ // equal TaxCode: a line that carries its own code (one of two for a
+ // meter) names it in metadata so it can be told from its sibling when
+ // read back. Omitted from the encoding when empty, so a request
+ // without it hashes exactly as before the field existed.
+ TaxCodeMetadata string `json:",omitempty"`
// Currency is the currency of the item: the invoice's and the
// calculation's own. Required, lower case; omitted from the encoding
// when empty for the same reason as DraftInvoiceParams.Currency.
@@ -277,12 +283,18 @@ func (c *Client) CreateInvoiceItem(ctx context.Context, p InvoiceItemParams, key
!validID(p.Meter) || !validDigest(p.CalculationDigest) || p.AmountCents <= 0 || p.PeriodStart <= 0 || p.PeriodEnd <= p.PeriodStart || len(p.Description) > 500 || !validCurrency(p.Currency) {
return nil, &Error{Code: CodeInvalidParams}
}
+ if p.TaxCodeMetadata != "" && p.TaxCodeMetadata != p.TaxCode {
+ return nil, &Error{Code: CodeInvalidParams}
+ }
form := url.Values{
"customer": {p.Customer}, "invoice": {p.Invoice}, "amount": {strconv.FormatInt(p.AmountCents, 10)}, "currency": {p.Currency},
"discountable": {"false"}, "tax_behavior": {"exclusive"}, "tax_code": {p.TaxCode},
"description": {p.Description}, "period[start]": {strconv.FormatInt(p.PeriodStart, 10)}, "period[end]": {strconv.FormatInt(p.PeriodEnd, 10)},
"metadata[meter]": {p.Meter}, "metadata[calculation_digest]": {p.CalculationDigest},
}
+ if p.TaxCodeMetadata != "" {
+ form.Set("metadata[tax_code]", p.TaxCodeMetadata)
+ }
var out InvoiceItem
if err := c.do(ctx, http.MethodPost, "/v1/invoiceitems", form, key, &out); err != nil {
return nil, err
diff --git a/internal/billingstripe/invoices_test.go b/internal/billingstripe/invoices_test.go
index 637597890..7a20c6650 100644
--- a/internal/billingstripe/invoices_test.go
+++ b/internal/billingstripe/invoices_test.go
@@ -163,6 +163,32 @@ func TestInvoiceItemTargetsOnlyItsDraftWithNoDiscount(t *testing.T) {
}
}
+// A line that carries its own tax code names it in metadata too; one that
+// does not sends no metadata[tax_code] at all; a metadata code that is not
+// the item's tax code is refused before any request.
+func TestInvoiceItemNamesItsOwnTaxCodeInMetadata(t *testing.T) {
+ var form url.Values
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ form = r.PostForm
+ w.Write([]byte(`{"id":"ii_usage","invoice":"in_usage","customer":"cus_paid","livemode":false,"currency":"eur","amount":800,"discountable":false}`))
+ }))
+ defer srv.Close()
+ p := invoiceItemFixture()
+ if _, err := testClient(t, srv.URL).CreateInvoiceItem(context.Background(), p, "k1"); err != nil || form.Has("metadata[tax_code]") {
+ t.Fatalf("a line without its own code: %v %v", form, err)
+ }
+ p.TaxCodeMetadata = p.TaxCode
+ if _, err := testClient(t, srv.URL).CreateInvoiceItem(context.Background(), p, "k2"); err != nil || form.Get("metadata[tax_code]") != p.TaxCode || form.Get("tax_code") != p.TaxCode {
+ t.Fatalf("a line with its own code: %v %v", form, err)
+ }
+ form = nil
+ p.TaxCodeMetadata = "txcd_other"
+ if _, err := testClient(t, srv.URL).CreateInvoiceItem(context.Background(), p, "k3"); err == nil || form != nil {
+ t.Fatalf("a metadata code that is not the tax code: %v %v", form, err)
+ }
+}
+
func TestFinalizeInvoiceLeavesCollectionExplicit(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/invoices/in_usage/finalize" || r.Method != "POST" || r.Header.Get("Idempotency-Key") != "stable-finalize" {
diff --git a/internal/console/billingcalculation.go b/internal/console/billingcalculation.go
index 12f2cde36..b62682b7c 100644
--- a/internal/console/billingcalculation.go
+++ b/internal/console/billingcalculation.go
@@ -9,6 +9,7 @@ import (
"math"
"math/big"
"sort"
+ "strings"
"time"
"github.com/carlosframework/platform/internal/plans"
@@ -77,6 +78,38 @@ func (c *billingCalculation) wellFormed() bool {
return false
}
+// validTaxCode is the shape of a product tax code: "txcd_" and at least
+// one more character. Which codes exist is the provider's; the console
+// checks the shape alone.
+func validTaxCode(code string) bool { return strings.HasPrefix(code, "txcd_") && len(code) > 5 }
+
+// taxShapeOK reports whether the calculation carries its tax codes in one
+// of the two shapes it may: one code on the calculation and none on any
+// line (every calculation frozen before lines carried codes, and every
+// month whose paid spans map to one code), or no code on the calculation
+// and one on every line (a month whose paid spans carry two codes). Either
+// way a calculation that is not waived has lines, and every code present
+// has the shape of one. A calculation in neither shape is not invoiced and
+// not priced again: it is a record to look at.
+func (c *billingCalculation) taxShapeOK() bool {
+ tagged := 0
+ for _, l := range c.Lines {
+ if l.TaxCode != "" {
+ if !validTaxCode(l.TaxCode) {
+ return false
+ }
+ tagged++
+ }
+ }
+ if len(c.Lines) == 0 && !c.waived() {
+ return false
+ }
+ if c.TaxCode != "" {
+ return validTaxCode(c.TaxCode) && tagged == 0
+ }
+ return tagged == len(c.Lines)
+}
+
// waived and totalMinor read the invoice total of either version. They are
// meaningful only for a well-formed calculation.
func (c *billingCalculation) waived() bool {
diff --git a/internal/console/billinginvoice.go b/internal/console/billinginvoice.go
index 07f78a6b1..72efcbbb4 100644
--- a/internal/console/billinginvoice.go
+++ b/internal/console/billinginvoice.go
@@ -10,7 +10,6 @@ import (
"errors"
"fmt"
"os"
- "strings"
"sync"
"time"
@@ -333,7 +332,7 @@ func (s *Server) invoiceBillingMonth(ctx context.Context, acct *Account, month s
// The stored calculation must be whole for its version and name one
// currency throughout. One that is not is never invoiced, and is never
// priced again in some other currency: it is a record to look at.
- if r.calc.Statement.Account != acct.Sqid || r.calc.Statement.Month != month || !r.calc.wellFormed() || !strings.HasPrefix(r.calc.TaxCode, "txcd_") || len(r.calc.TaxCode) <= 5 {
+ if r.calc.Statement.Account != acct.Sqid || r.calc.Statement.Month != month || !r.calc.wellFormed() || !r.calc.taxShapeOK() {
return r.review("billing_invoice_record_mismatch")
}
if err := r.reload(); err != nil {
@@ -955,15 +954,38 @@ func (r *billingInvoiceRun) stepCreating(ctx context.Context) error {
})
}
+// lineParams is the provider request for one line: the line's own tax
+// code when it carries one (a two-code calculation; the code is then also
+// sent as the item's metadata, which is how the line is told apart from
+// its sibling of the same meter afterwards), else the calculation's. A
+// line with a code at both levels, or at neither, is not sent.
func (r *billingInvoiceRun) lineParams(line plans.OverageLine) (billingstripe.InvoiceItemParams, error) {
- if !strings.HasPrefix(r.calc.TaxCode, "txcd_") || len(r.calc.TaxCode) <= 5 {
+ code, meta := r.calc.TaxCode, ""
+ if line.TaxCode != "" {
+ if code != "" {
+ return billingstripe.InvoiceItemParams{}, errBillingInvoiceMismatch
+ }
+ code, meta = line.TaxCode, line.TaxCode
+ }
+ if !validTaxCode(code) {
return billingstripe.InvoiceItemParams{}, errBillingInvoiceMismatch
}
- return billingstripe.InvoiceItemParams{Customer: r.rec.Customer, Invoice: r.rec.InvoiceID, Meter: line.Meter, TaxCode: r.calc.TaxCode,
+ return billingstripe.InvoiceItemParams{Customer: r.rec.Customer, Invoice: r.rec.InvoiceID, Meter: line.Meter, TaxCode: code, TaxCodeMetadata: meta,
Description: billingInvoiceLineDescription(line.Meter, r.month), CalculationDigest: r.rec.Digest,
AmountCents: line.AmountCents, PeriodStart: r.start.Unix(), PeriodEnd: r.end.Unix(), Currency: r.calc.Currency}, nil
}
+// lineKind is the journal's name for a line's operation: by meter, as it
+// always was, for a line without its own code; by meter and code for a
+// line with one, so two lines of one meter in a two-code calculation are
+// two operations. The two cannot collide: a code is never empty.
+func lineKind(line plans.OverageLine) string {
+ if line.TaxCode == "" {
+ return "line-" + line.Meter
+ }
+ return "line-" + line.Meter + "-" + line.TaxCode
+}
+
// billingInvoiceLineDescription is what the customer reads on the invoice
// line. There is no approved invoice copy yet (the billing-launch copy
// review has no result), so the text stays input-bound: the meter's own
@@ -993,7 +1015,7 @@ func (r *billingInvoiceRun) stepDraft(ctx context.Context) error {
if err != nil {
return r.review("billing_invoice_tax_code_mismatch")
}
- kind := "line-" + line.Meter
+ kind := lineKind(line)
if err := r.perform(ctx, providerOp{
kind: kind, hash: r.requestHash(kind, params),
post: func(ctx context.Context, key string) (string, string, error) {
@@ -1001,7 +1023,7 @@ func (r *billingInvoiceRun) stepDraft(ctx context.Context) error {
if err != nil {
return "", "", err
}
- if item.Invoice != r.rec.InvoiceID || item.Customer != r.rec.Customer || item.LiveMode != r.b.cfg.LiveMode || item.Amount != line.AmountCents || item.Currency != r.calc.Currency || item.Discountable || item.Metadata["meter"] != line.Meter || item.Metadata["calculation_digest"] != r.rec.Digest {
+ if item.Invoice != r.rec.InvoiceID || item.Customer != r.rec.Customer || item.LiveMode != r.b.cfg.LiveMode || item.Amount != line.AmountCents || item.Currency != r.calc.Currency || item.Discountable || item.Metadata["meter"] != line.Meter || item.Metadata["calculation_digest"] != r.rec.Digest || (line.TaxCode != "" && item.Metadata["tax_code"] != line.TaxCode) {
return item.ID, "attached", errBillingInvoiceMismatch
}
return item.ID, "attached", nil
@@ -1013,7 +1035,7 @@ func (r *billingInvoiceRun) stepDraft(ctx context.Context) error {
}
var matches []billingstripe.InvoiceLine
for _, l := range lines {
- if l.Metadata["meter"] == line.Meter && l.Metadata["calculation_digest"] == r.rec.Digest && l.InvoiceItem != "" {
+ if l.Metadata["meter"] == line.Meter && l.Metadata["calculation_digest"] == r.rec.Digest && l.InvoiceItem != "" && (line.TaxCode == "" || l.Metadata["tax_code"] == line.TaxCode) {
matches = append(matches, l)
}
}
@@ -1103,7 +1125,7 @@ func (r *billingInvoiceRun) validateDraftLines(ctx context.Context) error {
}
expected := map[string]plans.OverageLine{}
for _, line := range r.calc.Lines {
- op, ok := r.ops["line-"+line.Meter]
+ op, ok := r.ops[lineKind(line)]
if !ok || op.State != billingOpAcked || op.ObjectID == "" {
return errBillingConflict
}
@@ -1113,7 +1135,7 @@ func (r *billingInvoiceRun) validateDraftLines(ctx context.Context) error {
var sum int64
for _, l := range lines {
want, ok := expected[l.InvoiceItem]
- if !ok || seen[l.InvoiceItem] || l.Amount != want.AmountCents || l.Currency != r.calc.Currency || l.Discountable || l.Metadata["meter"] != want.Meter || l.Metadata["calculation_digest"] != r.rec.Digest {
+ if !ok || seen[l.InvoiceItem] || l.Amount != want.AmountCents || l.Currency != r.calc.Currency || l.Discountable || l.Metadata["meter"] != want.Meter || l.Metadata["calculation_digest"] != r.rec.Digest || (want.TaxCode != "" && l.Metadata["tax_code"] != want.TaxCode) {
return r.review("billing_invoice_lines_mismatch")
}
seen[l.InvoiceItem] = true
diff --git a/internal/console/billinginvoice_test.go b/internal/console/billinginvoice_test.go
index 2581f52db..afe7455fd 100644
--- a/internal/console/billinginvoice_test.go
+++ b/internal/console/billinginvoice_test.go
@@ -79,6 +79,19 @@ type fakeLine struct {
currency string
id, item, meter, digest string
amount int64
+ // taxCode is the item's tax_code as posted; metaTax its
+ // metadata[tax_code], set only by a line that carries its own code.
+ taxCode, metaTax string
+}
+
+// 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 {
+ m := map[string]string{"meter": l.meter, "calculation_digest": l.digest}
+ if l.metaTax != "" {
+ m["tax_code"] = l.metaTax
+ }
+ return m
}
func newFakeInvoiceStripe(sqid string, now func() time.Time) *fakeInvoiceStripe {
@@ -121,7 +134,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": map[string]string{"meter": l.meter, "calculation_digest": l.digest}, "period": map[string]int64{"start": 1, "end": 2}})
+ "metadata": l.lineMetadata(), "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{}})
@@ -190,6 +203,9 @@ func (f *fakeInvoiceStripe) serve(w http.ResponseWriter, r *http.Request) {
switch {
case path == "/v1/invoiceitems":
kind = "line-" + r.PostForm.Get("metadata[meter]")
+ if tc := r.PostForm.Get("metadata[tax_code]"); tc != "" {
+ kind += "-" + tc
+ }
case strings.HasSuffix(path, "/finalize"):
kind = "finalize"
case strings.HasSuffix(path, "/pay"):
@@ -228,9 +244,10 @@ func (f *fakeInvoiceStripe) serve(w http.ResponseWriter, r *http.Request) {
f.fault(kind, w, func() any {
f.seq++
amount, _ := strconv.ParseInt(r.PostForm.Get("amount"), 10, 64)
- 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}
+ 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": map[string]string{"meter": line.meter, "calculation_digest": line.digest}}
+ 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()}
}, key)
case strings.HasSuffix(path, "/finalize"):
inv := f.invoices[strings.TrimSuffix(strings.TrimPrefix(path, "/v1/invoices/"), "/finalize")]
diff --git a/internal/console/billingtaxshape_test.go b/internal/console/billingtaxshape_test.go
new file mode 100644
index 000000000..df0263ea2
--- /dev/null
+++ b/internal/console/billingtaxshape_test.go
@@ -0,0 +1,77 @@
+package console
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/billingstripe"
+ "github.com/carlosframework/platform/internal/plans"
+)
+
+// A calculation carries its tax codes in one of two shapes: one code on
+// the calculation and none on any line, or none on the calculation and one
+// on every line. Anything else is a record to look at, not to invoice.
+func TestACalculationCarriesItsTaxCodesInOneOfTwoShapes(t *testing.T) {
+ waived := func(c *billingCalculation) *billingCalculation { c.Total = &plans.Invoice{Waived: true}; return c }
+ v2 := func(top string, lines ...plans.OverageLine) *billingCalculation {
+ return &billingCalculation{V: 2, TaxCode: top, Lines: lines, Total: &plans.Invoice{TotalMinor: 1}}
+ }
+ for name, tc := range map[string]struct {
+ calc *billingCalculation
+ ok bool
+ }{
+ "one code, untagged lines": {v2("txcd_10102001", plans.OverageLine{Meter: "writes", AmountCents: 5}), true},
+ "one code, waived, no lines": {waived(v2("txcd_10102001")), true},
+ "two codes, every line tagged": {v2("", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "txcd_10102001"}, plans.OverageLine{Meter: "writes", AmountCents: 3, TaxCode: "txcd_10102000"}), true},
+ "two-code shape with one code emitted": {v2("", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "txcd_10102000"}), true},
+ "two-code shape, waived, no lines": {waived(v2("")), true},
+ "a code at both levels": {v2("txcd_10102001", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "txcd_10102001"}), false},
+ "a code at neither level": {v2("", plans.OverageLine{Meter: "writes", AmountCents: 5}), false},
+ "two codes with one line lacking its code": {v2("", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "txcd_10102001"}, plans.OverageLine{Meter: "awake_seconds", AmountCents: 3}), false},
+ "a malformed code on the calculation": {v2("txcd_", plans.OverageLine{Meter: "writes", AmountCents: 5}), false},
+ "a malformed code on a line": {v2("", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "10102001"}), false},
+ "not waived and no lines, one code": {v2("txcd_10102001"), false},
+ "not waived and no lines, no code": {v2(""), false},
+ } {
+ if got := tc.calc.taxShapeOK(); got != tc.ok {
+ t.Errorf("%s: taxShapeOK %v, want %v", name, got, tc.ok)
+ }
+ }
+}
+
+// The request for a line takes the line's own code when it carries one,
+// and then names it in metadata too; else the calculation's; never both,
+// never neither. A request without the metadata field encodes exactly as
+// before the field existed, so an old journal's hashes still match.
+func TestALinesRequestCarriesItsOwnCodeOrTheCalculationsNotBoth(t *testing.T) {
+ run := func(top string) *billingInvoiceRun {
+ return &billingInvoiceRun{month: "2026-09", rec: billingMonthRecord{Customer: "cus_x", InvoiceID: "in_x", Digest: "d"}, calc: billingCalculation{TaxCode: top, Currency: "usd"}}
+ }
+ for name, tc := range map[string]struct {
+ top string
+ line plans.OverageLine
+ code string
+ meta string
+ err bool
+ }{
+ "the calculation's code": {"txcd_10102001", plans.OverageLine{Meter: "writes", AmountCents: 5}, "txcd_10102001", "", false},
+ "the line's own code": {"", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "txcd_10102000"}, "txcd_10102000", "txcd_10102000", false},
+ "a code at both levels": {"txcd_10102001", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "txcd_10102000"}, "", "", true},
+ "a code at neither": {"", plans.OverageLine{Meter: "writes", AmountCents: 5}, "", "", true},
+ "a malformed line code": {"", plans.OverageLine{Meter: "writes", AmountCents: 5, TaxCode: "txcd_"}, "", "", true},
+ "a malformed top-level code": {"txcd", plans.OverageLine{Meter: "writes", AmountCents: 5}, "", "", true},
+ } {
+ p, err := run(tc.top).lineParams(tc.line)
+ if (err != nil) != tc.err || p.TaxCode != tc.code || p.TaxCodeMetadata != tc.meta {
+ t.Errorf("%s: %+v %v", name, p, err)
+ }
+ }
+ raw, _ := json.Marshal(billingstripe.InvoiceItemParams{Customer: "cus_x", TaxCode: "txcd_10102001"})
+ if strings.Contains(string(raw), "TaxCodeMetadata") {
+ t.Fatalf("a request without the metadata code encodes it: %s", raw)
+ }
+ if lineKind(plans.OverageLine{Meter: "writes"}) != "line-writes" || lineKind(plans.OverageLine{Meter: "writes", TaxCode: "txcd_10102000"}) != "line-writes-txcd_10102000" {
+ t.Fatal("the operation kinds")
+ }
+}