Skip to content

Lump-sum fallback when divided line price can't round-trip#42

Merged
pmenendz merged 3 commits into
mainfrom
lines-lump-sum-fallback
May 7, 2026
Merged

Lump-sum fallback when divided line price can't round-trip#42
pmenendz merged 3 commits into
mainfrom
lines-lump-sum-fallback

Conversation

@pmenendz
Copy link
Copy Markdown
Collaborator

@pmenendz pmenendz commented May 7, 2026

Summary

  • For each invoice/credit-note line, after computing price = line.Amount / quantity at currency-subunit precision, verify that quantity × price round-trips back to line.Amount. When it doesn't, collapse the line to (quantity=1, price=line.Amount) so the GOBL tax base reconciles with Stripe exactly. Tiered billing takes the lump-sum path unconditionally (no honest single per-unit price across tier breakpoints). Zero-quantity zero-amount lines keep the readable (qty=0, price=Price.UnitAmount) display.
  • The trigger in production is Stripe's transform_quantity (e.g. divide_by=50, round=up): on a real PL invoice with Quantity=751, Amount=3520.00 PLN, the old 3520/751 = 4.69 rounded down, then 751 × 4.69 = 3522.19, inflating the line by +2.19 PLN and leaving a rounding: -2.69 PLN residue at the invoice level. The fix eliminates the residue.
  • The Stripe-supplied item description (e.g. "751 users × Additional Active Users (at 220.00 zł per 50 users / month)") is preserved verbatim on lump-sum lines, so the per-unit narrative is never lost — only the synthetic per-unit price the data wouldn't support.

Test plan

  • Added unit tests covering: clean per-unit, transform_quantity, proration mismatch, tiered billing, zero-quantity zero-amount, zero-quantity nil-price; plus credit-note clean / non-reconciling / zero-quantity variants.
  • Regenerated examples/stripe.gobl/out/gobl_quantity_rounding.json golden — line 6 (metered 204 × 147 ≠ 30000) collapses to lump sum; the prior totals.rounding: 0.12 residue disappears.
  • Verified end-to-end against test/invoice.json: totals.payable == "17724.30" matches Stripe's reported total exactly, with no rounding field.
  • go test ./... green.

🤖 Generated with Claude Code

Stripe's transform_quantity (e.g. divide_by 50, round up) charges
ceil(qty / divide_by) × unit_amount, which often isn't a clean 2-decimal
multiple of line.Quantity. Today we naively compute Amount/Quantity and
let num.Amount.Divide round to the currency subunit, then GOBL re-multiplies
to a sum that no longer matches Stripe's amount — inflating the tax base
and producing a totals.rounding residue large enough to mask real errors.

After computing the per-unit price, check that quantity × price round-trips
back to line.Amount at currency-subunit precision. When it doesn't, collapse
the line to (quantity=1, price=line.Amount) so the GOBL tax base reconciles
with Stripe exactly. The Stripe-supplied item description carries the
per-unit narrative either way, so no information is lost.

Tiered billing always takes the lump-sum path (no honest single per-unit
price across tier breakpoints). Zero-quantity zero-amount lines preserve
the (qty=0, price=Price.UnitAmount) display for readability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Stripe→GOBL line conversion to ensure line tax bases reconcile exactly with Stripe totals by falling back to a lump-sum (qty=1, price=line.Amount) representation whenever a derived per-unit price cannot round-trip back to the original line amount (notably with transform_quantity and similar cases). It applies the same round-trip logic to credit note lines and refreshes tests and a golden example output accordingly.

Changes:

  • Add round-trip verification for derived per-unit prices; fall back to lump-sum when qty × price != amount (invoice + credit note).
  • Treat tiered invoice pricing as lump-sum unconditionally; preserve readable display for (qty=0, amount=0) invoice lines when a unit price exists.
  • Add targeted unit tests and regenerate the gobl_quantity_rounding.json golden output.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.

File Description
lines.go Introduces (quantity, price) resolution with round-trip validation and lump-sum fallback for invoices and credit notes.
lines_test.go Adds unit tests covering clean per-unit, transform_quantity/proration mismatches, tiered billing, and zero-quantity edge cases.
examples/stripe.gobl/out/gobl_quantity_rounding.json Updates golden output to reflect lump-sum fallback and removal of invoice-level rounding residue.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lines.go Outdated
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pmenendz pmenendz merged commit 3f19ec3 into main May 7, 2026
5 checks passed
@pmenendz pmenendz deleted the lines-lump-sum-fallback branch May 7, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants