Skip to content

Decide the pending state from what already happened to the money - #81

Merged
loevgaard merged 1 commit into
2.xfrom
fix/status-pending-decides-from-last-approved
Aug 17, 2026
Merged

loevgaard merged 1 commit into
2.xfrom
fix/status-pending-decides-from-last-approved

Conversation

@loevgaard

Copy link
Copy Markdown
Member

Fixes R4 of #71 — now verified live.

Quickpay's pending is not only an authorize held up in 3-D Secure: it is the payment's state whenever an operation is in flight, including an asynchronous capture/refund/cancel on a payment that already has money captured or held. Verified today with a 50 ms poll around an async refund of a captured payment:

t=0.16s state=processed balance=1000 ops=[authorize#1:20000 capture#2:20000]
t=0.61s state=pending   balance=1000 ops=[authorize#1:20000 capture#2:20000 refund#3:PENDING]
t=0.72s state=processed balance=750  ops=[authorize#1:20000 capture#2:20000 refund#3:20000]

StatusAction mapped that state straight to markPending(), so a captured payment reported pending mid-refund, and an authorized one pending while its (auto-)capture was queued — the window is short, but it is exactly when a GetStatus right after issuing an operation lands.

An operation in flight never changes what already happened, so pending now decides like processed: from the last approved operation (authorized / captured / refund-by-balance / canceled), through a shared markFromLastApprovedOperation(). Only a payment with nothing approved yet — no operations, an authorize in 3-D Secure, a declined attempt with a retry in flight — is genuinely pending. Tests for both groups; README, docs/UPGRADE-2.0.md and CLAUDE.md updated. composer all green (352 tests).

Quickpay's `pending` is not only an authorize held up in 3-D Secure:
it is the payment's state whenever an operation is in flight,
including an asynchronous capture, refund or cancel on a payment that
already has money captured or held. Verified live (2026-08-17): a
captured payment read `pending` — with the pre-operation balance — for
the ~second its refund took to settle. StatusAction mapped that state
straight to markPending(), so a captured payment reported pending
mid-refund and an authorized one pending while its capture was queued.

An operation in flight never changes what already happened, so
`pending` now decides like `processed`: from the last APPROVED
operation (authorized / captured / refunded-by-balance / canceled),
via the shared markFromLastApprovedOperation(). Only a payment with
nothing approved yet is genuinely pending.

Refs #71 (R4).
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.42%. Comparing base (fbfaa0a) to head (5170541).

Additional details and impacted files
@@             Coverage Diff              @@
##                2.x      #81      +/-   ##
============================================
+ Coverage     98.40%   98.42%   +0.02%     
- Complexity      199      203       +4     
============================================
  Files            19       19              
  Lines           501      508       +7     
============================================
+ Hits            493      500       +7     
  Misses            8        8              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@loevgaard
loevgaard merged commit c780ab4 into 2.x Aug 17, 2026
21 checks passed
@loevgaard
loevgaard deleted the fix/status-pending-decides-from-last-approved branch August 17, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant