Skip to content

C2-3: invoices and credit memos on the contract, gapless numbering, void and restock - #46

Merged
futurebuildai merged 27 commits into
refactor/v1from
refactor/c2-3-invoices
Oct 9, 2026
Merged

futurebuildai merged 27 commits into
refactor/v1from
refactor/c2-3-invoices

Conversation

@futurebuildai

Copy link
Copy Markdown
Contributor

Summary

C2-3 of the Gable v1 goal (money class): invoices and credit memos on the wire contract (ADR 0001) as ADR 0005 sections 4, 6 and 8 build them. Migration 097.

  • Invoices. The cursor list with working filters (status, customer_id, job_id, ship_to_id, order_id, overdue), the detail with its lines in the shared sales line shape (IN-1.8), a lowercase status, _cents money, the gapless IN- number, a revision with its ETag, the job and ship-to the invoice carries, the customer's terms snapshot (due date, early pay discount), open_cents and the computed is_overdue. OVERDUE is gone from storage, the wire and every reader.
  • The void (POST /invoices/{id}/transitions, roles admin, owner, finance): locks the order row and the customer's credit serialization first, reverses the invoice's whole entry (dated the void date), returns the billed stock to on hand, reduces the order lines' fulfilled quantities, re-runs allocation for exactly those quantities (whole kits for kits), re-derives the order status and credits the subledger back. Refused with has_applications (a payment recorded or an applied credit memo names it) or has_credit_memos.
  • Credit memos (/credit-memos, new): a draft with no number (minted at post, so a voided draft consumes none), lines that return invoice lines in proportion to what was charged (the credit memos of a line sum to its total to the cent, never more than was billed: exceeds_billed) or free lines (a price given back, a fee, a note), tax at the invoice's rate and never more than the invoice charged (the last memo takes the remainder), the post with restock and its balanced entry (COGS reversed at the cost that LEFT), and the void (reverses the entry and the restock).
  • Gapless numbering: document_counters, httpx.NextGaplessNumber, the two SQL functions as column DEFAULTs (the seed numbers through the same counter), the number minted late (after the customer row lock, just before the insert).
  • Also: gl.PostReversal, inventory.UnrestockQty, salesdoc.BilledDiscount, the payment module refusing a void invoice under the invoice row lock, period_closed as a 409 on every posting, the desk invoice and credit memo pages, the portal invoice page, Playwright flows, 16 CONTRACT-CHANGES rows.

Postings take the same interim path as C2-2's (gl.PostEntry, today's PostTransaction) until C2-4.

Carried items

  • The relieved cost read excludes void invoices (order/repository.go; test TestRelievedCostReadExcludesVoidInvoices, which fails when the exclusion is removed).
  • Invoice line discount_cents on partial pieces is the gross piece less the net piece, so total plus discount equals the gross to the cent and the shares sum to the order line's discount exactly (salesdoc.BilledDiscount, a property test and a wire test; the same wire test written against the base fails: 334 and 666 against the gross pieces 333 and 667).
  • The document print and email routes keep PR 39's wall (TestBranchWall_DocumentRoutes, now through GetInvoiceRecord).
  • Every act that adds to a customer's exposure (an invoice void, a posted credit memo's void) takes C2-2b's per customer advisory lock (TestExposureAddingActsTakeTheCustomerCreditLock).
  • customers.payment_terms and invoices.payment_terms are dropped by 097 with 091's mapping (TestMigration097_BackfillsRowsThatExist).
  • The payload branch rule on every write route and the branch wall on every read (TestBranchWall_InvoiceAndCreditMemoRoutes through serve's wiring).

Exit test

  • cd core && DATABASE_URL=<url> GABLE_TEST_REQUIRE_DB=1 CI=true go test -race ./...: 64 packages ok, no race.
  • make contract-go and cd web/packages/api-client && npm run drift && npm run typecheck: fresh, 0 pending, 368 covered, 1092 golden steps conform.
  • bash scripts/check-shape.sh, python3 .github/scripts/reuse_gate.py: pass. cd web && npm run lint && npm run typecheck && npm test && npm run build: pass (543 desk tests). Playwright on the real stack with VITE_AUTH_DEV_MODE=true: 27 passed.
  • Wire tests first, failing on the base: TestBaseInvoiceWireFacts written with the base's request shapes and the new outcomes fails 8 ways on 7cda840 (status casing, no number, no _cents, lines without line_total_cents, overdue silently ignored, the offset page, no credit memo route).

Not built here (ADR 0005 assigns it elsewhere)

AR aging and statements by job (C2-4, ADR 0005 section 10); invoice amount_open, applications and the write off (C2-4); a kit or component line is not credited by line (a 400 asks for the invoice line that billed the kit as a free line): kit credit memos need the component explosion the ADR leaves unwritten.

🤖 Generated with Claude Code

Gable v1 Coder and others added 27 commits October 8, 2026 12:39
… 4.1)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ters, numbers, terms by id, credit memo lines)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…count: credit memo and reversal row types

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ng credit memo)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…pository, void, credit memo lifecycle, gapless numbering), callers moved; WIP tests
…erlocks, seed, portal readers, document print wall kept

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s gain the credit memo and reversal values
…oid, numbering, credit memo lifecycle, tax cap, restock COGS)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… wall through serve, exposure lock, relieved cost and discount carried items

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…it memo pages, portal invoices, Playwright flows

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…_id, deposit applications, closed short orders, live piece billing after a void, restock cost telescoping, stale PENDING reader

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…invoices

# Conflicts:
#	web/packages/api-client/test/usage.ts
The sync merge of refactor/v1 brought in C3-1 (the product list) and C3-1b
(the inventory levels list), both now the cursor envelope with available as a
decimal string. stockedProducts still read bare arrays and threw "list is not
iterable"; it now walks the seed's products oldest first as fulfilment.spec.ts
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n pieces round down

With the remainder branch of creditTax replaced by case false the new
test fails (tax credited 45 of 47, balance 2); with it, green.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s it

A void committed while the charge was at the gateway made the in-transaction
lockInvoice refuse, leaving the customer charged with no document. The charge
is now voided (same day) or refunded (settled) through the existing gateway
interface before the refusal returns, and both outcomes are logged. No new
gateway method was needed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…retryable 409

voidCredit decided the finance role and the credit serialization from an
unlocked read. If a post committed before the row lock, the void ran on the
open memo with both skipped. After the lock, a draft read that is no longer a
draft is now a 409 credit_memo_changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…S(amount)

A legacy memo with a negative amount aborted the whole file on the nonpositive
CHECK. The totals and the ADJUST line now read the absolute value.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e counter sale void

An origin pos invoice is 409 counter_sale: its stock moved with the sale, so
voiding the invoice alone left the sale standing. The legacy entry test now
reclassifies its invoice as an order's. CONTRACT-CHANGES row added.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Three messages sent the user to each other while all refusing. Every kit
refusal now says a kit cannot be returned in v1 and to credit its price with a
free line that names no product; a text line keeps its own message.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…erse on a detached context

P2-1: reverseCharge returns its outcome (voided, refunded, failed). The
409 keeps ErrInvoiceVoid and names the reversal in its message (the
handler now writes that message to the body). When the void and the
refund both fail the service returns ChargeNotReversedError
(ErrChargeNotReversed) and the handler answers 502 CHARGE_NOT_REVERSED:
the card was charged, finance reconciles the gateway transaction id (the
id only). Every reversal writes a payment.charge_reversal audit row on
the invoice after the rolled back transaction ended, no card data.

P3-1: the reversal runs on context.WithoutCancel(ctx) with a 30 second
timeout. A transaction that never opened (the request ended during the
gateway call) now reverses too, since nothing was recorded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…invoices

# Conflicts:
#	docs/refactor/CONTRACT-CHANGES.md
@futurebuildai
futurebuildai merged commit 1eea582 into refactor/v1 Oct 9, 2026
9 checks passed
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.

1 participant