Skip to content

C2-5: POS and till on the contract (ADR 0005) - #72

Open
futurebuildai wants to merge 108 commits into
refactor/v1from
refactor/c2-5-pos-till
Open

futurebuildai wants to merge 108 commits into
refactor/v1from
refactor/c2-5-pos-till

Conversation

@futurebuildai

@futurebuildai futurebuildai commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

C2-5: POS and till (ADR 0005 section 14.2, migration 105 pos_wire_contract)

The counter onto the wire contract: lines in the shared salesdoc shape, cents tenders, the walk-in customer, and a completed sale as ONE transaction (stock out, the invoice posted through the AR core with tax and COGS, one payment per tender applied and stored net of change, an ACCOUNT tender open under the credit check). The void of the completed sale while its drawer is open reverses exactly what it posted; a return is a credit memo created, posted, refunded and restocked in one act. The post commit best effort GL calls, the fake product line under a split tender, and the hard coded USD card charge are gone.

14.2 C2-5 lines, each with its test (fail first: every behaviour below was red against the base commit's shapes or refused; the carried WIP proofs were fixed red first in 88ba873/d7fffde)

ADR 14.2 line Test
Lines in the shared shape (product, charge and text at the counter, typed override and discount with audit, kits exploding) TestSaleWireShape (the audit rows each adjustment owes asserted beside the shapes)
Cents tenders TestSaleWireShape, TestSplitTenderSalePostsEveryEntryInTheSaleTransaction (a decimal tender is a decode error, never a rounding)
The walk-in customer; ACCOUNT refused for it TestWalkInSaleRefusesAccountTender, and migration 105's walk-in in TestMigration105_POSWireContract
The completed sale one transaction: stock out, invoice (origin POS, pickup) with tax and COGS, tenders as payments applied and stored net of change, ACCOUNT open under the credit check TestSplitTenderSalePostsEveryEntryInTheSaleTransaction, TestChangeIsNeverSubtractedTwice
A failing event or audit write rolls every write back (no invoice, payment, stock move or entry) TestFailingEventWriteRollsTheSaleBack (both the completion and the void; the gapless series spends nothing)
The void while the session is open, one transaction in section 11's order (sale, payments, invoice); the card refund at the gateway before it TestVoidOfACardSaleLeavesTheLedgerAtZero, TestVoidAfterSessionCloseIsRefused
After the session closes it is a return TestVoidAfterSessionCloseIsRefused
A return is a credit memo created and posted in one act with its restock lines, refunded cash or card or left as account credit TestReturnRestocksAndReversesCOGS, TestAccountReturnLeavesOpenCredit
A split tender posts one invoice entry, one receipt per tender, one application entry per tender TestSplitTenderSalePostsEveryEntryInTheSaleTransaction
The till's expected cash equals the session's cash payments (net of change) plus the float less cash refunds; change never subtracted twice TestTillExpectedCashIncludesFloatAndRefunds, TestChangeIsNeverSubtractedTwice
A void of a card tender sale refunds through the gateway and leaves the ledger at zero TestVoidOfACardSaleLeavesTheLedgerAtZero
A return restocks and reverses COGS TestReturnRestocksAndReversesCOGS
The offline sync completes each sale through the same path TestOfflineSyncPendingOnTaxFailureCompletesOnRetry (found the sync broken: the completion carried no revision, so no sale ever synced; the golden had pinned the breakage) and the pos_sync golden (synced, duplicate, underpaid)
An offline sale synced while the tax provider fails stays pending and completes on retry, never rejected TestOfflineSyncPendingOnTaxFailureCompletesOnRetry
Contention: three registers at pool 4 get consecutive invoice numbers, no gap, no deadlock against a walk-in payment TestThreeRegistersConsecutiveNumbersAgainstAWalkInPayment
Two sales of the last unit: one completes, one refused insufficient_stock TestTwoSalesOfTheLastUnit
A void racing a return: one wins, the other refused TestVoidRacingAReturn (found both could win: the return now locks the sale row first and moves its revision)
The migration turns each historic ACCOUNT return into an open credit memo TestMigration105_POSWireContract (memo carries the return's own entry, no new journal or subledger row; awkward legacy rows, down, up, idempotent; the seeded database leg ran on the container at 104 + seed + 105)
The AR invariants of 9.3 after every act, proved by ledger sums fixture.assertARInvariants runs after every money test; the split tender test asserts the entry legs (1020/4010/2020/1030/5010, 1010/2200, 2200/1020 per tender)

14.1 inputs proved

  • IN-1.1 one lowercase status: TestSaleWireShape (IN-1.1 tag in the source)
  • IN-1.2 integer cents, never floats: the tenders, totals and floats across the wire tests; a decimal tender is a 400
  • IN-1.3 POS- and RTN- numbers on create and every read: TestSaleWireShape, TestReturnRestocksAndReversesCOGS
  • IN-1.4 the list envelope, [] in the bytes: TestSaleListEnvelopeAndFilters
  • IN-3.7 every refusal the error envelope with its blocker; the revision precondition: TestErrorEnvelopeAndRevision, and the blocker vocabulary (insufficient_tender, change_from_cash, walk_in_account, card_terminal, card_declined, tax_quote_stale, tax_rate_not_configured, credit_limit, insufficient_stock, session_closed, no_invoice, the 503 on a provider outage) is pinned by the wire tests and the goldens

Beside the tests

  • Migration 105 pos_wire_contract + down (the ADR 13 C2-5 steps), CONTRACT-CHANGES rows for every route and behaviour, the pos contract fragment rewritten, make contract green, the api-client regenerated.
  • The five pos goldens re-scripted and re-recorded (split tender completion against the 12 percent branch rate, the 428 and stale 409, the strict query refusals, the counter's own events read from the feed, the Z report payload masked); six other goldens re-recorded because the counter's sales now really consume stock and post events, and the harness now sorts a cursor envelope's items as it sorted the legacy data. Stability proven by consecutive runs.
  • The desk's POS terminal on the new shapes: cents tenders with a split tender list, the revision carried as If-Match, a return modal, the void with its reason. Playwright walks the split tender sale, the return and the void through the real stack; screenshots in shots/c2-5/.
  • The wire bugs the fresh contract, goldens and e2e exposed, all fixed: the typeahead and catalog queried a nonexistent products.is_active (500); a walk-in sale priced through the engine with an empty customer id (500); till/current answered 200 with a null body; fresh sales and empty maps/lists read back null; the split tender's payments read in a random order; the return's parse demanded a product beside the line it named; an unknown line or product reached the insert; the sync never completed a sale; the void and the return could both win.
  • The seed repoints REG-01 at the Kelowna branch (migration 027 plus 066 left it on MAIN, which carries no tax rate, and the counter refuses a sale on a branch without one).

Fix round 4 (seventh and eighth review findings)

The refund intent table is the one authority on what a card tender has refunded or may be refunding. Every refund act consults it under the sale row lock (the close: under the session lock) before any gateway call or money move.

  • Claim after a committed void or card return never reaches the terminal (zero gateway refunds, counted by the stub gateway): TestACardReturnAfterACommittedVoidNeverReachesTheTerminal, TestAVoidAfterACommittedCardReturnNeverReachesTheTerminal.
  • A cash return is refused 409 refund_in_flight while a card intent of the sale is PENDING; the card return claims the figure the lock sees and the money transaction refuses (refund_amount_changed, the intent staying PENDING beside the orphan row) when its restated total differs from the amount the terminal took: TestACashReturnWhile...Refuses, TestTheCardReturnBooksWhatTheGatewayTook.
  • The close is refused 409 refund_in_flight while any intent of the session's sales is PENDING: TestTheCloseRefusesWhileAVoidIsParkedAtTheGateway.
  • Only a definite refusal status releases a claim; an error with no verdict leaves it PENDING with a pos.refund_outcome_unknown audit row (the gateway interface has no idempotency key parameter): TestATimedOutCardRefundLeavesItsClaimStanding.
  • A stranded PENDING claim: the gateway's refund id is written onto it in its own committed write, an index on (state, created_at), and a guarded admin route POST /api/v1/pos/refund-intents/{id}/resolve (finance and owner only, a machine key refused in the route itself, bound or not; PR 83's key allowlist is not merged here). A recorded refund books through the act's own money transaction; no_refund with a reconciliation reference marks the claim FAILED and moves nothing; one audit row names the person, the intent, the outcome and the reason. ADR 0005 paragraph and CONTRACT-CHANGES rows added.
  • The card cap and the legacy tax reads now have tests that fail under their negated and plain sum mutants; a partly legacy line completes with the linked return that brings the rest.
  • Migration 105: the down refuses (an exception naming the count) while a completed sale holds a kit, component, charge or text line; the backfill pairs product lines only.
  • The declined later charge reversal loop, the intents a void leaves DONE, and the void claim's status recheck are tested; every request and every wait in the pos tests now has a deadline, so a stuck contender fails instead of hanging the package.
  • The generated api-client types follow the new route.

Fix round 5 (ninth and tenth review findings)

Each finding was red first (a test that fails on the previous head), and every proof gap closes with a mutant that turns its test red.

  • Every refund method but the card's own (cash, ACCOUNT and any other) is refused 409 refund_in_flight while a card intent of the sale is PENDING, before the return prices and again under the sale row lock: TestAnAccountReturnWhileACardReturnIsParkedRefuses, TestAnAccountReturnWhileAVoidIsParkedRefuses (both answered 201 before).
  • A void resolution accounts for each card tender separately: it books only the resolved tender's refund, and the sale's void completes only when no claim of the sale is PENDING anymore, a declined tender keeping its charge as unapplied credit owed to the customer. On the two card sale where the terminal refunded one card and declined the other, the books move exactly the refunded card: TestAVoidResolutionOfOneTenderOfTwoBooksOnlyThatTender, TestTheStrandedVoidOfTwoTendersResolvesOneTenderAtATime. A partly resolved void refuses every later return of the goods (409 void_refund_recorded): TestAGoodCannotBeReturnedAgainAfterAPartlyResolvedVoid. The refusing alternative was not taken: it deadlocks the shape where every card's refund moved and every claim carries its id.
  • The resolve route runs behind the branch middleware (serve mounts it through the branch wall at finance and owner), the claim's sale is read through the caller's branch context on both legs, and the no refund leg takes the same scoped sale row lock: a finance user of another branch finds a missing row and moves nothing: TestTheResolutionStaysInsideTheCallersBranch, TestTheResolutionRouteRunsTheBranchGuardItIsGiven, and the serve wiring legs in TestBranchWall_ServeWiring.
  • The claim row is locked inside the resolution's transaction, and every claim state change (Complete, Fail, FailByID) refuses 409 intent_not_pending when it settles no row; a lost replay answers the race whatever stage it loses at. Three concurrent resolutions (two refunds, one no refund) at pool 4 settle the claim once: TestThreeConcurrentResolutionsOfOneClaimSettleItOnce.
  • The outcome cannot contradict the claim's own record: a no refund over a claim carrying the terminal's refund id is 409 refund_recorded, a differing id is 409 refund_tx_id_conflict: TestANoRefundOverARecordedRefundIdIsRefused, TestARefundIdThatDiffersFromTheRecordedOneIsRefused.
  • Only the terminal's explicit DECLINED releases a claim; a PENDING or ERROR answer with no error leaves it standing with the unknown outcome audit row, for the void and the card return alike: TestANonVerdictStatusLeavesTheCardReturnClaimStanding, TestANonVerdictStatusLeavesTheVoidClaimStanding, with the void's timeout and decline paths proved the same way.
  • The round 4 proof gaps: the claim restates the remainder it is asked for and books (1794, never the naive 1795; the stub gateway records the cents of every call), a remainder moved in the parked window refuses refund_amount_changed with the claim standing and the replay refusing the same way, and the void leg's resolution audit row rides its booking's transaction: TestTheCardClaimIsAskedForAndBooksTheRestatedRemainder, TestAMovedRemainderRefusesTheBookingAndKeepsTheClaim, TestTheVoidResolutionsAuditRowRollsBackWithItsBooking. Mutants: the amount changed check, the audit write, the void timeout release, the void decline release, the legacy tax completion and the backfill's invoice side filter each turn exactly the named test red. The restate inside the claim transaction and the money transaction's cap, DoneVoid and held rechecks stay equivalent in any deterministic test (the intent table's own guards answer first in every ordering a test can fix); the money transaction's restate, whose refusal the moved remainder test kills, is the authority.
  • The P3s: the resolve writes no orphan row on a refused replay; the close's refusal names the standing claims and the resolve route; the 105 down refuses while any claim is PENDING (dropping the table would release its tender) and counts voided sales beside completed ones, said in both files' notes; the route fragment states the conflict refusals, the branch read and the dev mode empty actor.
  • ADR 0005's intent table paragraph and the CONTRACT-CHANGES C2-5r4 rows state the new rules; the merged openapi and the api-client types are regenerated.

Fix round 6 (r12 findings)

Each finding was red first (the new test shown failing against the unfixed code), and the gateway's call counts are asserted in every proof.

  • A split void completes whatever order its claims are resolved in (r12 P1-1). The completion (cash and check payments voided, every live card application reversed to unapplied credit, invoice voided, stock back, sale VOIDED, event and audit) is one function both legs of the resolution call, under the same sale and claim locks: the refund leg after its own tender's booking, the no refund leg when its release empties the sale's standing claims beside a void claim that already booked a refund. The drawer's session is held in the no refund leg the same order the refund leg holds it. A void whose every claim settles without a booked refund keeps the sale completable by a fresh void. TestASplitVoidEndsTheSameStateWhateverOrderItsClaimsAreResolved resolves r12's two and three card shapes (M3, M5, M6, M10 with cash) in EVERY order and asserts one identical end state per shape (sale status, 1010, 1020, 2200, the walk-in's balance, stock, the drawer's expected cash and card, the stub gateway's call count); TestAGoodCannotBeReturnedAgainAfterAPartlyResolvedVoid now expects VOIDED and the no refund tender's unapplied credit.
  • The resolution holds the sale to the caller's grants, header or not (r12 P1-2). With multi_branch_enabled on and default_branch_required off, a finance user held to branch B sending no X-Branch-Id header read the sale through a repository filter that never fired and settled branch A's claim. The route now holds the claim's sale to the caller's grants through the middleware.BranchGuard serve already passes the document and purchase order handlers (WithBranchGuard on the pos handler, wired in serve and the test fixture), whenever the context carries no branch and the caller is not admin or owner: a sale outside the grants is the same missing row (404, nothing moved) on both legs. TestTheResolutionHoldsTheSaleToTheCallersGrantsWithoutAHeader: both legs, both acts (the return claim and the void claim), a granted finance user resolving with no header, and a later card return and fresh void at the sale's branch proving exactly one terminal call per sale in the end.
  • A void race loser is told the race (r12 P3-1): the claim row is locked and read right after the sale lock, before every guard of the booking, so the loser of a same outcome race answers 409 intent_not_pending, never invalid_state_transition (the winner's VOIDED sale). TestTwoConcurrentRefundResolutionsOfAVoidClaimNameTheRace: one 200, one intent_not_pending, the sale VOIDED, one refund row, no gateway call.
  • The till report's CARD figure subtracts booked void refunds (r12 P3-2), as the CASH figure does for returns: the figure names the session's still completed sales' card tenders less the card refunds resolutions booked (written straight against the tender's payment), and drops with the tender when the void completes. TestTheTillReportCardFigureSubtractsBookedVoidRefunds (the M10 shape: 400 before, 200 with the refund booked, 0 once the void completes). The CARD entry is written only where the session collected on cards, so a cash only session's report and the close's stored map carry no card figure: TestTheTillReportCarriesNoCardFigureForACashOnlySession.
  • ADR 0005's resolve sentences and the CONTRACT-CHANGES C2-5r4 rows state the round's rules (the completion from either leg, the order independence, the branch hold for a context with no branch, the claim read right after the sale row, the CARD figure's net).

Fix round 7 (r11 findings: P2-1 and the P3s)

Every proof reaches its recheck through a stand-in row written in the window between an unlocked read and the lock the act takes (a sale row held FOR NO KEY UPDATE, the refund parked at the terminal, the money transaction parked on the till session row it takes between its two reads of the intent facts, the replay parked on the sale row), and every recheck's mutant turns exactly its named test red. No recheck was deleted: none is unreachable through a write.

  • r11 P2-1, the claim's restate: TestTheClaimRestatesItsRemainderUnderTheLockBeforeTheTerminalIsAsked. A remainder moved between the pricing and the claim (an unlinked return line through the real tables): the terminal is asked for the restated 1794, never the naive 1795, the return books and the books tie. The mutant (the claim transaction's restate off) reddens it: the terminal is asked the stale figure, the money transaction refuses refund_amount_changed beside the money that moved.
  • r11 P2-1, the money transaction's card cap and held figure: TestACardReturnPastTheBookedRefundsRefusesUnderTheLock (a booked CARD return row in the parked window; 409 exceeds_card_tender, the claim stands PENDING beside its orphan row, nothing booked) and TestAClaimWhoseTableAmountMovedRefusesTheBooking (the claim's own row amount moved in the parked window; 409 refund_in_flight). Each mutant books 201 instead.
  • r11 P2-1, the money transaction's tender and pending facts: TestATenderTheVoidRefundedBetweenTheFactsReadsRefusesTheBooking (a void claim of the tender stands DONE between the transaction's two facts reads: 409 tender_refunded) and TestAClaimSettledBetweenTheFactsReadsRefusesWithTheRaceNamed (the claim settles between the reads: 409 intent_not_pending, never the misleading refund_in_flight the later check would answer). The mutants book 201 and answer the wrong code.
  • r11 P2-1, the replay's pre-cap facts: TestAReplayWhoseClaimWasWonUnderItAnswersTheRaceNeverTheCaps (a winning resolution's whole footprint, the settled claim and the return rows its booking wrote, lands between the replay's read and its transaction: 409 intent_not_pending before the caps, never the winner's own exceeds_sold) and TestAReplayUnderAVoidRefundedTenderAnswersTheTenderNeverTheCaps (a void DONE claim beside that footprint: 409 tender_refunded, the intent table's authority outranking the figure guards). The mutants answer the caps' code and the wrong race code; a refused replay writes no orphan row and no unknown outcome row but the stranded act's own.
  • r11 P3-1: TestThreeConcurrentResolutionsOfASplitVoidsClaimsSettleEachOnce. Three contenders at pool 4 over a stranded two card void's claims (the refunded claim recorded once, the timed out claim fought by a refund and a no refund): exactly one 200 per claim, the loser 409 intent_not_pending, the books move once per settled refund (the booked total read from the claims, because which tender carries the terminal's refund follows the tenders' own order), the void completes exactly once whichever leg empties the claims, and the gateway sees only the void's own two calls. The stacked mutant (the route's read, both legs' in-transaction state checks and both row-count refusals off) reddens it: the fought claim settles twice.
  • r11 P3-2 and P3-4: the ADR 0005 resolve paragraph and the CONTRACT-CHANGES C2-5r4 resolve row state the interim state (one tender booked, a sibling PENDING: the booked refund reversed its application, so the invoice is open again for the booked cents and the customer's balance carries them as owed until the last claim resolves and the void completes, the close refusing while a claim of the session is pending, so the interim never slips into a counted drawer) and name a no refund of the last claim as a completing leg explicitly.
  • r11 P3-3: refusing the declined tender's credit at the payment apply route is not sound, so the limit is stated in the ADR (unapplied cash on one payment is fungible: a genuine overpayment and a void's reversal mix on the same row, so no refusal can be drawn at the cent level and refusing the row would refuse the overpayment too; the apply rule is the account core's own, per customer, with no origin to read; on the shared walk-in customer the exposure is named: a later walk-in invoice could absorb the credit, the dealer's duty is to refund the card). The way back is proved end to end: TestADeclinedVoidTendersCreditIsPaidBackToItsCard pays the declined card's cents back through the payment's own refund route at the terminal (the fixture wires the payment route's gateway the way serve wires Run Payments), the unapplied falls to zero, nothing is owed and the books tie.
  • No route, fragment or generated file changes this round: the merged openapi, the api-client types and the route census are untouched and fresh.

Fix round 8 (the fourteenth review's P1s and P2, the thirteenth review's P3s)

Every behaviour change was red first (the new test shown failing on the previous head a7a612b), the 9.3 ties asserted by ledger sums after every money act, and the stub gateway's call counts asserted in every proof. The three test-only gaps of the thirteenth review cannot be red on a correct head, so each is proved by its mutant (applied alone, the named test red, the source restored, the test green again).

  • The write path's row (the fourteenth review's P1-2, a regression the first cut made): a branch scoped request naming a location acted on the branch's row with the most quantity, so a cycle count of one yard overwrote another yard's units and a receipt landed in the largest yard. A named location now reads exactly that row, refused (403 on the stock routes, in process for the receive) when it sits in a branch the caller does not carry, never substituted; a first receipt at a yard of the caller's branch still creates its row; only a request naming no location lets the branch rule pick a row, and it takes the branch's first row by creation, a stated rule, never the largest quantity silently; an unbound request keeps the legacy unassigned row. TestAnAdjustWithANamedLocationActsOnThatLocationRowAlone, TestATransferMovesTheNamedRowsAlone, TestAnActOnALocationOutsideTheCallersBranchIsRefused, TestAReceiptAtAYardWithNoRowCreatesItThere, TestAnAdjustWithNoLocationTakesTheStatedRow, and the purchase order receive through the same read: TestAReceiptLandsInTheNamedYardNotTheBranchsLargestRow (red: the receipt landed in the big yard at the previous head, 103 against 100).
  • The claim's own terminal call (the fourteenth review's P1-1): a no refund resolution could settle a claim while its act's refund call was still on the wire, freeing the tender; the terminal then refunded it and a later act refunded the same tender again, and the drawer could close over the open call. Every claim now carries an in flight lease and an answer record (migration 107): the call is bounded, every path it can take stamps the answer (the refund id's committed write, the decline release, the no verdict record, and an abandon stamp for a tender whose call a refusal never reached), and the resolve refuses 409 refund_in_flight on an unanswered claim inside its lease under the claim's row lock; once the lease expires with no answer at all the claim is marked UNKNOWN and settles as recorded. The terminal's refund id is written onto the claim whatever state it is in, a recorded refund that never booked holds the tender against every later act, and the refund booked must be the terminal's own id. Red proofs: TestAResolutionCannotReleaseAClaimWhoseActIsStillAtTheTerminal (the resolve was 200 at the previous head and the terminal refunded the tender twice, 1198 against 599; now one refund, one card return refused, the books tie), TestTheCloseHoldsWhileAVoidsTerminalCallIsOnTheWire (the close was 200 under the open call), TestAnUnansweredClaimBecomesResolvableWhenItsLeaseExpires.
  • The interim invoice takes no application (the fourteenth review's P2-1): an application to a split void's interim invoice (the customer's own check through POST /api/v1/payments, another walk-in's declined card credit through POST /api/v1/payments/{id}/applications) left a live row the void's completion could not reverse, stranding the last claim (409 has_applications forever) and the drawer. Every application path now refuses 409 void_claim_pending naming the standing claims, before the amounts are read (the AR core asks the counter through a guard serve wires), so the void always completes and the credit stays on its own payment, payable back to its card. Red proofs: TestTheCustomersOwnReceiptCannotApplyToAnInterimVoidInvoice (the receipt was 201 at the previous head), TestAnotherWalkInCreditCannotApplyToAnInterimVoidInvoice (the apply was 200 and the credit was absorbed; now both sales and the drawer settle, the credit pays back to its card).
  • The CARD figure subtracts the session's card returns (the fourteenth review's P3-2), through their credit memos, the join the CASH figure always used: TestTheTillReportCardFigureSubtractsCardReturns (red: 1796 stayed after 1198 of card returns; now 598).
  • The Z report's payload is a JSON object again (the fourteenth review's P3-3): the first cut had changed it to a base64 string unnamed in CONTRACT-CHANGES; the fragment declares PosTillReport as before the cut, and the characterization golden stops masking the field, so the frozen report's figures are pinned (red: the re-recorded golden held a base64 string).
  • The thirteenth review's P3s, each proved by its mutant: the void that completes only beside a booked refund (TestAVoidWhoseEveryClaimSettlesWithoutARefundKeepsTheSaleCompletable; the gate's mutant voided the sale with no refund anywhere), the card refund SQL's completed sale filter (TestOneSalesBookedRefundDoesNotNetAnotherSalesCardFigure; without the filter another sale's figure lost the voided sale's 200), and the no refund leg's session hold (TestTheNoRefundLegAndTheCloseSerializeOnTheSession; the race settles as a completed void with a counted or refused then retried close, never a deadlock). The leg's session lock comment now states the two legs' actual orders and why they cannot deadlock.
  • Left for the principal's ruling (TO-PRINCIPAL 91): the ADR's walk-in credit paragraph and the payback rail (the fourteenth review's P3-1 and the thirteenth's P3-4 and P3-6) are untouched. The rechecks the fourteenth review could not reach through real writes stay as they are, covered by the r11 tests the thirteenth review proved bite.
  • Migration 107 refund_intent_in_flight_lease (+ down, both cycled on a scratch database beside the 105 cycle), ADR 0005's resolve and till sentences, CONTRACT-CHANGES C2-5r8 rows for every changed rule, the resolve and payment fragments restate the new refusals, and the api-client types are regenerated.

Fix round 9 (FROM-PRINCIPAL 60's walk-in credit rule, both parts)

Every behaviour change was red first (the new test shown failing on the previous head 92257c9), the 9.3 ties asserted by ledger sums after every money act, and the stub terminal's refund call counts asserted in every proof (the counter terminal and the online gateway counted separately: the fixture wires the payment route's gateway the way serve wires Run Payments, beside the counter's own stub).

  • The credit is held to its tender (FROM-PRINCIPAL 60a). A declined void tender's credit sat as unapplied cash on the tender's payment, and the apply route answered 200 for it, so on the shared walk-in customer a later walk-in invoice absorbed an earlier walk-in's declined card credit (the fourteenth review's Area 5 shape). The AR core now asks the counter through a held credit guard serve wires (the origin read from pos_tenders.payment_id, the voided sale and the claim the void released), and while the tender's payment still carries the credit unapplied, every path that applies a payment's unapplied cash (the apply route, a receipt's own applications, the deposit paths, all through the AR core's applyLocked) refuses 409 tender_credit_held naming the origin, the void's sale and its claim, and nothing is written. Red proof: TestADeclinedVoidTendersCreditAppliesToNoInvoice (the apply was 200 at the previous head; now 409, the credit unabsorbed, the invoice open, no live application row, the ties hold).
  • The credit's one way back is the counter terminal rail (FROM-PRINCIPAL 60b). The payment refund route answered 201 for the credit through the online gateway, the wrong rail (the thirteenth review's P3-4), and no counter route drove the terminal for it. The payback route POST /api/v1/pos/refund-intents/{id}/payback, a small sibling of the resolve route on its own mount (finance and owner only, a machine key refused in the route itself, the branch wall and the no context hold through the same sale read, audited with one pos.refund_intent.paid_back row in the resolve route's shape), keys on the void's own released claim: only a FAILED void claim of a voided sale whose tender's payment still carries the credit unapplied is paid back, exactly once (a second payback 409 no_held_credit, the terminal never asked again), every guard before the terminal is reached. Its terminal call runs under the claim discipline every terminal refund runs under: migration 110 widens pos_refund_intents.act to payback, the payback opens its own live claim before the call (a concurrent payback 409 refund_in_flight, the in flight lease holding the resolve route and the close), the terminal's refund id lands in its own committed write, and the money transaction books the credit out through the AR core's refund (the credit's unapplied cash falls to zero, the cash account moves by the refund). A terminal decline releases the claim and moves nothing (409 card_refund, the counter may ask again); a no verdict payback settles through the resolve route (its refund leg books under the terminal's own id, its no refund leg releases the claim; the resolve fragment's act enum gains payback). The payment refund route refuses a held credit 409 tender_credit_held before its online gateway is ever asked. Red proofs: TestADeclinedVoidTendersCreditIsPaidBackThroughTheCounterTerminal (the online refund was 201 at the previous head), TestAPaybackWhoseCallAnswersNoVerdictSettlesThroughTheResolveRoute and TestAPaybackTheTerminalDeclinedReleasesItsClaim (the route was a 404 at the previous head), TestThePaybackStaysInsideTheCallersBranch (the wall: another branch's caller 404 with no claim and no terminal call, a granted caller under another branch's header 403, the in branch payback 200), TestAMachineKeyNeverPaysBackAHeldCredit. No golden: the route is new and no seeded characterization data names it.
  • ADR 0005's walk-in paragraph states the rule (held to its tender; paid back only through the payback route on the counter rail; until then held), replacing round 7's fungibility argument (the thirteenth review's P3-6); CONTRACT-CHANGES C2-5r9 rows for the held credit refusal, the payback route, the payment refund route refusal and migration 110.
  • Migration numbering: this branch's in flight lease migration is renumbered 107 to 109 (git mv both files), so PR 83's 107 and 108 stay free and the two branches merge in either order. Fresh apply through 110, and the 105, 109 and 110 down/up cycles, proved on the round's container.
  • The merge of refactor/v1 (35 commits: PR 99's AP and GL changes, PR 107's contract fixes) landed with no conflicts: the merged openapi.yaml and schema.d.ts were regenerated from the fragments and the pinned openapi-typescript and are byte identical to the merge's, and CONTRACT-CHANGES keeps both sides' rows.
  • PR 83 has not merged at push time, so the key allowlist section does not apply here: the resolve and payback routes go into it as REFUSED for a branch bound key when PR 83 lands.

Migration 105 pos_wire_contract (ADR 0005 section 13 C2-5): the sale
numbered POS- and the return RTN- through sequences, currency, the
invoice link, the shared line columns on pos_line_items and
pos_return_lines with the per type CHECKs, the tender's payment link,
each historic ACCOUNT return migrated as an OPEN credit memo carrying
the return's own entry, and the walk-in customer with its setting.

The pos module rebuilt on the recipe: the sale and its lines in the
shared salesdoc shape (product, charge and text lines, typed override
and discount with audit, kit explosion), tenders in cents, the
completed sale one transaction (stock out through inventory.IssueQty,
the invoice with origin POS and pickup posted through the AR core with
tax and COGS, each cash, check or card tender a payment applied to it
stored net of change, an ACCOUNT tender left open on the invoice and
subject to the credit check), the void while the session is open one
transaction over the sale, its payments and its invoice with the card
refund through the gateway before it, the return a credit memo created
and posted in one act refunded in cash or card or left as account
credit, the till's expected cash from the session's payments plus the
float less cash refunds with the over/short entry inside the close's
transaction, and the offline sync replaying each sale through the same
path with a tax provider failure pending, never a rejection.

The card charge carries the sale's currency, never a hard coded one.
inventory.GetInventory's write path read now follows the branch wall
for a bound caller with no location (the carried fix from PR 44's
review).

Signed-off-by: colton <colton@futurebuild.ai>
The end to end fixture (a real Postgres, the invoice, inventory, GL and
account services wired as serve wires them) and the tests: the sale's
wire shape (POS- number, lowercase status, cents, the shared line shape
with charge, text, override, discount and kit lines, the audit rows
each adjustment owes), the split tender money moment (one invoice entry
with the tax and COGS legs, one receipt and one application entry per
tender, the account portion open, the AR invariants of 9.3 asserted),
the walk-in customer and its ACCOUNT refusal, change never subtracted
twice, the void (gateway refund, the ledger at zero, stock back), the
session-closed refusal, the return as a credit memo that restocks and
reverses COGS, the account credit return, the till's expected cash
from the session's payments plus the float less cash refunds with the
variance booked inside the close's transaction, the list envelope and
filters, and the error envelope with the revision precondition.

Signed-off-by: colton <colton@futurebuild.ai>
The safety copy from the interrupted run: the failing event write rolls
the sale and the void back, two sales of the last unit, three registers
against a walk-in payment, a void racing a return. Two proofs fail as
written (a count query given an argument it does not take, and a scalar
read back as the wrong type); they are fixed in the follow-up commits.

Signed-off-by: colton <colton@futurebuild.ai>
…pos-till

Signed-off-by: colton <colton@futurebuild.ai>
The four proofs fixed as written in the WIP commit: the rollback counts
run against the seeded tables the fixture shares, so the invoice,
payment and entry counts are taken before the failed act and asserted
unchanged after it (the tender and outbox counts name the sale and stay
absolute); the walk-in customer id is scanned as text (a uuid column
scanned into any is raw bytes); and the add line helper returns only the
error, the revision being a JSON number on the wire. All four proofs
pass under the race detector.

Signed-off-by: colton <colton@futurebuild.ai>
The migration test builds the schema to 104 in a scratch database,
writes the base commit's shape (two sales with old columns, old lines,
cash and card tenders, an ACCOUNT return carrying its GL reversal entry
and its line, a cash return), applies 105 and reads every backfill back:
the POS- numbers in (created_at, id) order with the raw insert numbered
past the maximum, the lines' shared shape with the per type CHECKs
biting (a text line with a price, a zero pair side, a double discount),
the RTN- numbers, the ACCOUNT return as an OPEN credit memo carrying the
return's own entry with its lines mirrored negative and no new journal
or subledger row anywhere, and the walk-in customer with its setting.

Three migration bugs it found, fixed: the guarded constraint adds caught
duplicate_object where Postgres raises duplicate_table, so a second
apply died on the first constraint; the pair and price unit backfills
clobbered the null pair of a TEXT line on re-apply and died on the shape
CHECK (they now skip TEXT lines); and the down deleted the migrated
memos while the link column's foreign key still stood (the key drops
first, the memos go by the up's own reason marker). The down drops the
columns, the memo, the text lines and the walk-in customer, returns
unit_price to scale 2, and the up applies again after it with nothing
duplicated. The seeded database leg ran on the container: migrations to
104, the demo seed, then 105, which lands the walk-in customer beside
the seeded ones.

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

The pos fragment rewritten from the module onto the wire contract: every
route's request and response shapes (the sale with its shared salesdoc
lines and tenders in cents, the returns, the till session, report and Z
report, the sync batch, the typeahead and catalog), the blockers each
refusal carries, the revision preconditions on the complete and the
void, and the schemas the shape test binds (PosTransaction,
PosTransactionSummary, PosLineItem, PosReturn, PosTillSession, the
report and sync bodies). The day's sale list is the envelope with an
empty page [] in the bytes; the typeahead, catalog, returns list and Z
report list keep their bare arrays, the counter's own helper surfaces.

Five wire bugs the fresh contract and goldens exposed, fixed: the
typeahead and catalog queried a products.is_active column that does not
exist and answered 500; a walk-in sale priced its lines through the
engine with an empty customer id and answered 500 (the walk-in customer
of record prices it now, resolved on the add); the register's open
session route answered 200 with a null body when no session was open
(it is a 404 now); a fresh sale read back with null lines and tenders
(empty arrays now, the lists are never null); and a split tender's two
payments read back in a random order, because both carry the same
transaction-stable timestamp and the id tie break is random (the order
is created_at, method, id now, so the golden is deterministic). The
demo seed repoints REG-01 at the Kelowna branch: migration 027 inserts
the register first and 066 backfills its branch with MAIN, which carries
no default tax rate, and the counter's money path refuses a sale on a
branch without one.

The goldens re-scripted onto the new shapes: the tenders and floats in
cents, the line wrapper, lowercase methods, the revision preconditions
(extracted, never counted by hand), the void of an open sale refused,
the split tender completion (cash and check against the 12 percent
branch rate) and its void, the 428 and the stale revision, the strict
query refusals, a step reading the counter's own events back from the
feed, and the Z report's payload masked (it embeds the session's own
ids and timestamps). The harness sorts a cursor envelope's items as it
sorted the legacy data array, logs a refusal's code and blockers beside
its status, and keeps the actual transcript beside a drifted golden;
six other goldens were re-recorded with it (their committed versions no
longer pass: the counter's sales now really consume stock and post
events, and the items sort fixed the order of three lists), and the
five pos goldens ran stable six times in a row after the tender order
fix.

Signed-off-by: colton <colton@futurebuild.ai>
Four more null-for-empty warts the conformance pass found in the fresh
goldens, fixed: a till session read carries its expected and counted
maps from the first read (the open constructs them, the scan fills
them), the sync response's errors and pending lists are arrays even
when empty, the day's return list is an array with each return's lines
an array, and the Z report list is an array on an empty day. The 404s
reference the lowercase WireNotFound response, the assembled contract
and the generated client are regenerated through make contract, and
CONTRACT-CHANGES carries the C2-5 rows: every counter route's before
and after, the walk-in and register-branch behaviour rows, the goldens
row and the migration row.

Signed-off-by: colton <colton@futurebuild.ai>
The types, the service and the terminal page onto the wire contract:
cents tenders and floats (a dollarsToCents helper parses the cashier's
input with string arithmetic, never parseFloat), the line wrapper on
the add, lowercase methods and statuses, the POS- number and the
revision the complete and the void carry as If-Match, the day's sales
through the list envelope, the register's open session read directly
(a 404 means none is open), and the error envelope parsed into the
page's message. The tender view takes a split: each entered amount
joins a pending tender list with the still owed or change running
under the total, and the completion sends the whole list. The completed
sale offers a void (with its reason, while the drawer is open) and a
return: the return modal lists the sale's product lines with
quantities, the refund method and the reason, and posts the credit
memo through the returns route.

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

The Playwright spec walks the desk's terminal through the real stack:
the drawer opens, a two line sale pays with a split tender (a dollar in
cash, the rest by check, the still owed running under the total), the
return takes one line back as a cash refunded credit memo, and a second
sale paid in cash with change is voided with its reason; the day's list
read back shows the completed and the voided sale. Screenshots land in
shots/c2-5.

Three bugs the walk found, fixed. The return's parse demanded a
product_id on every line even when line_id named the sale line whose
product, price and cost follow (the ADR's rule; the e2e return was a
400). A line_id naming no line of the original sale fell through to a
misleading price complaint and an unknown standalone product reached
the insert as a foreign key 500; both are 400s naming their field now,
with the product lookup running after the sale lines resolve so the
resolved products are covered too. And the void racing a return could
BOTH win: the return neither locked the sale row nor moved it, so a
trailing void restocked the returned goods again (the race test's
winner check also missed the return's 201). The return now locks the
sale row first (section 11, step 1), refuses anything but a completed
sale, and moves the sale's revision in process, so the void built on
the earlier revision is refused stale; the race test treats any 2xx as
a win and passed eight times in a row under the detector.

Signed-off-by: colton <colton@futurebuild.ai>
The suite's earlier specs consume the seeded stock, and the counter
issues from the branch its register belongs to: the picker now counts
the available rows of the Kelowna branch only, so the split tender run
never asks a branch short of stock to serve the till (the whole suite
passes with the counter in it).

Signed-off-by: colton <colton@futurebuild.ai>
The pending proof the ADR asks for (a sale synced while the tax
provider fails stays pending and completes on retry, never rejected)
found the sync broken twice over, and the golden had silently pinned
the breakage: the completion carried no revision, so every synced sale
died at the precondition (the golden's pos.sync recorded error_count 1,
'this write needs If-Match or a body revision'), and the sale a pending
attempt had built stayed OPEN behind it, so the retried batch counted
it a duplicate and never completed it.

The sync is an in process caller: it re-reads the sale and carries its
current revision, the recipe's in process rule. A batch item that
exists as an OPEN sale is a prior pending attempt: the retry completes
it (adding its lines first if the earlier attempt died before them)
through the same path a live sale takes, and one that fails on the
provider again simply stays pending; a completed or voided one remains
a plain duplicate. The new wire test walks the whole rule with a flaky
provider stub: pending with nothing persisted, the retry syncing,
the replay a duplicate, the books whole. The pos_sync golden now
records a synced sale, and the reporting and media goldens move with
the money and rows the synced sale honestly adds.

Signed-off-by: colton <colton@futurebuild.ai>
The opened and reopened pull_request events created no check suite for
this head (other branches ran in the same window); a synchronize event
asks for the run again. No code rides along.

Signed-off-by: colton <colton@futurebuild.ai>
…pos-till

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

# Conflicts:
#	docs/refactor/CONTRACT-CHANGES.md
…pos-till

Signed-off-by: colton <colton@futurebuild.ai>
Second review P1-1, first review P2-1. A return line that names a sale
line refunds at that line's own extension per unit (discount included)
with its share of the tax the sale actually collected, never the list
unit_price and never a client price: a client unit_price on a linked
line is a 400 naming the field. The quantity is capped at what the line
sold less every earlier return of it (409 exceeds_sold), checked before
the transaction and again under the sale row lock. Migration 105 gains
pos_return_lines.sale_line_id (dropped in the down) so the cap and the
restock cost can follow the line. The memo line mirrors the sale line's
unit and conversion pair, and a free line (no sale named) keeps its own
price and today's branch rate. Red first: TestReturnIsBoundedByTheSale
and TestADiscountedLineReturnsAtItsDiscountedAmount failed on the prior
head (a 10 unit return of a 4 unit line answered 201; a half price line
refunded 1198 for 599 taken).

Signed-off-by: colton <colton@futurebuild.ai>
Second review P1-2. A return bumps the sale's revision but leaves it
COMPLETED, so a void sent on the fresh revision paid the whole sale out a
second time and restocked the returned units again (the probe: 2395 in,
2994 out). The void now refuses 409 has_returns before its gateway calls
and again under the sale row lock inside the transaction (a return that
races the void and wins leaves the sale with returns and the void
refuses): after a return the way back is another return. Red first:
TestVoidAfterAReturnIsRefused answered 200 on the prior head.

Signed-off-by: colton <colton@futurebuild.ai>
…y is put back or recorded

First review P1-2, second review P1-5. Every guard that needs no lock now
runs before any gateway call: the void checks the revision, the status,
the returns and the session before it refunds a card (a stale revision
void makes zero gateway refunds); the return checks the sale's status
and its quantity caps before its card refund; the completion checks the
tender plan and reads the credit check before it charges a card. The
residual race (the state moves between the pre-read and the lock) is
closed: a charge the transaction refuses is reversed the C2-4 card
route's way (the same day void first, the refund when the void is
refused, 502 charge_not_reversed when neither works, pos.card_charge_reversal
audited in its own committed write), and a refund the transaction
refuses is recorded in a committed audit row of its own carrying the
gateway transaction and refund ids (pos.gateway_refund_orphaned) beside
a CRITICAL log. Red first: TestStaleRevisionVoidMakesNoGatewayRefunds,
TestAVoidTheTransactionRefusesRecordsItsGatewayRefund and
TestAChargeTheTransactionRefusesIsReversed failed on the prior head.

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

First review P1-1, second review P1-4. The pre-transaction check priced
with the branch rate alone, so an exempt customer paying exactly was
refused; and the in-transaction check compared the tenders with
recomputedChange, which reduces to min(tendered, total) and never
refused anything: a provider tax above the estimate completed the sale
underpaid with a negative change_due and an open balance on the walk-in
customer. The pre-check now prices with the same resolver the
transaction uses (exemption first, then the provider or the branch
rate), and inside the transaction, once the tax is known on the locked
lines, the change is the tenders less the total: a negative change is
refused insufficient_tender, a change above the cash taken
change_from_cash, and recomputedChange is gone. Red first:
TestProviderTaxAboveTheEstimateRefusesTheUnderTender (was 200 with
change -205), TestAnExemptCustomerPaysExactly (was 409), and the green
companion TestProviderTaxBelowTheEstimateGivesChangeFromTheLiveTotal.

Signed-off-by: colton <colton@futurebuild.ai>
…n invoice line

Second review P1-3. The completion copied each sale line's ParentLineID,
a pos sale line id, straight onto the invoice line, whose foreign key is
invoice_lines: completing any sale with a kit failed the insert with a
23503 and answered 500. The completion now builds a sale line id to
invoice line id map as it builds the lines (the parent is built before
its components, as position orders them) and maps the component's parent
through it. Red first: TestAKitSaleCompletesAndExplodes answered 500 on
the prior head; it now proves the invoice lines (kit whole, component at
zero under it), the component only stock out and COGS, and the AR
invariants after the act.

Signed-off-by: colton <colton@futurebuild.ai>
Second review P1-6. The return's restocking lines priced the restock at
the product's today average cost, so after the average moved a returned
unit relieved 5010 and overstated 1030 by the difference (the probe: a
unit sold at 3.25 relieved at 9.00). A linked return line now finds its
source invoice line by position through the sale's invoice and restocks
at that line's unit_cost, and the credit memo line records the link
(credit_memo_lines.invoice_line_id, the column the invoice module's own
credit memos already write); a free line, which names no sale, keeps
today's average, having no source to read. Red first:
TestAReturnReversesCOGSAtTheSalesCost relieved at 900 on the prior
head.

Signed-off-by: colton <colton@futurebuild.ai>
Second review P2-1, both reviews' P3 note that the check had no test.
The completion read the open receivable with no customer serialization,
so two concurrent ACCOUNT sales for one customer each read an exposure
that did not yet count the other and both passed over the limit (the
probe: balance 9580 against a 9000 limit). The completion now takes the
section 11 step 1a transaction scoped advisory lock
(order-credit:<customer id>, the order module's own key, so a counter
sale and an order confirm serialize on the same exposure) right after
the sale row and before the read. Red first:
TestTwoAccountSalesCannotBothPassTheCreditCheck (pool 4) answered
[200 200] with the balance over the limit on the prior head; green at
count 5 with -race.

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

Second review P2-2, first review P2-3. A sale whose session had closed
completed into the counted drawer (missing from its expected cash and Z
report, and unvoidable for ever after), and a void racing the close left
the closed session expecting cash for a voided sale: the session was
checked outside the transaction only, and the close aggregated with no
lock. The completion and the void now lock the sale's session row FOR
SHARE inside their transactions and refuse session_closed when it is not
open; the close takes the same row FOR UPDATE before it aggregates, so
the acts serialize. Red first:
TestASaleCannotCompleteIntoAClosedSession completed 200 on the prior
head, and TestAVoidRacingTheCloseEndsCoherent (pool 4) left a closed
session expecting the voided sale's cash.

Signed-off-by: colton <colton@futurebuild.ai>
…ll routes

Second review P2-3. The free return took its branch from the request's
register with no check against the caller's branch, and the till report
and close read their session with no branch filter: a cashier granted
only branch B took a cash return out of branch A's drawer, read A's
report and closed A's drawer. GetRegisterBranch, GetTillSession,
GetOpenTillSession and ListZReports now resolve through the branch wall
(branchctx.IDForQuery, 404 outside it), as the sale, void and linked
return reads already did. Red first:
TestTheBranchWallCoversTheFreeReturnAndTheTill answered 201 and 200 on
the prior head.

Signed-off-by: colton <colton@futurebuild.ai>
Second review P2-4. The PR deleted handler_machinekey_test.go (R1-13)
and quietly widened the cashier refusal to POST /pos/transactions/{id}/items
and POST /pos/till/{id}/close, against ADR 0002 section 4, which keeps
item changes, completion, void, the sync and the reports reachable with
pos scopes: only the routes that resolve their cashier from the request
identity (start, till open, return) are user-only. The two routes are
reachable for a key again (the acting subject, when one is on the
request, is the audit attribution), and the restored test proves the
whole matrix: the three user-only routes refuse a key 403 with the
envelope and 'a cashier must be a user', and add item, complete, void,
sync and close serve a keyed caller. Red first: the restored test failed
on the two routes on the prior head.

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

First review P2-2, second review P3-6. A walk-in sale returned with
refund_method account created an open credit memo on the walk-in
customer, which cannot hold an ACCOUNT tender on the sale side: the
credit was stranded where nobody could apply it. The return refuses
409 walk_in_account when its resolved customer is the walk-in and the
method is account, before anything is priced or paid. Red first:
TestAWalkInReturnRefusesAccount answered 201 on the prior head.

Signed-off-by: colton <colton@futurebuild.ai>
The full suite under load found the interleaving the standalone contender
runs never hit: a refund resolution that priced its replay while a contender
booked the winner was answered by the caps the winner's own return tripped
(exceeds_sold) instead of the race. The money transaction now reads the
claim's own state under the sale row lock before the caps recheck, and the
replay that fails while pricing rechecks the claim and answers
intent_not_pending when a resolution settled it meanwhile, so every loser of
a resolution race is told the same thing.

Signed-off-by: colton <colton@futurebuild.ai>
Signed-off-by: colton <colton@futurebuild.ai>
…pos-till

Signed-off-by: colton <colton@futurebuild.ai>
…ed in

r12 P1-1: a void whose last pending claim was resolved as a no refund never
completed, because the completion ran only inside the refund leg. A two card
void whose terminal refunded one card and timed out on the other ended
COMPLETED for good when the refund was booked first (the refunded card's
share reopened as a receivable, the stock stayed out, every return and fresh
void refused), and VOIDED in the other order.

The completion (cash and check payments voided, every live card application
reversed to unapplied credit, invoice voided, stock back, sale VOIDED, event
and audit) is factored into one function both legs call, under the same sale
and claim locks: the refund leg after its own tender's booking, the no refund
leg when its release empties the sale's standing claims beside a void claim
that already booked a refund. A void whose every claim settles without a
booked refund keeps the sale completable by a fresh void, as a terminal that
declined every card does. The drawer's session is held in the no refund leg
the same order the refund leg holds it (the sale, the session, the claim).

TestAGoodCannotBeReturnedAgainAfterAPartlyResolvedVoid now expects VOIDED
and the no refund tender's unapplied credit, and a new table test resolves
r12's two and three card shapes (M3a and M3b, M5a and M5b, M6a to M6c, M10
with cash) in EVERY order, asserting one identical end state per shape: sale
status, 1010, 1020, 2200, the walk-in's balance, stock, the drawer's expected
cash and card figures, and the stub gateway's call count.

Signed-off-by: colton <colton@futurebuild.ai>
r12 P3-1: the void leg of the resolution checked the sale's status before it
locked the claim row, so the loser of a same outcome race (two refund
resolutions of one stranded void claim) saw the winner's VOIDED sale first
and answered invalid_state_transition, a refusal its own request never
caused; the return leg already answered the race.

The claim row is locked and read right after the sale lock, before every
guard of the booking, so the loser answers 409 intent_not_pending: the claim
was settled by the winning resolution. Money was already unaffected (exactly
one 200); the answer now names what happened. A two contender same id race
test asserts one 200, one intent_not_pending, the sale VOIDED, one refund
row and no gateway call.

Signed-off-by: colton <colton@futurebuild.ai>
…r or not

r12 P1-2: with multi_branch_enabled on and default_branch_required off (a
shipped setting), a finance user held to branch B sending no X-Branch-Id
header got a context with no branch, so the sale read's repository filter
never fired and the user settled branch A's claim: a no refund released a
tender whose money may already have moved at the terminal, and the sale's
next card return or void reached the terminal again (a second refund of the
same tender).

After reading the claim's sale, the route holds the sale's own branch to the
caller's grants through the middleware.BranchGuard serve already passes the
document and purchase order handlers (WithBranchGuard on the pos handler,
wired in serve and in the test fixture), whenever the context carries no
branch and the caller is not admin or owner. A refusal is the same missing
row the branch scoped read answers (404, nothing moved), on both legs.

The probe as a test: a bound finance user, no header, the setting off, both
legs, both acts (the return claim and the void claim), a granted finance
user resolving with no header (the hold passes for a caller it covers), and
a later card return and fresh void at the sale's branch proving exactly one
terminal call per sale in the end. The wall test's in branch control now
resolves through a user the sale's branch is granted.

Signed-off-by: colton <colton@futurebuild.ai>
r12 P3-2: expected_by_method's CARD figure was the pass-through of what the
session's sales collected, so after a resolution booked one card tender's
void refund of a partly resolved sale the figure still named the full tender
(400 against 200 collected and 200 refunded), disagreeing with the books.

The CARD figure subtracts the card refunds booked against the session's
still completed sales' card tenders, the same net the CASH figure carries
for the session's returns' cash refunds: a void refund is written straight
against the tender's payment (a return's refund goes through its credit
memo and never joins the sum), and the figure drops with the tender when
the void completes, the same completed sale filter the tendered sum
carries. The M10 shape as a test: cash with two cards, one refunded, one
timed out; 400 before, 200 with the refund booked, and both figures naming
the voided sale's absence once the last claim resolves and the void
completes.

Signed-off-by: colton <colton@futurebuild.ai>
…und 6 rules

The resolve sentences now carry the round's three rules: the completion
runs from either leg of the resolution (whichever empties the claims, under
the same sale and claim locks), so the order the claims are resolved in
never decides the end state, while a void whose every claim settles without
a booked refund keeps the sale completable by a fresh void; the branch hold
for a context with no branch (the default branch setting off, no header)
holds the sale's own branch to the caller's grants, the same missing row
outside them; and the claim row is read right after the sale row, so a
same outcome loser is told the race. The intent table's row adds the till
report's CARD figure subtracting the void refunds resolutions book.

Signed-off-by: colton <colton@futurebuild.ai>
The CARD subtraction r12 P3-2 added wrote its entry unconditionally, so a
session that never collected on cards answered a CARD figure of 0 and the
close stored it: a shape no round asked for, read by consumers as a
collection that never happened. The figure is written only where the
session collected on cards (a booked card refund always implies the tender
that was collected, so nothing booked escapes the net).

A cash only session as a test: the report carries CASH alone, and the
close stores the same map.

Signed-off-by: colton <colton@futurebuild.ai>
…pos-till

Signed-off-by: colton <colton@futurebuild.ai>
…ts window

Eleventh review P2-1: five rechecks the brief names survived their mutants
because no test reached them; the routes the counter offers refuse the
writes that would. Each is now reached through the real tables it reads, in
the window between an unlocked read and the lock the act takes: the pricing
against the claim (a sale row held FOR NO KEY UPDATE, the remainder moved
inside), the claim against the money transaction (the refund parked at the
terminal, the intent table or a booked card refund written inside), the two
reads of the intent facts against each other (the transaction parked on the
till session row it takes between them), and the replay's read against its
own transaction (a winning resolution's whole footprint written inside).
Every mutant of every recheck now reddens its named test, and no recheck is
unreachable through a write, so none is deleted.

Signed-off-by: colton <colton@futurebuild.ai>
Eleventh review P3-1: the contention proof covered a return claim only, so
stacked mutants of the void legs' state guards survived. A stranded two
tender void (one claim carrying the terminal's refund id, one timed out) is
now fought over by three concurrent resolutions: the refunded claim recorded
once, the timed out claim by a refund and a no refund. Exactly one contender
answers 200 per claim, the loser names the race, the books move once per
settled refund, the void completes exactly once whichever leg empties the
claims, and no resolution reaches the terminal. The booked total and the
card balance read from the claims themselves, because which tender carries
the terminal's refund follows the tenders' own order. The stacked mutant
(the route's read, both legs' in-transaction state checks and both
row-count refusals off) reddens it: the fought claim settles twice.

Signed-off-by: colton <colton@futurebuild.ai>
…m state

Eleventh review P3-2 and P3-4: the resolve paragraph and the C2-5r4 resolve
row said the void completes from whichever leg empties the claims but never
said what stands between one tender's booked refund and the last claim's
resolution. Both now state the interim state (the booked refund reversed
its application, so the invoice is open again for the booked cents and the
customer's balance carries them as owed until the last claim resolves and
the void completes, with the close refused while a claim of the session is
pending, so the interim never slips into a counted drawer) and name the no
refund of the last claim as a completing leg explicitly.

Signed-off-by: colton <colton@futurebuild.ai>
… its card, the apply route stays open

Eleventh review P3-3: a declined void tender's credit lands as unapplied
cash on the sale's customer, and on a walk-in sale that is the shared
walk-in customer, whose later invoices the payment apply route could absorb
it into. Refusing that at the apply route is not sound, so the limit is
stated in the ADR instead: unapplied cash on one payment is fungible (a
genuine overpayment and a void's reversal mix on the same row, so no
refusal can be drawn at the cent level, and refusing the row would refuse
the overpayment too), and the apply rule is the account core's own, per
customer, with no origin to read. The way back is the payment's own refund
route to the card, and the proof runs it end to end on a walk-in sale: the
declined card's cents are paid back through the terminal, the unapplied
falls to zero, the customer owes nothing and the books tie. The fixture
wires the payment route's gateway the way serve wires Run Payments when a
test asks for it.

Signed-off-by: colton <colton@futurebuild.ai>
… branch's largest

The carried branch fix read the branch's row with the most quantity whenever
the context carried a branch, ignoring the location the request named: a
cycle count of one yard overwrote another yard's units, and a receipt landed
in the largest yard. A named location now reads exactly that row, refused
with ErrLocationOutsideBranch (403 on the stock routes) when the row sits in
a branch the caller does not carry; only a request naming no location lets
the branch rule pick a row, and it takes the branch's first row by creation,
a stated rule, never the largest quantity silently; an unbound request keeps
the legacy unassigned row. The purchase order receive goes through the same
read and is pinned with the stock routes (r14 P1-2).

Signed-off-by: colton <colton@futurebuild.ai>
…nd the close

A no refund resolution could settle a claim while its own act's refund call
was still on the wire, freeing the tender; the terminal then refunded it and
a later act refunded the same tender again, and the drawer could close over
the open call. Every claim now carries an in flight lease from the moment it
opens (answer_due_at, 90 seconds) and an answer record (answered_at,
answer_state): the terminal call is bounded, every path it can take stamps
the answer (the refund id's committed write, the decline release, the no
verdict record, and an abandon stamp for tenders whose call was never
reached), the resolve refuses 409 refund_in_flight on an unanswered claim
inside its lease under the claim's row lock, and once the lease expires with
no answer at all the claim is marked UNKNOWN and settles as recorded. The
gateway's refund id is now written to the claim whatever state it is in, and
a recorded refund that never booked holds the tender against every later
act, so a late answer behind an early release can never be paid twice
(r14 P1-1).

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

In the interim of a split void (one tender's refund booked, another claim
standing) the sale's invoice shows the booked cents as owed, and anything
applied to them left a live application the void's completion could not
reverse: the last claim then answered 409 has_applications on every attempt
and the drawer could not close, and an absorbed walk-in credit could no
longer be paid back to its card. The AR core's application choke point (the
receipt's applications, the apply route, a credit memo, a write off) now
asks the counter through a guard: while the invoice's sale has a standing
void claim the application is refused with 409 void_claim_pending naming the
claims, the credit stays on its own payment, and the void always completes
(r14 P2-1).

Signed-off-by: colton <colton@futurebuild.ai>
A card return's refund rides its credit memo, never the tender's payment,
so the CARD expectation never saw it and disagreed with the books by every
card return of the session. The aggregate now counts the session's card
return refunds through their memos, the same join the CASH figure always
used for its cash returns (r14 P3-2).

Signed-off-by: colton <colton@futurebuild.ai>
The first cut of the counter onto the wire contract changed the Z report's
payload from a JSON object to a base64 encoded string without a contract
row: any reader of the frozen report had to decode it before parsing it.
The payload is json.RawMessage again (an object on the wire, the fragment
declaring PosTillReport as before), and the characterization golden stops
masking the field, so the frozen report's figures are pinned (r14 P3-3).

Signed-off-by: colton <colton@futurebuild.ai>
… no refund leg's session hold

Three proofs the round 13 review found missing: a void whose every claim
settles without a booked refund keeps the sale completable by a fresh void
(the gate's mutant now dies), one sale's booked card refund never nets
another sale's CARD figure (the SQL's completed sale filter's mutant now
dies), and the no refund leg holds the drawer's session row against the
close (the race settles as the pair of a completed void and a counted or
refused then retried close, never a deadlock). The leg's session lock
comment now states the two legs' actual orders and why they cannot
deadlock: both hold the sale row first (r13 P3-1, P3-2, P3-3).

Signed-off-by: colton <colton@futurebuild.ai>
ADR 0005's resolve paragraph states the in flight lease (the bounded call,
the answer record, the refuse while live, the UNKNOWN mark on an expired
lease, the id whatever the claim's state, the never paid twice hold) and
that nothing applies to an interim void invoice; the C2-5 till sentence
carries the CARD figure's full net. CONTRACT-CHANGES gains the C2-5r8 rows:
the write path's named location row, the in flight lease, the interim
invoice's application refusal, the CARD figure's card returns, the Z
report's payload restored to an object, and migration 107. The round's code
comments spell the review ordinals out and carry no durations.

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

Signed-off-by: colton <colton@futurebuild.ai>
…unts are read

The guard sat at the application insert, so an amount over the open could
answer exceeds_open_amount for an invoice the counter holds: the operator
is told the invoice is held whatever they asked for, and the refusal no
longer depends on the amounts. The payment, credit memo and write off paths
check the hold at their invoice checks.

Signed-off-by: colton <colton@futurebuild.ai>
…pos-till

Signed-off-by: colton <colton@futurebuild.ai>
PR 83 holds 107 (category rules single target) and 108, so the two
branches merge in either order only if this branch's lease migration
takes 109. git mv of the up and down files, the CONTRACT-CHANGES C2-5r8
rows renamed with it. No content change: the fresh apply through 109 and
the 109 down then up cycle were run on a scratch database.

Signed-off-by: colton <colton@futurebuild.ai>
…s tender

FROM-PRINCIPAL 60a, red first (TestADeclinedVoidTendersCreditAppliesToNoInvoice:
the payment apply route answered 200 for a declined void tender's credit on
the shared walk-in customer, absorbing it into another walk-in invoice; the
fourteenth review's Area 5 shape). The AR core gains a held credit guard the
counter wires (account.TenderCreditGuard, serve and the fixture), answered
from pos_tenders joined to the voided sale and the claim the void released:
while the tender's payment still carries the credit unapplied, every path
that applies a payment's unapplied cash (the apply route, a receipt's own
applications, the deposit path, all through applyLocked) refuses 409
tender_credit_held naming the origin, the void and its claim, and nothing is
written. The credit's one way back is its card; the payback route is the
next commit.

Signed-off-by: colton <colton@futurebuild.ai>
…er terminal rail

FROM-PRINCIPAL 60b, red first (the payback route was no route, 404, and the
payment refund route answered 201 for the credit through the online
gateway, the wrong rail). The payback route POST
/api/v1/pos/refund-intents/{id}/payback, a small sibling of the resolve
route on its own mount (finance and owner only, a machine key refused in
the route, the branch wall and the no context hold through the same
saleOfIntentHeld), keyed on the void's own released claim: only a FAILED
void claim of a voided sale whose tender's payment still carries the
credit unapplied is paid back, once. Its terminal call runs under the
claim discipline every terminal refund runs under: migration 110 widens
pos_refund_intents' act to 'payback', the payback opens its own live claim
before the call (a concurrent payback 409 refund_in_flight, the lease
holding the resolve route and the close), the terminal's refund id lands
in its own committed write, and the money transaction books the credit out
(RefundPayment: the unapplied cash falls to zero, the cash account moves)
completing the claim DONE with one pos.refund_intent.paid_back audit row
in the resolve route's shape. A declined payback releases its claim and
moves nothing; a no verdict payback settles through the resolve route,
whose refund leg learns the payback act. The payment refund route refuses
a held credit 409 tender_credit_held before its online gateway is ever
asked. Fragment, census row and schema.d.ts regenerated; no golden (the
route is new, no seeded characterization data names it).

Signed-off-by: colton <colton@futurebuild.ai>
…ract rows

ADR 0005's walk-in paragraph states FROM-PRINCIPAL 60's rule and drops
round 7's fungibility argument (the thirteenth review's P3-6): the credit
is held to its own tender (it applies to no invoice, whatever the
customer, the refusal naming the origin), it is paid back only to that
card through the counter terminal rail by the payback route beside the
resolve route, the online gateway's payment refund route is never the way
back (the thirteenth review's P3-4), and until the card is paid back the
credit stays held. CONTRACT-CHANGES gains the C2-5r9 rows (the held credit
apply refusal, the payback route, the payment refund route refusal,
migration 110) and the r8 fungibility note now points at the r9 rows.

Signed-off-by: colton <colton@futurebuild.ai>
Signed-off-by: colton <colton@futurebuild.ai>
Signed-off-by: colton <colton@futurebuild.ai>
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