Repository navigation
C2-5: POS and till on the contract (ADR 0005) - #72
Open
futurebuildai wants to merge 108 commits into
Open
futurebuildai wants to merge 108 commits into
futurebuildai wants to merge 108 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
TestSaleWireShape(the audit rows each adjustment owes asserted beside the shapes)TestSaleWireShape,TestSplitTenderSalePostsEveryEntryInTheSaleTransaction(a decimal tender is a decode error, never a rounding)TestWalkInSaleRefusesAccountTender, and migration 105's walk-in inTestMigration105_POSWireContractTestSplitTenderSalePostsEveryEntryInTheSaleTransaction,TestChangeIsNeverSubtractedTwiceTestFailingEventWriteRollsTheSaleBack(both the completion and the void; the gapless series spends nothing)TestVoidOfACardSaleLeavesTheLedgerAtZero,TestVoidAfterSessionCloseIsRefusedTestVoidAfterSessionCloseIsRefusedTestReturnRestocksAndReversesCOGS,TestAccountReturnLeavesOpenCreditTestSplitTenderSalePostsEveryEntryInTheSaleTransactionTestTillExpectedCashIncludesFloatAndRefunds,TestChangeIsNeverSubtractedTwiceTestVoidOfACardSaleLeavesTheLedgerAtZeroTestReturnRestocksAndReversesCOGSTestOfflineSyncPendingOnTaxFailureCompletesOnRetry(found the sync broken: the completion carried no revision, so no sale ever synced; the golden had pinned the breakage) and thepos_syncgolden (synced, duplicate, underpaid)TestOfflineSyncPendingOnTaxFailureCompletesOnRetryTestThreeRegistersConsecutiveNumbersAgainstAWalkInPaymentinsufficient_stockTestTwoSalesOfTheLastUnitTestVoidRacingAReturn(found both could win: the return now locks the sale row first and moves its revision)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)fixture.assertARInvariantsruns 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
status:TestSaleWireShape(IN-1.1 tag in the source)TestSaleWireShape,TestReturnRestocksAndReversesCOGS[]in the bytes:TestSaleListEnvelopeAndFiltersTestErrorEnvelopeAndRevision, 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 goldensBeside the tests
pos_wire_contract+ down (the ADR 13 C2-5 steps), CONTRACT-CHANGES rows for every route and behaviour, the pos contract fragment rewritten,make contractgreen, the api-client regenerated.itemsas it sorted the legacydata. Stability proven by consecutive runs.shots/c2-5/.products.is_active(500); a walk-in sale priced through the engine with an empty customer id (500);till/currentanswered 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.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.
TestACardReturnAfterACommittedVoidNeverReachesTheTerminal,TestAVoidAfterACommittedCardReturnNeverReachesTheTerminal.refund_in_flightwhile 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.refund_in_flightwhile any intent of the session's sales is PENDING:TestTheCloseRefusesWhileAVoidIsParkedAtTheGateway.pos.refund_outcome_unknownaudit row (the gateway interface has no idempotency key parameter):TestATimedOutCardRefundLeavesItsClaimStanding.(state, created_at), and a guarded admin routePOST /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_refundwith 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.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.
refund_in_flightwhile 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).TestAVoidResolutionOfOneTenderOfTwoBooksOnlyThatTender,TestTheStrandedVoidOfTwoTendersResolvesOneTenderAtATime. A partly resolved void refuses every later return of the goods (409void_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.TestTheResolutionStaysInsideTheCallersBranch,TestTheResolutionRouteRunsTheBranchGuardItIsGiven, and the serve wiring legs inTestBranchWall_ServeWiring.intent_not_pendingwhen 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.refund_recorded, a differing id is 409refund_tx_id_conflict:TestANoRefundOverARecordedRefundIdIsRefused,TestARefundIdThatDiffersFromTheRecordedOneIsRefused.TestANonVerdictStatusLeavesTheCardReturnClaimStanding,TestANonVerdictStatusLeavesTheVoidClaimStanding, with the void's timeout and decline paths proved the same way.refund_amount_changedwith 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.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.
TestASplitVoidEndsTheSameStateWhateverOrderItsClaimsAreResolvedresolves 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);TestAGoodCannotBeReturnedAgainAfterAPartlyResolvedVoidnow expects VOIDED and the no refund tender's unapplied credit.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.TestTwoConcurrentRefundResolutionsOfAVoidClaimNameTheRace: one 200, one intent_not_pending, the sale VOIDED, one refund row, no gateway call.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.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.
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 refusesrefund_amount_changedbeside the money that moved.TestACardReturnPastTheBookedRefundsRefusesUnderTheLock(a booked CARD return row in the parked window; 409exceeds_card_tender, the claim stands PENDING beside its orphan row, nothing booked) andTestAClaimWhoseTableAmountMovedRefusesTheBooking(the claim's own row amount moved in the parked window; 409refund_in_flight). Each mutant books 201 instead.TestATenderTheVoidRefundedBetweenTheFactsReadsRefusesTheBooking(a void claim of the tender stands DONE between the transaction's two facts reads: 409tender_refunded) andTestAClaimSettledBetweenTheFactsReadsRefusesWithTheRaceNamed(the claim settles between the reads: 409intent_not_pending, never the misleadingrefund_in_flightthe later check would answer). The mutants book 201 and answer the wrong code.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: 409intent_not_pendingbefore the caps, never the winner's ownexceeds_sold) andTestAReplayUnderAVoidRefundedTenderAnswersTheTenderNeverTheCaps(a void DONE claim beside that footprint: 409tender_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.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 409intent_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.TestADeclinedVoidTendersCreditIsPaidBackToItsCardpays 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.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).
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).refund_in_flighton 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.POST /api/v1/payments, another walk-in's declined card credit throughPOST /api/v1/payments/{id}/applications) left a live row the void's completion could not reverse, stranding the last claim (409has_applicationsforever) and the drawer. Every application path now refuses 409void_claim_pendingnaming 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).TestTheTillReportCardFigureSubtractsCardReturns(red: 1796 stayed after 1198 of card returns; now 598).payloadis 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 declaresPosTillReportas 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).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.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).
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'sapplyLocked) refuses 409tender_credit_heldnaming 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).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 onepos.refund_intent.paid_backrow 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 409no_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 widenspos_refund_intents.acttopayback, the payback opens its own live claim before the call (a concurrent payback 409refund_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 (409card_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'sactenum gainspayback). The payment refund route refuses a held credit 409tender_credit_heldbefore its online gateway is ever asked. Red proofs:TestADeclinedVoidTendersCreditIsPaidBackThroughTheCounterTerminal(the online refund was 201 at the previous head),TestAPaybackWhoseCallAnswersNoVerdictSettlesThroughTheResolveRouteandTestAPaybackTheTerminalDeclinedReleasesItsClaim(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.git mvboth 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.openapi.yamlandschema.d.tswere regenerated from the fragments and the pinned openapi-typescript and are byte identical to the merge's, and CONTRACT-CHANGES keeps both sides' rows.