Repository navigation
Let a programmatic Capture settle a payment whose Quickpay-side auto-capture was declined - #73
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 2.x #73 +/- ##
============================================
+ Coverage 98.40% 98.42% +0.02%
- Complexity 199 204 +5
============================================
Files 19 19
Lines 501 509 +8
============================================
+ Hits 493 501 +8
Misses 8 8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This was referenced Aug 17, 2026
loevgaard
changed the base branch from
fix/synchronized-rejected-operations
to
2.x
August 17, 2026 09:39
loevgaard
force-pushed
the
fix/rejected-auto-capture
branch
from
August 17, 2026 09:39
0d477cd to
5180cfb
Compare
…capture was declined On a link with auto_capture, CaptureAction returned unconditionally once the payment was authorized: Quickpay captures, so a capture from here could only double up. That also covered the case where Quickpay's own capture had been DECLINED by the acquirer (test card 1000 0000 0000 0032): Quickpay attempts it once and does not retry, the payment reports `authorized`, and every Capture from then on — with or without capture_amount — was a silent no-op. Nothing short of the Quickpay manager could ever take the money, and a caller completing on "no exception" (the Sylius plugin's complete transition) completed a payment that captured nothing. Now the no-op holds while a capture is approved or pending, or none is recorded yet (the moment right after authorization), or on the return trip: a token-carrying Capture still moves no money, the customer lands on the after url with the payment `authorized`, which is the truth. A programmatic Capture (no token — the merchant settling) with the newest capture a completed decline captures through the API like on a plain link, capture_amount included. A decline after an approved capture (an over-capture attempted elsewhere) is not Quickpay giving up and stays a no-op. Refs #71 (R1).
loevgaard
force-pushed
the
fix/rejected-auto-capture
branch
from
August 17, 2026 09:46
5180cfb to
fc4976d
Compare
Member
Author
|
Verified live (2026-08-17, test card
Nothing left to verify here from my side. |
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.
Fixes R1 of #71. Stacked on #72 (shares
Operations::latestOfType()) — retarget to2.xbefore merging #72 so GitHub does not auto-close it when that branch is deleted.The problem
On a link with
auto_capture,CaptureActionreturned unconditionally once the payment was authorized: Quickpay captures, so a capture from here could only double up. That also covered the case where Quickpay's own capture was declined by the acquirer (test card1000 0000 0000 0032, "capture rejected"): Quickpay attempts it once and does not retry, the payment reportsauthorized, and everyCapturefrom then on — with or withoutcapture_amount— was a silent no-op. Nothing short of the Quickpay manager could ever take the money. Confirmed with the mock seam: authorize20000+ capture40000+link.auto_capture: true→Captureissued nothing.Sharpest consequence:
setono/sylius-quickpay-plugin'sPaymentProcessorrunsGetStatus(not captured) →Capture→ no exception → the Sylius payment completes with no money captured.The rule now
On an auto-capture link,
Capturestays a no-op whileauthorized, which is the truth; the return trip never moves money.When the newest capture is a completed decline and the
Captureis programmatic (no token — the merchant settling: the shop's own code, a state-machine hook), it captures through the API like on a plain link,capture_amountincluded. A decline after an approved capture (an over-capture attempted elsewhere) is not Quickpay giving up and stays a no-op.The token distinction is Payum's own: its controllers execute token-carrying requests, programmatic callers pass the details. It keeps "the return trip never captures" intact — the invariant #69 established — while giving the merchant a way to settle. The alternative (always fall through) would have re-issued a capture on the return trip and, if
stategoespendingfor an async op (R4 in #71), parked a Sylius payment inprocessinguntil a reconcile.Live check before merging
composer e2e:create, pay with1000 0000 0000 0032, confirm the return trip logsdoneand status isauthorized; thencomposer e2e:operate -- capture <number>— expect one capture issued by the gateway (accepted or declined by the acquirer, but issued), where before it was a silent no-op. Please also note whether Quickpay records its declined attempt as an operation withpending: falseand a non-20000qp_status_code— that is the shape the tests assume.Docs
README (flow section),
docs/UPGRADE-2.0.md, CLAUDE.md, class docblock.Checks
composer alland the dependency analyser green locally; 328 tests.