Repository navigation
Store credit is a payment, not a discount: import, push and invoice refusal (o3d-zvec.28) - #760
Conversation
…efusal (o3d-zvec.28) Supersedes #758 (same content; the migration directory carries its single later name 20261009210000_sales_order_store_credit, which the old branch's history could not do). - Smart Coupons store credit is classified from the order item's coupon_info / coupon_data and the order's smart_coupons_contribution record, held in SalesOrder.storeCreditForeign (gross), and kept out of discountAmount, so the warehouse push carries the full goods value. - An unknown coupon type with money off the lines, an unreadable credit, or a credit that does not reconcile (tolerance capped at two minor units) refuses the import. - Sales invoice (create/update), credit note, payment registration, invoice email and WooCommerce invoice note are refused at the Xero posting boundary for a store-credit order until the 813/816 payment posting exists; a census test classifies every processor case. - A re-read order held before credit was recorded gets the credit recorded from the payload (no restatement). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9be9fe6cc1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| // The residual that is not in the lines, with an UNKNOWN coupon in play, is the ambiguous case. | ||
| if (classified.unknown.length > 0 && unallocated > 0) { |
There was a problem hiding this comment.
Refuse unknown coupons even when line markdowns mask them
When an untyped £10 coupon is store credit that was not applied to any line, but the order also has at least £10 of unrelated per-line markdowns, resolveResidual clamps the aggregate residual to zero and this condition accepts the order. The credit is then neither recorded in storeCreditForeign nor represented as a discount, so the imported components exceed the Woo total; the WMS check is advisory and accounting can post the larger invoice with only the lower Woo payment. Allocation of an unknown coupon cannot safely be inferred from the aggregate line-discount total; refuse it or reconcile the resulting order total before import.
Useful? React with 👍 / 👎.
| // the document rather than post one that exceeds what the customer was charged. Read from the | ||
| // order itself, under the same lock, so it covers every producer (import, hold release, manual | ||
| // re-queue, update) and not just payloads that happen to carry a marker. | ||
| if (orderCarriesStoreCredit(so.storeCreditForeign)) return { kind: 'storeCredit' as const } |
There was a problem hiding this comment.
Recheck store credit at the remote-write boundary
When a queued invoice is processed concurrently with updateExistingWcOrderFromPayload discovering credit on an existing order, this transaction can read zero and release its row lock before the updater commits a positive storeCreditForeign; the worker then continues through the number/lease fences and posts the invoice. The updater neither checks the processing claim nor shares a lock through the remote call, so the lock here does not provide the claimed coverage. Recheck credit in the immediate beforePost fence or otherwise serialize the updater with the posting attempt.
Useful? React with 👍 / 👎.
…push withhold, locked conditional update) - reconciliation of a credit order is exact (quarter minor unit), not an allowance - every disagreement involving a credit signal (coupon_info / coupon_data / smart_coupons_contribution) is refused whatever the allocation residual - an order with credit whose discount is not proven credit-free (no LINE_ALLOCATED stamp) is withheld from the warehouse push and parked with a reason - update path takes the order row lock and records the credit with a conditional write (WHERE storeCreditForeign = 0) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…; provenance; Woo's own amounts) - storeCreditForeign is set only by the credit-aware import's creating write; the record-on-update path is gone - a later delivery that shows credit (or a credit conflict) the row does not account for sets StoreCreditAssessment.REVIEW_REQUIRED, which every posting boundary and the warehouse push refuse - the push guard needs storeCreditAssessment = ASSESSED (written only by the creating write); NULL fails closed - the credit reconciliation uses WooCommerce's own line / fee / shipping figures, not the 6dp unit-price rebuild Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…re-credit-payment-2
…store credit) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
- the manual invoice email, the accounting invoice email, the outbox preparation of INVOICE / ACCOUNTING_INVOICE rows and the on-the-fly invoice download refuse an order with store credit / held for review, with one single-sourced text - the order confirmation and sales-order PDF show the credit as its own line so their figures add up - a census lists every place that renders or sends a customer document (printed counts, GUARDED / EXEMPT-with-reason) - the pushed-payload total check (push-total-guard) expects the full goods value: expected gross = order total + store credit Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…heir presence Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…ever dropped) - a non-zero or unreadable smart_coupons_contribution entry with no matching coupon line (case / whitespace normalised) refuses the import and, on update, holds the order for review; so does a non-zero wallet / gift-card order meta value - contribution codes are matched ignoring case and surrounding whitespace Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…ed line Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…ve path
Credit evidence that is not a clean, fully parsed, fully reconciled credit is refused on import and holds the
order for review on update. The classifier is restructured around a whitelist of clean shapes (credit line,
contribution record, genuine discount line, wallet meta); every other shape - missing, malformed, unreadable,
conflicting, orphaned, credit-worded, or an absent discount_type - falls to a problem.
- smart_coupons_contribution present in ANY value shape (null, '', 0, array, empty map, malformed JSON, nested) that is not a clean
non-empty {code: amount} map with every code matched by a coupon line is a problem
- a credit line with a missing / unreadable / negative amount is a problem (not an UNKNOWN that sums to 0)
- the update decision is one pure function (wcUpdateNeedsStoreCreditReview)
- generated invariant test over ~195 payload shapes with an explicit oracle
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…eing-type branches; docs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…re-credit-payment-2
…e like a real order A coupon line with no recorded discount_type is now refused (not provably a discount), so the synthetic store's coupon lines carry coupon_info as WooCommerce 8.7+ reports them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…he credit is refused A smart_coupon line with discount 0 beside a positive smart_coupons_contribution was accepted with credit 0 (no reconciliation, order ASSESSED with no recorded credit). The contribution amount is now compared with the line gross (within 0.0025, per code, summed over duplicate lines); any other difference is a signal problem: refused on import, review hold on update. The generated invariant test gains the amount-mismatch dimension (22 shapes). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…al shortfall refused, every wallet key generated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…the amount rule, fee lines and no-credit shortfall Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
Every order-scoped posting guard (sales invoice create/update, credit note, payment and other follow-ups) now reads the store-credit state under the order row lock, and the review transition, after taking the same lock, defers (retryable, whole update rolled back) while a fresh PROCESSING claim of an order-scoped posting for the order exists. Real-Postgres race arms in both directions, isolating arms for stale/settled/other-order. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
…only status distinguishes it Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb
Supersedes #758 (closed; same content, migration directory renamed on a fresh branch, history not rewritten).
What this does
Smart Coupons store credit is a payment, not a discount. WooCommerce takes it off
order.totalwithout reducing any line, so the old importer (which summed everycoupon_lines[]entry) stored the whole credit asSalesOrder.discountAmount; the Mintsoft push then sent it asDiscountTotalExVat(reduced goods value for customs/IOSS) and the accounting document would have been understated in revenue and VAT.Decision D-A1 = option (b): the Xero 813/816 payment posting is not built here. Instead IMS fails closed for an order that carries store credit until o3d-iklv lands.
Design
lib/connectors/woocommerce/sync/coupon-classification.ts, pure)coupon_info([id, code, discount_type, amount], WC 8.7+) or legacycoupon_data.discount_type, and from the order'ssmart_coupons_contributionmap. Read from the code of the production woo-mintsoft sync plugin (read-only, via ssh): it identifies store credit ONLY bysmart_coupons_contribution, never bydiscount_type. Both signals are honoured; a disagreement is a conflict, not a pick.smart_coupon= credit;fixed_cart/percent/fixed_product/percent_product/acfw_bogo= genuine discount; anything else or nothing recorded = UNKNOWN. Credit amount is gross (discount + discount_tax).wc_coupon_import_refused, and the import returns the same text. Also refused: an unreadable credit amount, and a credit order whose lines + tax + shipping - genuine order discount - credit do not equal the Woo total (credit also inside the lines, e.g. apply-before-tax).SalesOrder.storeCreditForeign Decimal(18,4) NOT NULL DEFAULT 0(gross, order currency) + migration20261009090000_sales_order_store_credit(additive, metadata-only default, no backfill). APaymentrow was rejected: every payment reader would read it as cash.discountAmountcarries only the genuine-coupon residual (today's y14 logic, unchanged); credit goes tostoreCreditForeign._registerPayment/_paymentAmountare withheld for a credit order (its document is at full value, not the order total). WARNINGwc_store_credit_invoice_withheldwhen the invoice is queued.guardCancelledSalesOrderInvoice(already runs under the order row lock for SALES_INVOICE and SALES_INVOICE_UPDATE) now also refuses whenstoreCreditForeignis non-zero or unreadable (cancellation still outranks it); newguardStoreCreditCreditNoterefuses the refundCREDIT_NOTEfor such an order (an unreadable order fails closed). Text single-sourced inlib/domain/accounting/store-credit-invoice-refusal.tsand carries the repo's ledger-check preamble. Read from the order, not a payload marker, so it covers import, held release, manual re-queue and update.buildPushInput/buildPushPayloadunchanged: with the credit out ofdiscountAmount,DiscountTotalExVatis genuine discount only. Matches the plugin: it strips the credit coupon out ofDiscountTotalExVatand sends no payment field at all (the credit settles the invoice, Mintsoft is told the full value). The push-time total check (orderTotalDriftPence) now subtracts the credit, so an honest credit order is not flagged as drift.LINE_ALLOCATED-stamped (POST_FIX). A legacy row that stored credit as discount stays as it was (production is unused).help-docs/woocommerce.md(symlinked fromdocs/woocommerce.md),docs/mintsoft.md,docs/todo/woo-mintsoft-plugin-parity-gap.md(its "already handled" claim corrected). No new env vars.Tests (each arm prints its precondition)
tests/connectors/wc-store-credit-payment.test.ts(pure credit, credit + percentage, credit + fixed with remainder, unknown unallocated, no-type unallocated, unknown allocated, zero credit, legacycoupon_data, conflict, contribution-only, unreadable amount, reconciliation ok / double-counted / rounding, refusal text, importer wiring order),tests/accounting/store-credit-invoice-refusal.test.ts(control posts, credit order refused, cancelled outranks, unreadable fails closed, refund of a credit order, create/update/credit-note guard-before-post wiring, single-sourced conditional text),tests/wms-order-push-store-credit.test.ts(push discount, genuine-discount control, drift with/without credit, Mintsoft payload has no credit/payment key),tests/connectors/wc-store-credit-backfill.test.ts(characterisation: unchanged module, cannot be red on trunk).Mutation table (final head; each shown red, restored from a copy, md5 verified equal, match count 1)
smart_couponas a discount (sum-everything)mapWcOrderDiscount(sum every coupon) restoredWhat is NOT done
importWcOrdertest (no harness runs it without a DB): importer wiring is pinned by source order-of-operations scan and the planner is tested purely. Follow-up o3d-3qlyw (DB-tier test).next buildnot run locally.Rebase notes
Based on
development6a2d905. PRs #751/#753 also touchlib/domain/wms/order-push-sweep.ts(exportedbuildPushInput, one word); my edits there are thestoreCreditForeignselect/type/drift lines, so expect a trivial rebase. This PR deliberately does not exportbuildPushInputand tests the push throughrunWmsOrderPushSweepCore. #753's penny guard, if it re-derives the total, must also subtractstoreCreditForeign.Gate (exact head 484bb5f; CI env incl. CRON_SECRET, SETTINGS_ENCRYPTION_KEY, AUTH_*, CI=true; heavy tiers under /var/tmp/ims-gate-heavy.lock)
npm run validatenot ok=# TODO o3d-qf6qand the known local-only o3d-t2lvg pair (CI validate is green); the migration-deletion guard passes heregit status --porcelainemptyCI on 484bb5f: all checks green (validate, build, fresh-db-drift, db-backed-regressions, migration-scope, migration-conventions, producer-census, and the rest).
next buildnot run locally (CIbuildgreen).Codex round 1 (carried over from #758)
tests/accounting/store-credit-posting-boundaries.test.tsclassifies all 32 processor cases (6 GUARDED, 26 EXEMPT with a reason) and fails on an unclassified one; one mutation per boundary.wc_store_credit_recorded_on_update.20261009210000_sales_order_store_crediton this fresh branch (the old branch's history carried two names, which the repo's migration-deletion guard forbids).Codex round 2
CREDIT_SIGNAL_CONFLICT).LINE_ALLOCATEDstamp, e.g. recorded on update):buildPushInputthrows the single-sourced reason, so the create pass parks itVALIDATION_FAILEDbefore anything is claimed or sent. Tested through the push sweep with a printed matrix.UPDATE ... WHERE storeCreditForeign = 0; log only when it wrote. Mocked race (40 rounds) and a real-DB concurrency test (15 rounds, both winners observed).Codex round 3 (head a52111f) - the update path is simplified
storeCreditForeignis written only by the credit-aware import's creating write, so no code path can change it after an invoice guard read it.SalesOrder.storeCreditAssessment(ASSESSED | REVIEW_REQUIRED), in the same single migration. A later delivery is re-classified; credit or a credit CONFLICT the row does not account for (unassessed order, changed credit, conflict) sets REVIEW_REQUIRED (order row lock, conditional write, idempotent, one ERRORwc_store_credit_review_requiredentry). Every posting boundary (invoice create/update, credit note, payment, email, note) and the warehouse push refuse REVIEW_REQUIRED with a review-specific single-sourced text. No existing hold field fit (the only SalesOrder hold is the withdrawal hold, with unrelated semantics), hence the one column.storeCreditAssessment = ASSESSED(written only in the creating write) for any credit order; NULL (any row older than the column, native orders) fails closed whenever credit is present.wcReportedOrderAmounts), not IMS's six-decimal unit-price rebuild; fixture: 3 lines x quantity 997 (rebuild drifts -0.000638, Woo's figures reconcile, accepted). The 1-2 minor unit credit-in-lines refusals stay.Codex round 4 (head 304c56d) - customer-facing invoice documents
NOTHING WAS GENERATED OR SENTtext: the manual invoice email (sendInvoiceEmailviagetInvoiceQueueData), the accounting invoice email (getAccountingInvoiceQueueData), the outbox preparation of INVOICE / ACCOUNTING_INVOICE rows (so queued or retried rows are covered), and the on-the-fly invoice download (409; the stored accounting PDF branch is unchanged).tests/accounting/store-credit-customer-documents.test.ts, printed counts): every file that renders a PDF (8: 3 guarded, 5 exempt with a reason), every place a stored invoice PDF is served (5; all the accounting system's own document, never fetched for a credit order), every queued/prepared email kind (SALES_ORDER_CONFIRMATION, INVOICE, ACCOUNTING_INVOICE, SHIPMENT_DISPATCHED - no totals). A new renderer or kind fails the census.PushTotalsInput.storeCreditGrossis added to the expected gross, tested both ways.orderTotalDriftPencekeeps the credit subtraction in its Decimal form.Codex round 5 (head 484bb5f) - a credit signal with no coupon line is never dropped
smart_coupons_contributionwas used only to classify MATCHING coupon lines; a contribution whose line WooCommerce omitted recorded zero credit and skipped the reconciliation. NowclassifyWcCouponLinesreturnssignalProblems: every non-zero (or unreadable) contribution with no coupon line (codes matched ignoring case and surrounding whitespace; list form has no amounts so is unreadable; multiple codes handled; a zero amount is no credit). Any of them refuses the import (nothing created,CREDIT_SIGNAL_CONFLICT, single-sourced viadescribeWcCouponRefusal) and, on a later delivery, holds the order (REVIEW_REQUIRED)._wc_store_credit_used,_used_wallet_amount,_ywgc_applied_gift_cards_total, ... the legacy sync plugin's default key list) - fixed: a non-zero or unreadable value is refused.Codex round 6 (head 5dbf9b1) - one invariant; session parked, state as of now
wcUpdateNeedsStoreCreditReview, one pure function). Covers a present-but-unparseablesmart_coupons_contributionin any shape, credit lines with missing / unreadable / negative amounts, missing or unreadablediscount_type, disagreeing sources, credit words on a not-cleanly-typed line, wallet / gift-card meta. Generated test (wc-store-credit-evidence-invariant.test.ts, 201 shapes: 49 clean, 152 evidence, explicit oracle, printed table). Mutations M1, M42, M46-M58, M49/M51 (after widening the generator) red, restored by copy.coupon_infolike a real order. A genuine but exotic untyped coupon would be refused for operator review (fail closed).rehearse-woo-import, 18/18).Codex round 7 (head 5dbf9b1) - the contribution AMOUNT is compared with its coupon line
smart_couponline withdiscount0 /discount_tax0 beside a POSITIVEsmart_coupons_contributionfor the same code was accepted with credit 0 (the import skips reconciliation when credit is 0), creating an ASSESSED order with no recorded credit and invoice/warehouse push unblocked.classifyWcCouponLines): per contribution code, sum the gross (discount + discount_tax) of every coupon line with that code and compare with the contribution amount. Equal within 0.0025 (a quarter of a two-decimal minor unit: storage noise only) => the credit is the line gross and the normal exact total reconciliation applies. ANY other difference is asignalProblem=> import REFUSED (CREDIT_SIGNAL_CONFLICT, the single-sourceddescribeWcCouponRefusaltext) and REVIEW_REQUIRED on update: zero line + positive contribution, line greater than contribution, contribution greater than line, positive line + zero contribution, a penny either way, 12.0025 (exactly the bound: refused), duplicate lines for one code (summed), several codes (per code, so a total that matches by accident does not hide a per-code mismatch). A contribution of 0 beside a 0 line is no credit and is clean. A credit line typedsmart_couponthat the contribution does not list is still clean (the contribution is optional evidence).help-docs/woocommerce.mdupdated in the same change.Payload field coverage (every field that can carry credit)
coupon_lines[].discount,.discount_taxwc-store-credit-paymentunreadable-amount armscoupon_lines[].meta_datacoupon_info/coupon_data(type)coupon_lines[].codeand meta (credit words on a not-cleanly-typed line)smart_coupons_contributionamountsmart_coupons_contribution(any shape; orphans; unreadable amounts){code: amount}map, every code has a linefee_lines[]negative / unreadable amount named for creditfee_lines[]ordinary namesdiscount_total,discount_tax(order level)totalversus lines + fees + shipping + tax (credit order)totalversus the same (NO credit)NO-CREDIT ordertable (6 rows) + wiring armtotal_tax,shipping_total,shipping_tax,cart_taxtotal_taxinwcReportedOrderAmountswcUpdateNeedsStoreCreditReview=> REVIEW_REQUIREDwc-store-credit-update-pathRound 7 mutations (committed first; each restored from a copy, md5 verified equal; the generator run is one test so the first failing row is printed)
created credit 0same code on two lines, sum equals contributionrefusedcontribution 12.0025 (exactly the bound)Store credit, -10.00Gift card, tax only negativeWallet, unreadable totala penny short 129.99a penny over 130.01EVIDENCE NOT HELD ON UPDATE_smart_coupon_credit_used = nullCodex round 8 (head cb454cb) - a posting-time order-state fence for the review transition
lockSalesOrder: sales invoice create/update (already did), credit note (guardStoreCreditCreditNote, locks the refund's order), payment / invoice email / WooCommerce note (guardStoreCreditFollowUp).updateExistingWcOrderFromPayload), after taking the same lock and before writing, callsassertNoOrderPostingInFlightForReview(newlib/domain/accounting/store-credit-posting-fence.ts). A fresh (under 15 minutes) PROCESSING claim of SALES_INVOICE, SALES_INVOICE_UPDATE, INVOICE_PAYMENT, INVOICE_EMAIL or WC_INVOICE_NOTE for the order (directly, or viapayload.referenceId), or a CREDIT_NOTE keyed by one of its refunds, makes it throwStoreCreditReviewDeferredError: the whole update rolls back and the delivery is retried, and the review is recorded once the posting has settled.Either the review commits first and the guard reads REVIEW_REQUIRED and refuses, or the posting was authorised first and the review waits. Lock order: order lock, then sync-log reads; no other lock.
tests/concurrency/store-credit-posting-fence.concurrent.test.ts, 9 arms; nothing ordered by sleeping: the parked side is observed in pg_stat_activity): REVIEW FIRST x3 (invoice guard, credit-note guard, payment guard: parked behind the transition's lock, then refuse), GUARD FIRST x5 (a live claim of each of SALES_INVOICE, SALES_INVOICE_UPDATE, INVOICE_PAYMENT, CREDIT_NOTE-by-refund, INVOICE_EMAIL-by-payload: the transition parks behind the guard's lock, then defers; after the claim settles the review commits), one isolating arm (stale 30-minute claim, settled claim with a fresh stamp, another order's claim: no deferral). Printed preconditions in every arm. Unit arms inwc-store-credit-update-path.test.ts(lock before the claim read, no claim read when no review is needed); existing mocks updated for the guards' new transaction.| # | Mutation | Red |
|---|---|---|
| MC1 | transition does not look for the claim | all 5 guard-first arms |
| MC2 | invoice guard reads without the order lock | review-first invoice (never parks) |
| MC3 | credit-note guard reads without the lock | review-first credit note |
| MC4 | follow-up guard reads without the lock | review-first payment |
| MC5 | claim lookup ignores
payload.referenceId| guard-first INVOICE_EMAIL || MC6 | claim lookup ignores refunds | guard-first CREDIT_NOTE |
| MC7 | claim lookup ignores staleness | isolating arm |
| MC8 | claim lookup ignores status (first NOT red: the settled row had a null stamp; the arm now keeps a fresh stamp) | isolating arm |
help-docs/woocommerce.md. Merged origin/development.Gate on the exact head 1890832 (CI env; heavy tiers under the lock; scratch PG17, scram, port 55760; stamp + concurrency on their own freshly migrated DB)
not okis# TODO o3d-qf6qnpm run validategit status --porcelainemptynext build(CIbuild). CI on 1890832: 28/28 check-runs green via the check-runs API.🤖 Generated with Claude Code
https://claude.ai/code/session_01M2fZEeZ5fYPBhB3TEKv9vb