Skip to content

feat(gift-cards): vendor-agnostic gift card spend rail — TBC adapter, purchase orchestration, GraphQL API (ENG-574) - #508

Open
forge0x wants to merge 11 commits into
mainfrom
feat/gift-cards-provider
Open

forge0x wants to merge 11 commits into
mainfrom
feat/gift-cards-provider

Conversation

@forge0x

@forge0x forge0x commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Vendor-agnostic gift card spend rail for Flash: one GiftCardProvider port routed by country, The Bitcoin Company (TBC) as the first adapter, Bitrefill wired in config only until partner credentials arrive. Users pick a card, Flash pays the vendor's BOLT11 from the user's own wallet over the IBEX rail, the code comes back encrypted at rest and is shown only to the owner.

Linear: epic ENG-574 (project "Gift Cards — Spend Rail"), design doc "Gift Cards — Architecture Design". Tickets covered here: ENG-576, 577, 578, 579, 580, 581, 582, 583, and the docs half of 588.

What is in this PR

  • Domain + configsrc/domain/gift-cards, giftCards config block (routing, providers, catalog, limits off|log-only|enforce, flags-off baseline), GiftCard* errors registered and mapped, RateLimitConfig.giftCardPurchase, giftcard ops-event flow.
  • TBC adaptersrc/services/gift-cards/bitcoin-company: JWT auth cached in Redis under a lock, paginated catalog, quote, order, status with claim data; zod-validated vendor envelopes; retries only on idempotent reads; tokens and claim data redacted from logs. Shared provider contract test.
  • PersistenceGiftCardOrder Mongo schema + repository with conditional transition(), partial-unique providerOrderId, unique {walletId, idempotencyKey}, migration. Claim codes AES-256-GCM with per-order key id; toJSON never emits ciphertext.
  • Catalog — Redis cache per provider/country with stale marker; 6h sync in cron under a lock; list/search/paginate served from cache only.
  • Purchase pathpurchaseGiftCard: gate → product → quote → authorize → persist CREATED → vendor order → quote tolerance → INVOICE_ISSUED → idempotent payment → PAID/PAYMENT_PENDING/PAYMENT_FAILED → one fulfilment read. Payment uses the IBEX inline rail (src/app/payments/pay-invoice-via-ibex.ts, mirrors lnInvoicePaymentSend) inside withPaymentIdempotency with fingerprint ln|<bolt11>|giftcard|<orderId>; IBEX transaction.id stored as providerPaymentRef.
  • Limits — level, account age, per-card and daily caps (Redis reservations), vendor caps, velocity, open-loop rule; log-only by default with would-reject ops events (same rollout discipline as ENG-573).
  • WorkerreconcileGiftCardOrders: expiry, pending settlement via IBEX lookup, vendor polling with backoff, REFUND_REQUIRED escalation; cron job + 30s trigger interval. Fulfilment push notification with no claim data.
  • GraphQLgiftCardCatalog, giftCardQuote, giftCardOrder, giftCardOrders, giftCardPurchase, globals.giftCardsEnabled; relay connections; mutation at wallet level; API-key scope map entries BLOCKED; SDL + supergraph regenerated.
  • Docsdocs/gift-cards/{README,ARCHITECTURE,FLOWS,API,CONFIG,ALERTING,RUNBOOK,TESTING}.md.

Feature is off by default (giftCards.enabled: false, every provider disabled). No behaviour change until config overrides turn it on.

Review notes

  • Order reads (giftCardOrder, giftCardOrders) are deliberately not behind the master gate: switching the rail off must never hide codes a customer already paid for. Ownership is enforced in the app layer.
  • Payments.payInvoiceByWalletId was not used on purpose: it pays through LND, which this deployment does not run.
  • Country routing is phone-based (Account has no country); NANP numbers fall back to routing.default. Persisting a country on the account is an open decision (ENG-575).
  • make codegen / yarn check:sdl fail locally on two pre-existing Express type errors in src/servers/graphql-server.ts and graphql-admin-server.ts (they fail on main too). SDL was regenerated with the same steps run by hand.
  • Known gaps, documented in the runbook rather than hidden: no claim-key rotation script yet; no hash-based fallback when providerPaymentRef is missing; feeBps and referralCode are config-only; failureReason is machine text.

Tests

  • Full yarn test:unit: 290 suites, 3667 passed, 3 skipped.
  • New: ~600 unit tests across domain, adapter, persistence, crypto, cache, orchestration, limits, worker, GraphQL; Mongo integration spec for the transition race (test/flash/integration/gift-cards, not run locally, needs Mongo).
  • yarn tsc-check: clean except the two pre-existing errors above. eslint: clean on all touched files.

Not in this PR

Mobile (ENG-589–591), ERPNext writer (ENG-584, blocked on doctype decision), Bitrefill adapter (ENG-585, blocked on credentials), alert wiring/dashboards (ENG-588), rollout (ENG-592).

🤖 Generated with Claude Code

https://claude.ai/code/session_01W5e4H8MF7oX2LPUxBRW3Pf

bobodread876 and others added 4 commits September 9, 2026 23:59
…ration (ENG-574)

Vendor-agnostic gift card spend rail. One GiftCardProvider port, routed by
country from config; The Bitcoin Company is the first adapter (self-serve,
US/CA/EU catalog), Bitrefill is wired in config only. Orchestration, limits,
idempotency, persistence, and accounting hooks live once in Flash.

Domain + config
- src/domain/gift-cards: product/quote/order/claim types, IGiftCardProvider
  port, order state machine (GIFT_CARD_TRANSITIONS), GiftCard* errors
  registered in app/errors and mapped in graphql/error-map.
- giftCards config block (routing, providers, catalog, limits with
  off|log-only|enforce) + flags-off baseline; RateLimitConfig.giftCardPurchase;
  "giftcard" ops-event flow.

Adapter (ENG-577)
- services/gift-cards/bitcoin-company: JWT auth cached in Redis under a lock,
  paginated catalog, quote, order (BOLT11), status with claim data; zod-validated
  envelopes; retries only on idempotent reads; tokens and claim data redacted.
- Shared provider contract test.

Persistence (ENG-579)
- GiftCardOrder Mongo schema + repository with conditional transition(),
  partial-unique providerOrderId index, unique {walletId, idempotencyKey},
  migration; claim codes AES-256-GCM at rest with per-order keyId; toJSON
  never emits ciphertext.

Catalog (ENG-578)
- Redis catalog cache per provider/country with stale marker; 6h sync job in
  cron under a lock; list/search/paginate served from cache only.

Purchase path (ENG-580/581/583)
- purchaseGiftCard: gate -> product -> quote -> authorize -> persist CREATED ->
  vendor order -> quote tolerance -> INVOICE_ISSUED -> idempotent payment ->
  PAID/PAYMENT_PENDING/PAYMENT_FAILED -> one fulfilment read.
- Payment goes through the IBEX inline rail (new app/payments/
  pay-invoice-via-ibex.ts mirroring lnInvoicePaymentSend) inside
  withPaymentIdempotency with fingerprint ln|<bolt11>|giftcard|<orderId>;
  IBEX transaction id persisted as providerPaymentRef for pending re-checks.
  payInvoiceByWalletId is the dormant LND rail and is not used.
- authorizeGiftCardPurchase: level, account age, per-card and daily caps
  (Redis reservations), vendor caps, velocity, open-loop rule; log-only by
  default with would-reject ops events.
- reconcileGiftCardOrders: expiry, pending settlement via IBEX lookup,
  vendor polling with backoff, REFUND_REQUIRED escalation; cron + 30s trigger.
- Fulfilment push notification (no claim data in the payload).

Tests: 477 unit tests across domain, adapter, persistence, crypto, cache,
orchestration, limits, worker; Mongo integration spec for the transition race.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5e4H8MF7oX2LPUxBRW3Pf
…(ENG-582)

Public API for the gift card rail.

- Queries: giftCardCatalog (relay connection over the Redis catalog, country
  defaults to the account's), giftCardQuote, giftCardOrder, giftCardOrders.
- Mutation: giftCardPurchase(input { productId, value, quantity, walletId,
  idempotencyKey }) at wallet level so the existing walletId middleware proves
  ownership before the resolver; payload carries the order and, when already
  fulfilled, the decrypted claim.
- globals.giftCardsEnabled = feature on AND at least one provider enabled.
- Catalog, quote, and purchase open with the master gate. Order reads are
  deliberately ungated: they are owner-scoped reads of orders the customer
  already paid for, and switching the rail off must never hide their codes.
- GiftCardOrderStatus enum values are typed against the domain union; the
  order type is built from an allow-list mapper so ciphertext/keyId can never
  be exposed. failureReason is machine-oriented for now.
- API-key scope map: all five fields BLOCKED.
- SDL and supergraph regenerated (write-sdl + compose-supergraph run by hand
  because `make codegen` trips on the two pre-existing Express type errors in
  `yarn build`).

Tests: 120 resolver/type/enum tests incl. api-key scope enforcement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5e4H8MF7oX2LPUxBRW3Pf
…document ungated order reads

- RateLimitConfig.giftCardPurchase used getFygaroCheckoutCreateAttemptLimits()
  by copy-paste; point it at getGiftCardPurchaseAttemptLimits() (same values
  today, independent knob from now on).
- gift-card-gate.ts header and globals.giftCardsEnabled description said every
  giftCard* field is gated; only catalog, quote, and purchase are. Order history
  and delivered codes stay readable with the rail off. SDL regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5e4H8MF7oX2LPUxBRW3Pf
…, testing (ENG-588)

docs/gift-cards/{README,ARCHITECTURE,FLOWS,API,CONFIG,ALERTING,RUNBOOK,TESTING}.md
written against the code as built, including the as-built deviations from the
design: IBEX inline payment rail, phone-only country resolution, no claim-key
rotation script yet, no hash-based payment fallback when providerPaymentRef is
missing, feeBps/referralCode defined but unused, Bitrefill present in config
only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5e4H8MF7oX2LPUxBRW3Pf
@linear

linear Bot commented Sep 10, 2026

Copy link
Copy Markdown

ENG-574

…quote budget, pagination (#508)

Blocking
- Indeterminate send errors no longer become PAYMENT_FAILED. Only proven
  refusals (insufficient balance, FailedIbexPayment, send-guard rejection,
  idempotency key errors) are terminal; a generic IbexError / unconfirmed
  payment moves the order to PAYMENT_PENDING and the pending order is
  returned so the client polls instead of re-buying. The reconciler falls
  back to a vendor poll when IBEX cannot account for the payment (no
  providerPaymentRef or unknown), including for INVOICE_ISSUED orders at
  expiry, so a paid invoice can no longer be written off.
- giftCardQuote now consumes its own attempt budget (30/min, 5 min block)
  before touching the vendor, with the same store-fault fall-through as
  purchase, and carries query complexity 120. New error code
  GIFT_CARD_QUOTE_RATE_LIMITED.

Should fix
- 24h fulfilment timeout polls the vendor first; REFUND_REQUIRED only when
  the vendor does not report fulfilled.
- The giftCards.enabled kill switch stops new money leaving only. The
  reconciler runs whenever non-terminal orders exist and settles through the
  registered (not enabled) provider, so paid customers still get their codes
  and the 24h alert still fires.
- Same-key replay of an unpaid, unexpired INVOICE_ISSUED order resumes from
  the pay step (idempotent by construction) instead of returning a row that
  would only ever expire. CREATED is never re-entered.
- giftCardOrder returns the order with claim null and records the crypto
  error at Critical instead of throwing; the mutation keeps returning the
  order plus the mapped error.
- Vendor expiresAt is nullable; the order's expiry is the earliest of the
  order TTL, the decoded BOLT11 expiry, and any vendor-stated expiry.
- TBC catalog pagination advances by rows returned and stops on an empty
  page; at the page cap the sync fails (keeping the last good catalog)
  rather than writing a truncated or duplicate-heavy one.

PAYMENT_FAILED enum description updated; SDL regenerated. Docs updated to
match as-built behaviour (API, FLOWS, ARCHITECTURE, ALERTING, RUNBOOK,
CONFIG, TESTING).

Tests: full yarn test:unit green (see PR); ~150 new or retargeted specs
across purchase, reconcile, settle, registry, quote, get-order, resolvers,
adapter client and mapping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5e4H8MF7oX2LPUxBRW3Pf
@forge0x

forge0x commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Review round 1 applied — 3fc2191ef

Simon-style review returned NEEDS WORK (2 blocking, 6 should-fix). All eight are addressed in this commit.

Blocking

  • Indeterminate send errors no longer land in PAYMENT_FAILED. Only proven refusals do; a generic IBEX error or unconfirmed payment moves the order to PAYMENT_PENDING, the pending order is returned so the client polls instead of re-buying, and the reconciler falls back to a vendor poll when IBEX cannot account for the payment (including INVOICE_ISSUED at expiry, which closes the documented write-off window).
  • giftCardQuote now consumes its own attempt budget (30/min) before touching the vendor and carries query complexity 120. New code GIFT_CARD_QUOTE_RATE_LIMITED.

Should fix

  • 24h fulfilment timeout polls the vendor first; REFUND_REQUIRED only when the vendor does not report fulfilled.
  • giftCards.enabled now stops new money leaving only. The reconciler runs whenever non-terminal orders exist and settles through the registered provider, so paid customers still get codes and the 24h alert still fires.
  • Same-key replay of an unpaid, unexpired INVOICE_ISSUED order resumes from the pay step (idempotent by construction). CREATED is never re-entered.
  • giftCardOrder returns the order with claim: null and records a claim-crypto error at Critical instead of throwing.
  • Vendor expiresAt is nullable; the order expiry is the earliest of order TTL, decoded BOLT11 expiry, and any vendor-stated expiry.
  • TBC catalog pagination advances by rows returned and stops on an empty page; at the page cap the sync fails and keeps the last good catalog.

PAYMENT_FAILED enum description updated and SDL regenerated. Docs updated to as-built. Two authentication specs that enumerate @config getters gained the two new limit getters.

Full yarn test:unit: 292 suites, 3738 passed, 3 skipped. Round 2 review in progress.

… escalation, post-pay bookkeeping, vendor hardening (#508)

Blocking
- A busy idempotency lock during a same-key replay no longer writes
  PAYMENT_PENDING with a null ref over the first attempt's outcome; it returns
  the current row unchanged (regression from round 1).
- The 24h fulfilment escalation writes REFUND_REQUIRED only when the final
  vendor poll positively reports not fulfilled. Vendor errors, claim-key
  faults, and repository faults keep the order PAID, are counted, and retry.
  The claim key is verified loadable (claimCryptoReady) immediately before
  every pay, on first attempt and replay-resume; failure fails the order
  (claim-key-not-configured) before IBEX is called.
- After IBEX has answered, purchaseGiftCard never returns a bare error. A
  lost transition re-reads and returns the row (PAID/FULFILLED/PENDING); a
  repository fault returns the in-memory order and pages a Critical
  giftcard/paid-not-recorded event. Same treatment on the pending and failed
  bookkeeping steps.
- quantity is capped per product (GiftCardProduct.maxQuantity; 1 for TBC
  until a multi-card fulfilment response is captured); the adapter refuses
  quantity > 1 before calling the vendor.

Should fix
- Resolver-level gate removed from giftCardPurchase so a same-key replay
  still returns the existing order while the rail is off.
- PAYMENT_PENDING with no IBEX ref, vendor not-paid, past expiresAt + 24h ->
  PAYMENT_FAILED (payment-unresolved-expired); polls that leave status
  unchanged bump updatedAt so the worker batch rotates (repo touch()).
- New transition EXPIRED -> PAID (payment-settled-after-expiry) so a late
  Success after worker expiry is recorded and fulfilled, not written off.
- Unique-index race with no readable winner returns UnknownGiftCardError
  (the repository duplicate-key error no longer reaches error-map).
- Product country is compared with the account's gated country at quote and
  purchase (skipped when the country is unknown), as the docs claimed.
- Registry requires a provider to be registered as well as enabled; byCountry
  keys are case-normalised.
- Vendor 4xx text no longer reaches customers: fixed message on order
  rejection; a 4xx on quote maps to GIFT_CARD_INVALID_VALUE at Warn.
- TBC Disputed is held pending, not treated as refunded. Physical and
  non-Lightning products are skipped at sync (counted); non-resellable rows
  are flagged and kept until KYB (TODO ENG-586). VariableNoCents products
  carry wholeUnitsOnly and cents are refused up front. Non-load-bearing
  vendor fields are nullish. Inline first poll uses retry:false.
- A sync that yields zero products is a failed sync; the previous catalog
  keeps serving. Cache reads validate shape, default legacy rows, and
  de-listed product keys are deleted on each sync.
- Claim ciphertext AAD now binds version, keyId, and orderId, so a ciphertext
  cannot be transplanted between orders. Strict base64 check on decrypt.
- Same-key replay no longer consumes the purchase rate limit.
- Test fixtures' fake repository enforces the real transition table.

GraphQL: GiftCardProduct gains maxQuantity and wholeUnitsOnly; paidSats
description corrected; SDL regenerated. Docs brought to as-built throughout.

Tests: full yarn test:unit green (see PR); +~350 specs incl. list-orders,
registry, domain primitives, claim-crypto order binding, mutation payload
shape after payment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5e4H8MF7oX2LPUxBRW3Pf
@forge0x

forge0x commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Review round 2 applied — 7fbdf3a2d

Round 2 (reviewing 3fc2191ef) returned NEEDS WORK: 4 blocking, 18 should-fix. All addressed.

Blocking

  • A busy idempotency lock during a same-key replay no longer clobbers the first attempt's outcome with a null-ref PAYMENT_PENDING; it returns the current row unchanged (round-1 regression).
  • 24h escalation writes REFUND_REQUIRED only when the final vendor poll positively says not fulfilled; vendor errors, claim-key faults, and repo faults keep PAID and retry. The claim key is checked loadable immediately before every pay; failure fails the order before IBEX is called.
  • After IBEX answers, purchaseGiftCard never returns a bare error: lost transitions re-read and return the row; a repo fault returns the order and pages paid-not-recorded.
  • quantity is capped per product (maxQuantity, 1 for TBC until a multi-card fulfilment response is captured); the adapter refuses more before calling the vendor.

Should fix (highlights)

  • Resolver gate removed from giftCardPurchase so a replay works while the rail is off.
  • No-ref PAYMENT_PENDING self-terminates after expiresAt + 24h when the vendor reports not-paid; polls bump updatedAt so the batch rotates.
  • New EXPIRED → PAID transition so a late Success after worker expiry is fulfilled, not written off.
  • Product country is enforced against the account's gated country (as documented). Registry requires registration + enabled.
  • Vendor 4xx text never reaches customers; vendor quote 4xx → GIFT_CARD_INVALID_VALUE. TBC Disputed held pending. Physical/non-Lightning rows skipped at sync; VariableNoCents refuses cents up front; zero-product syncs fail closed.
  • Claim ciphertext AAD binds keyId + orderId (no cross-order transplant).
  • Replay does not consume the purchase rate limit; fake repo enforces the real transition table; ~350 new/retargeted specs.

GraphQL: GiftCardProduct.maxQuantity, wholeUnitsOnly; SDL regenerated. Docs brought to as-built.

Full yarn test:unit: 294 suites, 3920 tests (3 skipped), green. One earlier run hit a jest worker SIGSEGV in an unrelated suite; it passes in isolation on branch and main and the rerun is clean. Round 3 review in progress.

bobodread876 and others added 5 commits September 14, 2026 15:10
Local node_modules carries prettier 3.9.6 while yarn.lock pins 3.6.2 (what
CI installs); the two disagree on long union types, so `ops-events.ts` and
`fixtures.ts` passed locally and failed Check Code. Reformatted with the
pinned version. Also reworded "mis-mapping" (x2) and "unparseable" for the
typos check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014etuRoG7S3DxsAmWMP9jAX
…y and pending exit holes (#508)

Three review findings on the reconcile worker's write-off paths:

1. Expiry wrote off a PAID order when the vendor reported
   paidPendingFulfillment. settleOrderFromVendor returned the order
   unchanged for it, so processExpiry saw no movement and expired an
   INVOICE_ISSUED row the vendor had positively vouched for — money left,
   a card ships, the row says EXPIRED and nothing polls it again. The
   vouch is now recorded: settleOrderFromVendor's paidPendingFulfillment
   arm transitions INVOICE_ISSUED/EXPIRED -> PAID (reason
   vendor-reported-payment, paidSats = invoiceSats ?? quoteSats) and the
   normal PAID poll path finishes fulfilment. PAYMENT_PENDING keeps its
   payment re-read as the authority; PAID is already past it.

2. A PAYMENT_PENDING order WITH a providerPaymentRef whose IBEX send
   stayed IN_FLIGHT forever had no terminal exit and was never
   vendor-polled — the payment-unresolved escalation required no ref.
   Past the PAYMENT_PENDING_WARN_MS warn horizon the vendor is now
   polled too: its payment vouch or fulfilment settles the order, and a
   vendor awaitingPayment past the 24h grace escalates to
   payment-unresolved-expired with ibex: "in-flight" in the event.

3. An INVOICE_ISSUED order whose expiry vendor poll ERRORED was expired
   unconditionally; EXPIRED is never revisited, so one transient outage
   permanently buried a possibly-paid order. A non-answer (vendor
   unreachable) or a vouch that failed to record now leaves the row
   INVOICE_ISSUED, touched so the batch rotates; expiry fires only when
   the vendor answers and does not report payment.

The expiry spec expectations that asserted EXPIRED under the
paidPendingFulfillment mock are rewritten to the new behaviour; new
tests cover the vouch transition, the with-ref in-flight vendor poll and
its escalation, and the deferred expiry. Prettier 3.6.2 (pinned) and
typos clean; tsc and eslint clean on touched files.

Co-Authored-By: Claude Code <noreply@anthropic.com>
#508)

A PAYMENT_PENDING order with no providerPaymentRef gets "unknown" back
from the payment re-read unconditionally — lookupSentPaymentStatus never
asks IBEX without a ref — so the vendor is the only possible arbiter for
that row. vendorVouchedPayment nevertheless excluded it, and
vendorFailed's PAYMENT_PENDING arm returned it unchanged on every vendor
answer: a send that errored without a verdict (socket reset / 5xx after
IBEX accepted) and then settled at the vendor polled forever, recording
nothing — never PAID, never the 24h escalation.

- vendorVouchedPayment now accepts a ref-less PAYMENT_PENDING row's
  paidPendingFulfillment vouch (reason vendor-reported-payment); rows
  with a ref keep the payment re-read as the authority. The PAID path's
  24h fulfilment timeout and the vendor-positive REFUND_REQUIRED
  escalation remain the safety net if the vouch proves wrong.
- vendorFailed's no-ref arm terminates on vendor refunded (proof money
  moved: PAID via vendor-reported-payment, then REFUND_REQUIRED, which
  pages). A plain failed still only warns — it says nothing about money
  that may have left.
- FLOWS.md §4/§6/§4a and ALERTING.md order-paid row brought in line with
  the vouch behaviour (FLOWS was stale from the previous round;
  ARCHITECTURE.md was already updated).
- reconcile-orders.spec's settlement mirror and paidPendingFulfillment
  cases extended to cover the no-ref vouch; settle-order.spec gains
  with-ref vs no-ref coverage and the refunded two-step.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…ref test

check-code (tsc) failed on the it.each ternary: an inferred
{ kind: string } is not assignable to GiftCardProviderOrderStatus. Give
the argument an explicit annotation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
)

- FLOWS.md: the with-ref pending vouch is fulfilled only — a
  paidPendingFulfillment answer is left for the re-read.
- ALERTING.md: vendor-reported-payment on a ref-less PAYMENT_PENDING row
  also covers the vendor's refunded (lands PAID then REFUND_REQUIRED).

Co-Authored-By: Claude Code <noreply@anthropic.com>
@islandbitcoin

Copy link
Copy Markdown
Contributor

Review rounds 3–5 applied

Three more simon-review rounds ran over f622de7d9/7ac5dda94. Round 1 (of this batch) found one blocking: the expiry path wrote off a PAID order when the vendor reported paidPendingFulfillment — that vouch now transitions INVOICE_ISSUED/EXPIRED → PAID (vendor-reported-payment) and the normal PAID poll path finishes fulfilment (f622de7d9). Also fixed the with-ref IN_FLIGHT dead-end (vendor polled past the warn horizon; fulfilled settles, unpaid past grace escalates) and the expire-on-vendor-error hole (a failed poll no longer expires the row unconditionally).

Rounds 2–3 returned ACCEPTABLE, 0 blocking; their remaining items were doc drift, fixed in 37fc89966:

  • FLOWS.md: the with-ref pending vouch is fulfilled only — a paidPendingFulfillment answer is left for the re-read.
  • ALERTING.md: vendor-reported-payment on a ref-less PAYMENT_PENDING row also covers the vendor's refunded (lands PAID then REFUND_REQUIRED).

Final verdict: no blocking, no should-fix outstanding. Review history this PR: rounds 1–2 (2026-09-12), rounds 3–5 (this batch, 2026-09-14).

🤖 Generated with Claude Code

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.

3 participants