Skip to content

C4-1b: AP fixes on the contract (ADR 0008 7.4) - #78

Merged
futurebuildai merged 36 commits into
refactor/v1from
refactor/c4-1b-ap
Oct 10, 2026
Merged

futurebuildai merged 36 commits into
refactor/v1from
refactor/c4-1b-ap

Conversation

@futurebuildai

@futurebuildai futurebuildai commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

C4-1b: the AP fixes of ADR 0008 section 7.4 on the wire contract, the migration of section 12 and the builds of section 13, level with refactor/v1 at 76d266f. Every line with its proof, and each proof bitten by a mutant (the mutant named, the failure it produced):

ADR 0008 section 7.4 (the AP posting fixes)

  • Approve loads the lines: TestWire_ApproveEntryCarriesLineDebits reads the entry back: the line's debit on its account beside the 2010 credit, balanced. Mutant (the entry posts payable legs only): FAIL, the entry's accounts = map[2010:{10000 10000}], want 5020 (the line) and 2010 (the payable). TestWire_ApproveSpreadsTax pins the tax spread pro rata with the last line taking the remainder.
  • The entry posts through gl.PostEntry inside the approve transaction and a failure fails the act: TestWire_ApprovePostingFailureFailsTheAct closes the fiscal period holding the bill's date and the approve answers 409 with the period_closed blocker, the bill staying pending at revision 1 with no entry written. Mutant (the posting error logged and swallowed): FAIL, the approve answered 500 with no cause. TestWire_ApproveRefusesLineWithoutAccount holds the legacy line refusal (line_needs_account, nothing posts).
  • VOIDED is reachable by a transition that reverses the entry: TestWire_Lifecycle voids a pending bill (no entry to reverse, owes 0) and an approved bill (exactly one reversal entry), and refuses the void of a bill with money applied with the has_payments blocker. Mutant (the void skips the reversal): FAIL, the void wrote 0 reversals, want 1.
  • vendor_invoices gains branch, currency, revision, a status CHECK, po_id as a real FK and UNIQUE (vendor_id, invoice_number): migration 106 with TestMigration106_OnAwkwardLegacyRows. Mutant (the duplicate suffixing removed): FAIL, could not create unique index vendor_invoices_vendor_number_key (SQLSTATE 23505).
  • A payment applies only to invoices in approved or partial, locked in id order (section 9): TestWire_PaymentRefusesUnapprovedInvoice refuses a pending bill with the invoice_not_approved blocker and writes nothing. Mutant (the status check removed): FAIL, the refusal came as exceeds_open instead. The id order half: TestTx_PaymentLocksInIDOrder races three payments across the same three approved bills named in rotated orders, five rounds at pool size 4; every contender succeeds, the cents apply exactly once and the control account ties after each round. Mutant (the id sort removed): FAIL, deadlock detected (SQLSTATE 40P01) in round 0. TestTx_Concurrency_Pool4ThreeContenders adds the two payments of one invoice (the 10 cents applied exactly once) and a payment racing a void (exactly one winner).

ADR 0008 section 12 (migration 106, planned 098)

dabe8e0 plus 45fe983. TestMigration106_OnAwkwardLegacyRows: duplicates suffixed #2, #3 in (created_at, id) order with a notice, the orphan po_id set null with a notice, the NULL created_at filled, odd statuses normalized with notices, number unique and backfilled in (created_at, id) order with the sequence past the backfill, the currency, amount_open (zero on a voided bill), the constraints refusing the old shapes, the lines positioned, unlinked, with a scale 4 unit price; a second up is a no-op; the down rolls the shape back, refuses while a unit price is finer than cents, and the up applies again to the same result.

TestMigration106_OnTheSeededDatabase (new, 45fe983) applies the repository's own seed (DEMO_SEED=1, the seed binary, the scratch URL on its own command) and then migration 106 over awkward legacy bills written on top, the pattern of migration 099's seeded proof. Only this proof reaches the branch backfill's purchase order source: a bill tied to a real seeded purchase order takes that order's branch, not the default branch every fixture row falls to, and keeps it through the down-up cycle; the duplicate pair suffixes, the orphan nulls, lowercase statuses normalize, every row ends with a unique AP- number, a branch, a currency and amount_open at total less paid, a second up is a no-op, and the down then up over the seeded database loses no row. The test went red first and found a live defect: the guard compared information_schema's table_schema to the literal string 'current_schema', never to the schema's name (092 and 094 call current_schema()), so the purchase order branch backfill was dead code; 45fe983 corrects the file, which is this branch's own and unmerged.

ADR 0008 section 13 (C4-1b builds)

  • The AP fixes of 7.4: as above, 1ddab4a.
  • Migration (as numbered 106): dabe8e0, the seeded proof and guard fix 45fe983.
  • The vendor invoice wire (Gable's number AP-, the vendor's as vendor_invoice_number) and vendor_invoice.* events: TestWire_InvoiceShape, TestWire_List (the cursor, the filters, the strict query guard, include=total), TestWire_GetRefusals, TestWire_Revision (428, 409 stale, weak If-Match), TestWire_Idempotency, TestWire_BranchWall (another branch's bill a 404 on the by-id routes, never in the list, a foreign payload branch a 403; mutant with the wall predicate neutralized: FAIL, a held caller voids another branch's bill: 409, want 404 and the walled list shows it), TestWire_Aging (the buckets from amount_open), and the acts' event order pinned in TestWire_Lifecycle and TestWire_PaymentAppliesAndTies beside the ap and ap_payments goldens.
  • The three live failures as tests: the entry with only the payable leg (TestWire_ApproveEntryCarriesLineDebits, mutant FAIL above), the swallowed posting error (TestWire_ApprovePostingFailureFailsTheAct, mutant FAIL above), the payment applied to an unapproved invoice (TestWire_PaymentRefusesUnapprovedInvoice, mutant FAIL above).
  • Transactions: TestTx_FailedEventWriteRollsBackEachWrite and TestTx_FailedAuditWriteRollsBackEachWrite roll each kind of write back when the event or the audit row cannot be written (mutants that swallow the write error: both FAIL), TestTx_Pool4SaturationNeedsNoSecondConnection holds each kind of write at pool saturation inside one transaction (mutant with one read through the pool inside the transaction: FAIL, the read sees nothing the transaction wrote), and the concurrency proofs above run at pool size 4.
  • The GL ties after every act: TestWire_PaymentAppliesAndTies and both concurrency proofs assert each entry balanced and the 2010 control account equal to what the open bills owe.

Level with refactor/v1 (76d266f)

The merge commit 64aa638 resolved one conflict, docs/refactor/CONTRACT-CHANGES.md, as the union of both sides: ours 227 rows, theirs 258, shared base 218, merged 267, the sorted merged rows equal to the sorted union, no duplicate row. The generated files were regenerated from their sources, never hand merged: openapi.yaml by api/tools/merge, ROUTES.txt by cmd/census -write, the TypeScript schema by openapi-typescript. The merge brought six files unformatted from the integration head (no CI job there checks gofmt); a76c573 is formatting only so this tree is gofmt clean.

The clockwindow and gl_accounts_entries goldens were re-recorded (31670bb): both were last recorded in the C2-4 era, this branch's own 7.4 work post-dates that recording (the approve posting, the payment seam's memo casing, the aging from amount_open), and the merge itself changed no AP, GL, dashboard, reports or seed line. Every other golden re-recorded byte identical. The suite ran twice green after the re-record with no further drift.

The wire changes are in docs/refactor/CONTRACT-CHANGES.md; the desk AP page walks the cursor, sends cents, ten thousandths and the revision, and prices its draft total in the server's exact arithmetic.

Fix round 3 (the lead, FROM-PRINCIPAL 39)

From reviews r5 (Sonnet) and r6 (Opus), each red first:

  • 2d253ad: the void refuses only a bill that owes and has no linked entry; a bill that prices to zero (a zero price, or one that rounds to 0 cents) voids with nothing to reverse (TestWire_VoidZeroBill). The refusal texts, the transitions description and the CONTRACT-CHANGES row state the limit plainly: no route links an entry to a bill yet, so such a bill stays approved and payable until a later repair item.
  • d14124f: migration 106 clamps amount_open between zero and the total (a hand edited negative amount paid no longer aborts it), counts APPROVED as well as PARTIAL overpaid bills and the negative paid ones in notices, and its range CHECK says the rule exactly (a voided or paid bill owes zero).
  • ac73087: the bill create bounds the tax, the unit price, the line total, the subtotal and the total at their columns, each a 400 naming the field (TestWire_CreateMoneyCeilings).
  • Left: the approve's entry_unbalanced mapping stays as a defensive branch (unreachable by the wire, since lines are validated and the total derived).

futurebuildai and others added 30 commits October 10, 2026 05:41
The C4-1b migration of ADR 0008 section 12: vendor_invoices gains
created_at NOT NULL, revision, the number (AP-, a sequence, backfilled
in (created_at, id) order with the DEFAULT minting through the writer's
transaction), the branch (from the purchase order's branch when that
column exists, else the default branch; a defaults trigger serves raw
inserts), the currency, a status CHECK on today's values (lowercase
spellings uppercased and unknown values become PENDING, each with a
notice), po_id as a real foreign key (orphans set null, with a notice),
UNIQUE (vendor_id, invoice_number) after duplicates are suffixed #2, #3
in (created_at, id) order, amount_open and gl_entry_id; the lines gain
position, purchase_order_line_id, product_id, a unit_price widened to
scale 4 and po_freight_charge_id (the carrier freight line link of
7.3, written from package C's freight rule on); the keyset indexes.

The test builds the awkward legacy rows in a scratch database (three
duplicates of one vendor number, an orphan po_id, a NULL created_at,
odd statuses, paid, partial and voided rows), reads every backfill
back, checks the constraints refuse what the old columns took, applies
the migration a second time (a no-op), runs the down (which keeps the
suffixed duplicates, normalized statuses and nulled orphans, and
refuses while a unit price is finer than cents) and applies again.

Signed-off-by: colton <colton@futurebuild.ai>
The vendor invoice acts rebuilt on the module recipe:

- approve loads the lines inside the transition's transaction and posts
  the entry through gl.PostEntry there (a debit leg per line on the
  line's account, the tax spread across the line accounts pro rata with
  the last line taking the remainder, the credit to 2010 for the whole
  total); a posting failure fails the act with its cause (a closed
  period the 409 period_closed blocker, a legacy line with no account
  the line_needs_account blocker) where the base posted an entry with
  only the payable leg and swallowed the error;
- VOIDED is reachable by a transition that reverses the entry (a
  pending bill has nothing posted and simply dies; a bill with money
  applied is refused has_payments);
- a payment applies only to approved or partial bills (the pending,
  paid and voided ones refused invoice_not_approved), locks them in id
  order with the money applying in the order the request named them,
  and posts its entry through gl.PostEntry inside the act (a failure
  fails the act); more than the named bills still owe is refused
  exceeds_open and writes nothing;
- the wire: Gable's own number (AP-) as number, the vendor's as
  vendor_invoice_number, the cursor list with vendor_id, po_id and
  status filters, the error envelope with one 400 naming every field,
  revision with If-Match (428 without, 409 stale), lowercase statuses,
  cents and ten thousandths, the transitions route replacing /approve,
  POST /ap/invoices/{id}/transitions, and the branch wall on every
  read and write (the create's payload branch held by the guard);
- vendor_invoice.created, .approved, .partial, .paid and .voided
  through the outbox as the last statements of their transactions, and
  an audit row per act; a failing event or audit write rolls the whole
  act back.

The two broken sync functions are deleted from internal/gl (their only
caller was AP) and gl gains the 2010 account code constant; the matcher
reads the new invoice types and compares the wire's scale 4 line facts
through its own unconverted arithmetic until its own conversion; serve
wires the service with the outbox, audit log, transaction runner and
branch guard behind scoped roles, as every converted module is.

The tests carry the recipe's set (the create shape, exact money, the
validation 400, the list and its cursor, the error envelope, the
revision preconditions, the lifecycle edges with their events,
idempotency, the branch wall) beside the three live failures in their
converted form, the GL ties after every act (each entry balanced, the
2010 control account equal to what the open bills owe), the tax spread,
the failing event and audit writes rolling each write back, the pool 4
contenders (two payments of one invoice, a payment racing a void, three
racers on one revision) and the gated saturation proof.

Signed-off-by: colton <colton@futurebuild.ai>
The fragment describes the vendor invoice routes exactly (the cursor
envelope, the cents and ten thousandths, the transitions route with its
precondition and blockers, the payment and aging routes in their today
shapes with the payment act's new refusals); the model binding ties the
new ApVendorInvoice, ApVendorInvoiceSummary and ApVendorInvoiceLine to
the Go structs; the census carries the transitions route in place of
/approve and the pending list empties; CONTRACT-CHANGES gains the
C4-1b rows.

The ap and ap_payments goldens re-record on the converted routes: the
create in cents, ten thousandths and a decimal string quantity, the 428
transition without a precondition, the approve through /transitions and
its 409 on a second attempt, the events feed step, the partial and
settling payments, the paid bill's further payment refused
invoice_not_approved, the void with payments refused has_payments, the
validation 400, the status filters and the pending void. The harness
gains the expenseAccount seed anchor the bills' lines name.

Signed-off-by: colton <colton@futurebuild.ai>
The service walks the invoice list's cursor to the last page; the types
carry the wire's cents, ten thousandths, decimal string quantities,
lowercase statuses, revision and both numbers (Gable's AP- and the
vendor's own); the page approves through the transitions route with the
revision it loaded, pays from amount_open_cents, and prices the draft
total in exact integer arithmetic with the server's one rounding (half
away from zero), so the preview never drifts a cent from the bill that
is filed; the bill form sends cents, a decimal string quantity and ten
thousandths with each line's gl_account_id. The tests assert the
request bodies at the wire's scales beside the rendering, and the
generated client schema follows the fragment.

Signed-off-by: colton <colton@futurebuild.ai>
The two broken sync functions leave internal/gl (their only caller was
AP: SyncVendorInvoice posted only the payable leg and swallowed the
insert's error, SyncVendorPayment resolved its accounts by name the
same way), and gl gains the 2010 account code constant beside the
sales postings'; serve wires the AP service as every converted module
is (outbox writer, audit log, database as transaction runner, branch
guard) behind scoped admin, owner, finance with the branch middleware,
and the wall fixture's AP stub follows the new invoice type.

Signed-off-by: colton <colton@futurebuild.ai>
The matcher reads the converted invoice types: the purchase order link
and the lines keep their places, and the line's quantity and unit price
arrive as the wire's scale 4 facts (a decimal string and ten
thousandths), which the unconverted tolerance arithmetic reads through
its own float and cents lens until the matcher's own conversion (C4-2
E) rebuilds it on the line keys of ADR 0008 7.3.

Signed-off-by: colton <colton@futurebuild.ai>
Brings the branch level with refactor/v1 at 76d266f (the delivery wire
contract of C5-1d, the follow-ups and the drafts and links work of C5-2a,
and the delivery defect fixes).

Conflicts and resolutions:
- docs/refactor/CONTRACT-CHANGES.md, one content conflict where this
  branch's nine C4-1b rows and refactor/v1's C5-1d and C5-2a rows were
  both appended at the end of the same table. Resolved as the union: the
  refactor/v1 rows first, then the nine C4-1b rows, every row of both
  sides exactly once. Verified by script: ours 227 rows, theirs 258,
  shared base 218, merged 267, the sorted merged rows equal the sorted
  union of both sides, no duplicate row.

Generated files, regenerated from their sources, never hand merged:
- core/api/openapi.yaml by go run ./api/tools/merge (319 paths, 406
  operations, 511 schemas)
- core/api/ROUTES.txt by go run ./cmd/census -write (406 routes)
- web/packages/api-client/src/schema.d.ts by openapi-typescript over the
  merged openapi.yaml

Signed-off-by: colton <colton@futurebuild.ai>
Both were last recorded in the C2-4 era; this branch's own 7.4 work
(the approve posting through gl.PostEntry, the payment seam rewrite, the
aging from amount_open) post-dates that recording, so the two knock-on
goldens drifted. The refactor/v1 merge changed no AP, GL, dashboard,
reports or seed line (verified by diff 52bff83..76d266f on those
directories: empty), so the merge itself changed no recorded shape here;
every other golden, including all the delivery ones the merge brought,
re-recorded byte identical under -update.

- gl_accounts_entries.json: the ap_payments group's approve now posts
  the VENDOR_INVOICE entry for bill AP-000006 (10000 cents), which no
  earlier recording held, so every later entry number shifts by one
  (143 to 144 and the reversal memo follows) and the positional id
  tokens renumber; the payment postings' memo casing follows the seam
  rewrite (Vendor Payment to Vendor payment, the old sync functions
  deleted).
- clockwindow.json: the AP aging reads amount_open, so the group's
  fully paid 100 dollar bill (paid 40 then 60) owes nothing in the
  current bucket: BC Roofing Wholesale current 10000 to 0, total
  222212 to 212212. No other response changed.

The suite ran twice after the re-record with both runs green and no
further drift.

Signed-off-by: colton <colton@futurebuild.ai>
TestMigration106_OnTheSeededDatabase applies the repository's own seed
(DEMO_SEED=1, the seed binary, the scratch database's URL on its
command) and then migration 106 over awkward legacy bills written on
top of it, following migration 099's pattern: only this proof reaches
the branch backfill's purchase order source, because the seed writes
purchase orders on the yards while every fixture row falls to the
default branch.

The test went red first and found a live defect in the migration: the
guard compared information_schema's table_schema to the literal string
'current_schema', never to the schema's name (migrations 092 and 094
call current_schema()), so the purchase order branch backfill was dead
code and every bill took the default branch. The guard now calls
current_schema(). The migration is this branch's own and unmerged, so
the file is corrected rather than followed by a fix migration.

The test holds: a bill tied to a real seeded purchase order takes that
order's branch (and keeps it through the down-up cycle), the duplicate
pair suffixes in (created_at, id) order, the orphan po_id is nulled,
lowercase statuses normalize, every row ends with a unique AP- number,
a branch, a currency and amount_open at total less paid, a second up
is a no-op, and the down then up over the seeded database loses no
row.

Signed-off-by: colton <colton@futurebuild.ai>
The id order half of ADR 0008 7.4's payment line (section 9's lock
order) had no biting test: the concurrency proof's payment race names
one bill, where every lock order agrees. TestTx_PaymentLocksInIDOrder
races three payments across the same three approved bills named in
rotated orders, five rounds at pool size 4, and holds that every
contender succeeds, the cents apply exactly once and the control
account ties after each round. Red first: with the id sort removed the
requests lock in caller order and Postgres cancels a contender with
deadlock detected, SQLSTATE 40P01, in round 0.

Signed-off-by: colton <colton@futurebuild.ai>
The merge with origin/refactor/v1 brought six unformatted files
(units_report_test.go, chain_c5_2a_test.go, serve.go, feed_test.go,
levels_embed_test.go, unit model.go); they are unformatted at the
integration head itself, and no CI job checks gofmt there. This commit
is formatting only, no line of code changes, so this branch's tree is
gofmt clean as its gates require.

Signed-off-by: colton <colton@futurebuild.ai>
The approve handler compared and reloaded invoiceId, a name that
survived an earlier draft of the page; the compiler sees only invoice,
so the desk, the local stack image and the Playwright bundles all
refused to build (TS2552 on two lines). The handler now uses
invoice.id, and the workspace build, lint and test run green.

Signed-off-by: colton <colton@futurebuild.ai>
…one with none

A bill the base approved holds its journal entry (the removed sync posted it
with source VENDOR_INVOICE and source_ref_id the bill) but no pointer to it,
so after 106 every legacy approved bill had gl_entry_id NULL and its void
posted no reversal: the bill died owing nothing while the credit stayed in
2010 (both reviews' P2-1).

106 now links every approved or partial bill to its newest posted, unreversed
VENDOR_INVOICE entry and counts the bills left without one in a notice. The
void transition refuses an approved or partial bill with no linked entry
(409, entry_not_linked) instead of voiding it silently.

Red first: TestWire_VoidLegacyApprovedBill failed on 27dce25's code (the void
answered 200, wrote no reversal and left control 10 against open 0; the
unlinked bill voided 200) and TestMigration106_LinksLegacyApprovedEntries
failed (the link stayed null, no notice). Both pass with the backfill and the
refusal; every money act in the new test asserts the ties (entries balanced,
2010 equal to the open bills' amount_open).

Signed-off-by: colton <colton@futurebuild.ai>
The aging query summed amount_open over every branch and the payments list
showed payments applied to other branches' bills, while every other AP read
and write held the wall (both reviews' P2-2).

The aging query now carries the same wall predicate the list uses, and the
payments list filters through the bills a payment applies to (a payment whose
every bill is outside the wall is not the caller's to read).

Red first: TestWire_BranchWall failed on the previous code (the aging total
behind the wall was 1610 for a caller whose branch holds 1010, and the
payments list behind the wall showed the other branch's payment). Green with
the two predicates; the test now covers a user bound by user_locations and a
key bound to one branch, on aging, the payments list and the payment act
itself, with the GL ties asserted after the payment.

Signed-off-by: colton <colton@futurebuild.ai>
…ready holds

The suffix pass wrote X #2 without checking whether the vendor already had a
bill numbered X #2, so a genuine suffixed number beside duplicate X rows made
the UNIQUE fail and rolled the whole migration back, blocking the upgrade
(review r2 P2-3; the down's own comment names the case).

Each suffixed row now takes the next suffix no row of that vendor holds, so
the duplicates of X beside a genuine X #2 become X #3 and X #4.

Red first: TestMigration106_OnAwkwardLegacyRows, grown with a genuine DUP-1
#2 beside three DUP-1 rows, failed on the previous code (could not create
unique index vendor_invoices_vendor_number_key). Green with the next free
suffix, the genuine number kept.

Signed-off-by: colton <colton@futurebuild.ai>
The apply loop walks invoice_ids with the locked snapshot taken before the
first application, so a repeated id re-applied a bill off stale numbers and
the amount_open CHECK turned the client mistake into a bare 500 (review r1
P2-3, review r2 P2-4).

PayRequest.Parse now refuses a repeated id with a 400 on invoice_ids[i]
(appears twice) before any money moves.

Red first: TestWire_PaymentRefusesDuplicateInvoiceIDs failed on the previous
code (pay 12 cents with [A, A] against a 10 cent bill answered 500
internal_error). Green with the check; the test asserts nothing is written and
the GL ties hold after the refusal and after a following clean payment.

Signed-off-by: colton <colton@futurebuild.ai>
The bill is 10 cents (price 1000 ten thousandths); the refused duplicate asks
12 cents and the clean following payment 10, not 1200 and 1000.

Signed-off-by: colton <colton@futurebuild.ai>
The payment request's amount was float dollars converted with one silent
rounding (10.005 became 1001 cents, 0.1 + 0.2 became 30), the one float left
on the AP money paths (review r2 P3-4, promoted by the lead: ADR 0008
section 11 lists payments and aging as converted).

The request now takes amount_cents, int64 cents like every other money field:
a decimal is a 400 naming amount_cents and the old float amount field is
refused as unknown. The response's amount stays cents as it already was, and
the rest of the payment request shape keeps its today form until the payment
routes' own conversion. The desk AP page sends cents (its form still speaks
dollars to the clerk, converted once and exactly on submit), and the CONTRACT
-CHANGES row records the change.

Red first: TestWire_PayAmountIsCents failed on the previous code (amount_cents
was an unknown field, 400, where 201 was wanted). Green with the new field;
the golden ap_payments is re-recorded for exactly the five pay request bodies
(amount 40, 60 and 1 dollars become amount_cents 4000, 6000 and 100), every
response unchanged.

Signed-off-by: colton <colton@futurebuild.ai>
…lows the trigger's rule

A hand-written CANCELLED bill became PENDING owing its whole total, and the
amount_open backfill zeroed only VOIDED, so a short-paid PAID bill migrated
still owing and an overpaid PARTIAL one silently clamped (review r2 P3-1 and
P3-2).

Cancel-like values (CANCELLED, CANCELED) now map to the module's voided state
owing nothing, and the backfill uses the insert trigger's rule: zero on a
voided or paid bill, the total less paid otherwise. A notice names the
short-paid PAID and overpaid PARTIAL rows so an operator can mend them.

Red first: TestMigration106_OnAwkwardLegacyRows, grown with a CANCELLED, a
short-paid PAID and an overpaid PARTIAL row, failed on the previous code
(CANCELLED read back PENDING with 60.00 open, PAIDSHORT 6.00 open). Green
with the mapping and the rule.

Signed-off-by: colton <colton@futurebuild.ai>
An approve whose control account (2010) was missing, or a payment whose
entry debits it, answered 500 internal_error: nothing was written, but the
wire told the operator nothing about the gap to mend (review r2 P3-3).

gl wraps the refusal as ErrAccountMissing and both AP postings map it to a
409 blocker account_missing naming the code.

Red first: TestWire_ApproveAccountMissing failed on the previous code (the
approve answered 500 with no 2010 in the chart). Green with the mapping; the
test covers the approve and the payment, asserting nothing is written and the
ties hold.

Signed-off-by: colton <colton@futurebuild.ai>
The module recipe says a create that rolls back abandons its number and never
reuses it; the number comes from the sequence DEFAULT, so the behaviour was
already right, but nothing proved it (review r1 P3-1).

TestTx_RolledBackCreateAbandonsItsNumber creates a bill, lets a create fail
after its insert (the event write fails), and shows the sequence stands past
the minted number with only the committed bill present, and that the next
bill takes a later number. No behaviour change: the test went green on
arrival, which is the point of a proof of existing behaviour.

Signed-off-by: colton <colton@futurebuild.ai>
…one scaling helper

The bill form silently truncated a quantity or unit price finer than the
wire's four decimals (1.00005 filed as 1.0000), and exactLineCents repeated
scaledTenThousandths byte for byte as an inner function (review r1 P3-3).

exactLineCents now calls scaledTenThousandths, and the submit validation
refuses a quantity or unit price that does not round-trip at scale 4.

Red first: the new vitest case failed on the previous code (the bill with a
1.00005 price was filed, truncated to 10000 ten thousandths). Green with the
refusal; all 18 AP page tests pass.

Signed-off-by: colton <colton@futurebuild.ai>
ListVendorInvoices listed the headers with a Limit of 100000 and then read
every bill whole with one Get per row: an N+1 read on every match run, in
code this PR itself adds (review r1 P3-5, judged worth fixing because the
read moved into the AP module here).

Repository.ListFull reads the page of summaries and then every listed bill's
lines in one query, and ListVendorInvoices filters by storage status in Go.
Same answers, two queries whatever the page holds; the matching tests and
the ap suite pass unchanged. No red first: the behaviour is identical, only
the query count changed.

Signed-off-by: colton <colton@futurebuild.ai>
One row per behaviour change this fix round makes: the entry_not_linked void
refusal, the branch wall on aging and the payments list, the repeated
invoice_ids 400, the account_missing 409, the migration 106 mapping fixes,
and the amount_cents conversion beside its code change. One note records the
documented boundary r2 raised: a legacy pending bill with no lines, or a line
with no account, can never be approved and voiding is its only way out, until
line editing arrives.

Signed-off-by: colton <colton@futurebuild.ai>
… a 500

Review r4 P2-1: the base's approve posted a one leg entry (the Accounts
Payable credit with no debit; it loaded no lines), migration 106 linked
it, and the void's reversal mirror was refused as unbalanced, so voiding
every base approved bill answered 500.

Migration 106 now links only entries whose debits equal their credits:
the newest posted, unreversed, balanced VENDOR_INVOICE entry of each
approved or partial bill. A bill whose only entry has one leg stays
unlinked and a notice of its own names it (post the missing debit legs,
then link), apart from the bills with nothing to link. The void maps
gl.ErrUnbalanced to a 409 invalid_state_transition with the
entry_unbalanced blocker and writes nothing; the fragment, the assembled
openapi and the CONTRACT-CHANGES row say so.

The migration test seeds the base's real one leg shape (one credit leg,
no debit) and asserts both the refusal to link and the notice; the wire
test rewrites an approve entry into that shape and asserts the refused
void leaves the bill, the entry count, the only unbalanced entry (the
seeded one) and both sides of the tie untouched.

Signed-off-by: colton <colton@futurebuild.ai>
Review r4 P2-2: the base's create validated no sign, so a vendor credit
entered as a negative bill is a plausible legacy row; for it amount_open
became 0, the range CHECK 0 <= total failed, and the whole migration
rolled back, blocking the upgrade.

Chosen shape: map, not refuse. The migration's rule everywhere else is
that an awkward legacy row never blocks the upgrade (duplicates are
suffixed, orphans nulled, cancel-like statuses mapped, each with a
notice), and a refusal, however well named, still blocks every table
behind one bad row. A negative total bill owes nothing as a bill: it
becomes VOIDED with open 0 and a notice naming the count and telling the
operator to re-enter it as a credit, exactly the cancel-like mapping. The
range CHECK's upper bound (never more than the total) now holds only
while the bill owes: a voided or paid bill is zero by rule at any total,
so the constraint admits the mapped rows and a negative total already
VOIDED. The down keeps the mapping with the other normalized statuses.

The awkward rows test seeds a PENDING -10.00 bill (mapped, noticed) and a
VOIDED -5.00 one (kept, admitted by the CHECK) and asserts the migration
applies, the statuses, the open amounts and the down/up cycle. The
CONTRACT-CHANGES migration row gains the mapping and the CHECK's carved
shape, and its link clause takes the balanced wording the previous
commit introduced.

Signed-off-by: colton <colton@futurebuild.ai>
Review r4 P2-3: the walled payments list needed an application row on a
bill inside the wall, so a legacy payment with no application rows (the
base wrote one whenever invoice_ids was empty) vanished from every caller,
an administrator with no header, an unbound key and a single branch install
included. The wall now filters only a walled caller; an open wall lists
every payment. TestWire_BranchWall seeds an applicationless legacy payment
and asserts both callers (red on the old query, green now). CONTRACT-CHANGES
states the rule, the cross branch payment shown whole, the account_missing
row's true code (409 conflict, review r3 P2-1) and points the superseded
older rows at their review fix rows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: colton <colton@futurebuild.ai>
Review r3 P2-2: ListFull had no test that could fail. Two bills of
different line counts, each carrying its own lines in order, with and
without the status filter; it fails with AND false on the lines query.
The approve in it is followed by the GL ties.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: colton <colton@futurebuild.ai>
Review r4 P3-a: an amount_cents past the payment column's NUMERIC(12,2)
(the int64 maximum included) is a 400 naming amount_cents, not a 500
numeric overflow; TestWire_PayAmountIsCents asserts it with the ties after
(red before: 500).
Review r4 P3-b and P3-c: migration 106 raises a notice counting the VOIDED
bills (hand cancelled or negative total) that still hold a posted,
unreversed entry, and another counting the bills posted more than once.
ADR 0008 7.4 does not ask the migration to reverse a cancelled bill's
credit, so it is counted for an operator, not reversed. The migration test
asserts both counts (red on the old migration).
Review r4 P3-f: the stray SyncVendorInvoice comment heading
PostTillOverShort is removed. Review r3 P3-1: the older CONTRACT-CHANGES
rows point at the review fix rows that supersede them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: colton <colton@futurebuild.ai>
futurebuildai and others added 6 commits October 10, 2026 12:22
The one leg void fix changed an openapi description; the Frontend job's
drift check (openapi-typescript --check) failed on the stale generated file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Signed-off-by: colton <colton@futurebuild.ai>
Signed-off-by: colton <colton@futurebuild.ai>
…sals state the limit

A bill that prices to zero approves with no journal entry, and the void
refused it for ever with entry_not_linked (review r6 P2-1). The void now
refuses only a bill that owes and has no linked entry. The refusal texts,
the transitions description and the CONTRACT-CHANGES row no longer name an
operator step that does not exist (review r5 P2-1): no route links an entry
to a bill yet, so such a bill stays approved and payable until a later
repair item. TestWire_VoidZeroBill covers a zero price and a price that
rounds to 0 cents, with the ties after each act.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: colton <colton@futurebuild.ai>
…nd negative paid bill, and state the range rule exactly

A hand edited negative amount_paid on a positive bill opened the bill past
its total and aborted the migration on the range CHECK (review r6 P3-1):
amount_open is now clamped between zero and the total, and a notice counts
such rows. The overpaid notice counts APPROVED bills as well as PARTIAL ones
(review r6 P3-3). The CHECK says what the code means: a voided or paid bill
owes zero, any other owes between zero and its total (review r5 P3-2).
TestMigration106_OnAwkwardLegacyRows seeds both rows; it failed on the old
migration with the abort, and each of the clamp, the notice filter and the
CHECK fails it alone when reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: colton <colton@futurebuild.ai>
The bill create summed its lines and took the tax with no bound, so a tax,
unit price, line total, subtotal or total past its NUMERIC column answered
500 on a numeric overflow (reviews r5 P3-1 and r6 P3-2, the create twin of
the payment ceiling). Each is now a 400 naming the field that tipped it over,
with nothing written. TestWire_CreateMoneyCeilings failed on all six cases
before the change (500 each) and creates a bill near the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: colton <colton@futurebuild.ai>
Signed-off-by: colton <colton@futurebuild.ai>
@futurebuildai
futurebuildai merged commit 62de27e into refactor/v1 Oct 10, 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