Repository navigation
Serialize concurrent Quickpay callback handling per payment - #128
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 2.x #128 +/- ##
============================================
+ Coverage 92.35% 93.86% +1.51%
- Complexity 189 205 +16
============================================
Files 23 25 +2
Lines 719 766 +47
============================================
+ Hits 664 719 +55
+ Misses 55 47 -8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Quickpay retries callbacks, and a retry can race the original delivery. A new Payum extension (tagged payum.extension factory="quickpay") takes a non-blocking Symfony lock keyed on the quickpayPaymentId around notify handling: it matches the post-rewrap Notify whose model is the payment details, the one shape both the per-payment token endpoint and the shared /payment/quickpay/notify endpoint funnel through exactly once per dispatch. An in-flight duplicate is resolved to a no-op action, so it is acknowledged with the controller's normal 2xx and Quickpay stops retrying without anything being processed twice. A broken lock store fails open, and the lock TTL self-heals after a crashed worker, which is why no processing flag is persisted in the payment details. The default FlockStore serializes per server; the README documents aliasing setono_sylius_quickpay.lock.factory to the framework's lock.factory for multi-server setups. The test application now trusts local tunnel clients (documented in the new dev trusted_proxies.yaml), which makes a full end-to-end run against the real Quickpay API possible; one was performed for this change, covering checkout, authorize callback, concurrent duplicate callbacks on both endpoints, and admin-triggered capture. Closes #109
loevgaard
force-pushed
the
notify-idempotency
branch
from
August 24, 2026 08:39
8c79a04 to
a2dbd66
Compare
loevgaard
commented
Aug 24, 2026
loevgaard
commented
Aug 24, 2026
Instead of hand-wiring a FlockStore and LockFactory as plugin services, the extension now prepends a named framework.lock resource, setono_sylius_quickpay (default store: flock), and injects the factory FrameworkBundle generates for it (lock.setono_sylius_quickpay.factory). That is the Symfony framework way: stores are configured as DSNs, env vars work, the lock log channel is wired, and a multi-server shop overrides the store by redefining the resource in one line of standard configuration - framework.lock resources replace rather than merge, so the plugin default steps aside cleanly.
loevgaard
commented
Aug 24, 2026
…abled flag - resolveQuickpayPaymentId() now reads the id through the gateway library's Details helper (hasPaymentId/paymentId); a present but unusable id surfaces the library's own LogicException instead of being silently ignored - The anonymous no-op action is extracted to AcknowledgeNotifyAction with a docblock explaining the pre-set-action mechanism - The prepended framework.lock config drops 'enabled' => true - FrameworkBundle normalization adds it to any lock array that lacks it
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.
Closes #109.
What this does
Adds
Payum/Extension/NotifyIdempotencyExtension, a Payum extension taggedpayum.extension factory="quickpay", that takes a non-blocking Symfony lock keyed onquickpayPaymentIdaround notify handling. A callback arriving while another one for the same payment is still being processed is resolved to a no-op action, so the controller answers its normal 2xx — Quickpay stops retrying — and nothing is processed twice. Nothing is lost either: notify handling re-fetches the payment from Quickpay, so the processing that holds the lock sees the state the duplicate carried.Scoping decisions
Controller\NotifyAction(which the issue's task list predating rc.1 suggested): since payum-quickpay rc.1, most callbacks arrive on the per-payment Payum token endpoint, not the plugin's shared endpoint. The extension matches the post-rewrapNotifywhose model is the payment details — the one shape both endpoints funnel through exactly once per dispatch (via Sylius'ExecuteSameRequestWithPaymentDetailsAction) — so one lock guards both entry paths, including a token-endpoint delivery racing a shared-endpoint one.framework.lockresource,setono_sylius_quickpay(default store:flock, per-server serialization), prepended by the plugin extension and injected as the generatedlock.setono_sylius_quickpay.factory. Multi-server setups redefine the resource with a shared store DSN in one line of standard configuration — documented in the README's Callbacks section.QuickPay-Callback-Urlon every operation); the shared endpoint stays for operations made outside the store.Tests
Gateway(extension + Sylius' rewrap action) where a second identical callback is delivered while the first is mid-processing — both answer 204, exactly one is processed. Plus sequential callbacks both processing (lock released).Verified live against the real Quickpay API
Full end-to-end run with the test application served through a tunnel (the new documented
dev/trusted_proxies.yamlmakes this possible):authorizedwith scalar-only details.A callback for this Quickpay payment is already being processed), the rest processed sequentially.completed,balance: 1627, Quickpay stateprocessed.