Skip to content

Fix PayflowPro response resolver notifying a payment failure for a cart with no Payflow payment - #41253

Open
TuVanDev wants to merge 1 commit into
magento:2.4-developfrom
TuVanDev:fix/payflowpro-response-guard-payment-context
Open

Fix PayflowPro response resolver notifying a payment failure for a cart with no Payflow payment#41253
TuVanDev wants to merge 1 commit into
magento:2.4-developfrom
TuVanDev:fix/payflowpro-response-guard-payment-context

Conversation

@TuVanDev

@TuVanDev TuVanDev commented Sep 10, 2026

Copy link
Copy Markdown
Member

Description (*)

Magento\PaypalGraphQl\Model\Resolver\PayflowProResponse::resolve() loads the cart from the caller-supplied masked cart_id and, on any validation failure of paypal_payload, calls PaymentFailuresInterface::handle(), which sends the merchant "Payment Transaction Failed" notification email:

} catch (LocalizedException $exception) {
    $parameters['error'] = true;
    $parameters['error_msg'] = $exception->getMessage();
    $this->paymentFailures->handle((int) $cart->getId(), $parameters['error_msg']);
    throw new GraphQlInputException(__($exception->getMessage()));
}

Nothing between loading the cart and this point confirms the cart ever entered a Payflow payment flow. A well-formed request for a cart that simply has no Payflow method selected still fails validation and still reaches handle(), so the store sends itself the merchant payment-failure notification even though no genuine payment was ever attempted on that cart. The legitimate Payflow Pro flow always establishes a Payflow method first (setPaymentMethodOnCart(payflowpro), then createPayflowProToken), so a real decline always carries a Payflow method; only the no-payment case is unguarded.

This PR requires a Payflow payment method on the cart before the resolver proceeds:

$selectedMethod = (string)$cart->getPayment()->getMethod();
if ($selectedMethod !== Config::METHOD_PAYFLOWPRO
    && $selectedMethod !== Transparent::CC_VAULT_CODE
) {
    throw new GraphQlInputException(__('Transaction has been declined.'));
}

A genuine Payflow decline still carries a Payflow method and still notifies, so the intended behavior is unchanged. A cart with no Payflow method is refused with the resolver's existing Transaction has been declined. message and no notification is sent. Config::METHOD_PAYFLOWPRO and Transparent::CC_VAULT_CODE are the complete set of Payflow method codes that reach this resolver in core (Transparent was already imported; Config is added as a use).

Related Pull Requests

Same change submitted to Mage-OS: mage-os/mageos-magento2#341

Fixed Issues (if relevant)

N/A. No existing GitHub issue was found. Searched magento/magento2 issues and pull requests for PayflowProResponse, handlePayflowProResponse payment failed, and open PRs with PayflowProResponse in the title; none matched. Search coverage is not proof of absence, so please close this as a duplicate if a prior report exists.

Manual testing scenarios (*)

The module ships no unit tests, so this PR adds PayflowProResponseTest. The behavior can also be exercised end to end on a store with Magento_PaypalGraphQl enabled:

  1. Obtain a guest cart with no payment method selected (for example via createEmptyCart).
  2. Call the handlePayflowProResponse mutation for that cart with a paypal_payload that fails response validation.
    • Before: the resolver reaches PaymentFailuresInterface::handle() and the store sends the merchant "Payment Transaction Failed" notification, even though the cart never had a Payflow payment selected.
    • After: the resolver returns the Transaction has been declined. input error and sends no notification.
  3. Regression, genuine decline: select a Payflow method first (setPaymentMethodOnCart with payflowpro), then submit a declining response. Both before and after this change the merchant notification is sent, confirming genuine Payflow declines are unaffected.

Contribution checklist (*)

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests. Added app/code/Magento/PaypalGraphQl/Test/Unit/Model/Resolver/PayflowProResponseTest.php (the module had no unit tests). It covers a cart with no Payflow method (and an unrelated method) being refused without notifying, a genuine Payflow decline still notifying, and a valid response returning the cart, across both payflowpro and payflowpro_cc_vault.
  • All automated tests passed successfully. Ran the new test on 2.4-develop with PHPUnit 12.5.14: 7/7 green, and confirmed it fails (getResponseObject ... was not expected to be called) when the guard is reverted, so it is a real regression guard. vendor/bin/phpcs --standard=Magento2 reports 0 errors and 0 warnings on both changed files. I did not run the full unit suite locally; relying on CI for that.

…rt with no Payflow payment

Magento\PaypalGraphQl\Model\Resolver\PayflowProResponse loads the cart from
the caller-supplied masked cart_id and, on any validation failure of the
paypal_payload, calls PaymentFailuresInterface::handle(), which sends the
merchant "Payment Transaction Failed" notification. It does this without
confirming the cart ever entered a Payflow payment flow, so a well-formed
request for a cart that has no Payflow method selected reaches the
notification even though no genuine payment was attempted.

Require a Payflow payment method (payflowpro or payflowpro_cc_vault) on the
cart before the resolver proceeds. The legitimate Payflow Pro flow selects
the method with setPaymentMethodOnCart before this resolver runs, so a
genuine decline still carries a Payflow method and still notifies; a cart
with no such method is refused with the existing "Transaction has been
declined." error and no notification is sent.

The module had no unit tests; this adds coverage for all three cases: a cart
without a Payflow method is refused without notifying, a genuine Payflow
decline still notifies, and a valid response returns the cart.
@m2-assistant

m2-assistant Bot commented Sep 10, 2026

Copy link
Copy Markdown

Hi @TuVanDev. Thank you for your contribution!
Here are some useful tips on how you can test your changes using Magento test environment.
❗ Automated tests can be triggered manually with an appropriate comment:

  • @magento run all tests - run or re-run all required tests against the PR changes
  • @magento run <test-build(s)> - run or re-run specific test build(s)
    For example: @magento run Unit Tests

<test-build(s)> is a comma-separated list of build names.

Allowed build names are:
  1. Database Compare
  2. Functional Tests CE
  3. Functional Tests EE
  4. Functional Tests B2B
  5. Integration Tests
  6. Magento Health Index
  7. Sample Data Tests CE
  8. Sample Data Tests EE
  9. Sample Data Tests B2B
  10. Static Tests
  11. Unit Tests
  12. WebAPI Tests
  13. Semantic Version Checker

You can find more information about the builds here
ℹ️ Run only required test builds during development. Run all test builds before sending your pull request for review.


For more details, review the Code Contributions documentation.
Join Magento Community Engineering Slack and ask your questions in #github channel.

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