Skip to content

fix(paypal): a capture is credited only in the currency the shop charges in - #186

Merged
Panelica merged 2 commits into
Panelica:mainfrom
ertugrulkeyvan:fix/paypal-capture-currency
Oct 12, 2026
Merged

Panelica merged 2 commits into
Panelica:mainfrom
ertugrulkeyvan:fix/paypal-capture-currency

Conversation

@ertugrulkeyvan

Copy link
Copy Markdown
Contributor

What

A PayPal capture is now credited only when it is in the shop's currency.

The PayPal buttons build the order in the browser (actions.order.create), currency included. Before crediting, the capture is verified with PayPal (verifyCapture()), which checks that it is COMPLETED and takes its amount from PayPal rather than from the browser. It did not check the currency.

So a capture of 100.00 in a currency worth far less than the shop's was credited as 100.00 in the shop's currency. With a USD shop, a 100.00 USD invoice was marked paid by a 100 HUF capture. This happens on both paths:

  • the capture endpoint (/gateway/paypal/capture/{invoice});
  • the PAYMENT.CAPTURE.COMPLETED webhook.

Whether such a capture completes depends on the merchant account's settings for payments in currencies it does not hold. Many accounts accept and convert them.

After this:

  • Verification: verifyCapture() refuses a capture in any currency but the expected one, the shop's by default, compared in upper case.
  • Capture endpoint: a refused capture leaves the invoice unpaid, and the existing payment-failed notification goes out.
  • Webhook: a refused capture is not applied.
  • Captures in the shop currency are credited exactly as before.

How

  • PayPalModule::verifyCapture(): takes an optional $expectedCurrency, defaulting to shop_currency_code(). It compares that with amount.currency_code, logs a mismatch and refuses it.
  • Callers: both callers, the capture endpoint and processWebhook(), use the default, so neither changes.

The work is split into two commits: the fix, then its tests.

Tests

tests/Feature/PayPalCaptureCurrencyTest.php, 4 tests:

  • Verification: a capture in another currency is refused. One in the shop currency is accepted, in either letter case.
  • Capture endpoint: another currency leaves the invoice unpaid with no transaction.
  • Webhook: a capture in another currency is not applied.

Three of them fail on main; there, the 100 HUF capture marks the 100.00 USD invoice paid. PayPalWebhookSecurityTest and PayPalTokenisationTest pass unchanged (63).

Full suite: 4107 passed. The one failure is the CurrencyAndBackupTest db backup test, which fails on main too in my local setup.

@Panelica
Panelica merged commit e5d2a53 into Panelica:main Oct 12, 2026
1 check passed
@Panelica

Copy link
Copy Markdown
Owner

Thank you, @ertugrulkeyvan, merged. A PayPal capture is now credited only in the currency the shop charges in, so a capture of 100 in a cheaper currency can no longer settle an invoice of 100 in the shop's.

Checked before merging: every order the panel creates, from the pay button and from the unattended charge of a stored PayPal account, is opened in the shop currency, so genuine payments go through as before. Three of your tests fail on main and all four pass here, the existing PayPal webhook and tokenisation tests pass unchanged, and the full suite passes.

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.

2 participants