Skip to content

fix: skip stale authorization voided webhooks#659

Open
Vladimir Kalchenko (vkalchenko) wants to merge 2 commits into
shopware:trunkfrom
vkalchenko:fix/authorization-voided-stale-webhook
Open

fix: skip stale authorization voided webhooks#659
Vladimir Kalchenko (vkalchenko) wants to merge 2 commits into
shopware:trunkfrom
vkalchenko:fix/authorization-voided-stale-webhook

Conversation

@vkalchenko
Copy link
Copy Markdown
Contributor

Ignore PAYMENT.AUTHORIZATION.VOIDED webhooks when the authorization resource ID does not match the one stored on the order transaction. This prevents stale webhooks from cancelling unrelated transactions.

1. Why is this change necessary?

When a customer re-authorizes a PayPal payment (e.g. after a failed initial capture), PayPal eventually voids the old authorization and sends a PAYMENT.AUTHORIZATION.VOIDED webhook. The current AuthorizationVoided handler resolves the Shopware order transaction via custom_id (which contains the orderTransactionId) but does not verify whether the voided authorization ID matches the one currently stored on the transaction. Because both the old and new authorizations reference the same orderTransactionId, the handler cancels the transaction - even though the new authorization is still valid on PayPal's side.

This causes Shopware and PayPal to go out of sync: the transaction shows as "Cancelled" in Shopware while the PayPal authorization remains active and capturable.

2. What does this change do, exactly?

In AuthorizationVoided::invoke(), before cancelling the transaction, the patch compares the PayPal authorization ID from the webhook resource ($resource->getId()) against the swag_paypal_resource_id stored in the order transaction's custom fields. If they do not match, the webhook is for a stale/previous authorization and is silently skipped.

3. Describe each step to reproduce the issue or behaviour.

  1. Customer places an order with PayPal (AUTHORIZE intent). The transaction is authorized in both PayPal and Shopware.
  2. A capture attempt fails (e.g. during packing/fulfillment), and the transaction moves to a failed state in Shopware.
  3. Customer re-authorizes the payment via the storefront order edit page (/account/order/edit/{orderId}). A new PayPal authorization is created, and swag_paypal_resource_id is updated on the Shopware transaction.
  4. After some time (minutes to hours), PayPal voids the old authorization and sends a PAYMENT.AUTHORIZATION.VOIDED webhook.
  5. The webhook handler finds the Shopware transaction (same orderTransactionId in custom_id), sees it is in "authorized" state, and cancels it — even though the webhook refers to the old authorization, not the current one.
  6. Shopware now shows the transaction as "Cancelled", but the PayPal authorization is still valid and capturable.

4. Please link to the relevant issues (if any).

5. Checklist

  • I have written tests and verified that they fail without my change
  • I have created an entry in the CHANGELOG.md files with all necessary user information about my changes
  • This change has comments for package types, values, functions, and non-obvious lines of code
  • I have read the contribution requirements and fulfill them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, thanks for the improvement 💙

Comment thread CHANGELOG.md Outdated
- Fixes an issue, where the express checkout could choose a customer country that was not assigned to the correct sales channel (shopware/SwagPayPal#479)
- Fixes an issue, where cookies are added even though associated payment methods are not active (shopware/SwagPayPal#457)
- Fixes an issue, where the state of a country was not correctly transmitted to PayPal (shopware/SwagPayPal#469)
- Fixes an issue, where stale `PAYMENT.AUTHORIZATION.VOIDED` webhooks could cancel unrelated order transactions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version has long been released. Actually, there are currently no unreleased changes.
Please add a new version "tag" here # REPLACE_GLOBALLY_WITH_NEXT_VERSION and put your changelog entry in there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@mstegmeyer Max Stegmeyer (mstegmeyer) requested a review from a team May 7, 2026 07:52
Copy link
Copy Markdown

@untilu29 Chuc Le (untilu29) left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 👍. Just a tiny nitpick (feel free to ignore).
Thanks for the contribution!

Comment on lines +69 to +70
$orderTransactionRepo = $container->get('order_transaction.repository');
$orderTransactionRepo->update([[
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$orderTransactionRepo = $container->get('order_transaction.repository');
$orderTransactionRepo->update([[
$this->orderTransactionRepository->update([[

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, thanks for suggestion

Comment on lines +42 to +47
if ($resource instanceof Authorization) {
$storedResourceId = ($orderTransaction->getCustomFields() ?? [])[SwagPayPal::ORDER_TRANSACTION_CUSTOM_FIELDS_PAYPAL_RESOURCE_ID] ?? null;
if ($storedResourceId !== null && $storedResourceId !== $resource->getId()) {
return;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($resource instanceof Authorization) {
$storedResourceId = ($orderTransaction->getCustomFields() ?? [])[SwagPayPal::ORDER_TRANSACTION_CUSTOM_FIELDS_PAYPAL_RESOURCE_ID] ?? null;
if ($storedResourceId !== null && $storedResourceId !== $resource->getId()) {
return;
}
}
if ($resource instanceof Authorization) {
$customFields = $orderTransaction->getCustomFields() ?? [];
$storedResourceId = $customFields[SwagPayPal::ORDER_TRANSACTION_CUSTOM_FIELDS_PAYPAL_RESOURCE_ID] ?? null;
$isStaleAuthorization = $storedResourceId !== null && $storedResourceId !== $resource->getId();
if ($isStaleAuthorization) {
return;
}
}

tt's just easier to understand

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and updated the code


$webhook = $this->createWebhookV2(Event::RESOURCE_TYPE_AUTHORIZATION);
$resource = $webhook->getResource();
static::assertInstanceOf(Payment::class, $resource);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
static::assertInstanceOf(Payment::class, $resource);
static::assertInstanceOf(Authorization:::class, $resource);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

When a customer re-authorizes a PayPal payment, PayPal voids the old
authorization and sends a PAYMENT.AUTHORIZATION.VOIDED webhook. Because
both old and new authorizations share the same orderTransactionId in
custom_id, the handler was cancelling the transaction even though the
current authorization is still valid.

Before cancelling, compare the webhook resource ID against the
swag_paypal_resource_id stored on the order transaction. If they do not
match, silently skip the stale webhook.
@vkalchenko Vladimir Kalchenko (vkalchenko) force-pushed the fix/authorization-voided-stale-webhook branch from 7459958 to d15497f Compare May 8, 2026 10:47
Comment thread CHANGELOG.md
- Added Apple Pay support for third-party browsers (shopware/SwagPayPal#485)
- Added setting to mark the shop as local environment to preventing connection issues when testing in a non-publicly accessible environment (shopware/SwagPayPal#463)
- Updated and optimized payment method icons
- Fixes an issue, where stale `PAYMENT.AUTHORIZATION.VOIDED` webhooks could cancel unrelated order transactions
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now you moved all entries to that new version. Please only add your entry there.
Also, please provide a translated version in CHANGELOG_de-DE.md (via AI / DeepL or similar if you don't speak German 😉 )

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.

5 participants