Skip to content

CaptureAction early return breaks payment retry after abandoned checkout #329

Description

@jorisduenas

Description

When a customer abandons the Mollie payment page (closes the tab without paying) and later retries payment from their account page, the order gets stuck in an unrecoverable state.

Steps to reproduce

  1. Customer places an order and selects a Mollie payment method (e.g. iDEAL, PayPal, Klarna)
  2. Customer is redirected to the Mollie checkout page
  3. Customer closes the page without paying and goes back to Sylius
  4. The Sylius Payment remains in state new, but Payment.details still contains the order_mollie_id / payment_mollie_id from the first attempt
  5. Customer goes to "My Account" → order page → selects a payment method → submits
  6. A new Payum capture token is generated, CaptureAction is executed
  7. CaptureAction sees order_mollie_id in details → early returns without creating a new Mollie payment (lines 73-97)
  8. Payum proceeds to StatusAction → queries the old Mollie payment (status open) → markPending() → Payment transitions to processing
  9. The thank-you page is displayed, but no real payment happened
  10. The customer can no longer retry: ChangePaymentMethodType only shows payments in state new or cart — the processing payment is hidden

The order is now permanently stuck.

Root cause

In CaptureAction.php, when payment_mollie_id or order_mollie_id already exists in the payment details, the action returns immediately without checking whether the Mollie payment is still valid or whether the customer is retrying with a
new capture token.

// CaptureAction.php - simplified
if (isset($details['payment_mollie_id']) || isset($details['order_mollie_id'])) {
// checks for QR code / Apple Pay, then:
return; // ← Does nothing. No new Mollie payment is created.
}

This early return is correct for the normal flow (customer returning from Mollie after paying), but it does not distinguish between:

  • A return from Mollie (same capture token, same backurl) — should do nothing -> ok
  • A retry attempt (new capture token, stale Mollie IDs) — should handle the stale payment -> nok

Expected behavior

When a customer retries payment after abandoning the Mollie checkout page, CaptureAction should detect that the capture token has changed (i.e. this is a retry, not a return from Mollie) and either:

  • Check the real Mollie payment status via API
  • If the old payment is terminal (expired, canceled, failed): clear the stale IDs and create a new Mollie payment
  • If the old payment is still open: redirect to the existing checkout URL (to avoid double charges)

Workaround

We implemented an event listener on sylius.order.pre_update that intercepts the retry form submission before Payum runs. It queries the Mollie API for the real payment status, then:

  • If the old Mollie payment is terminal (expired, canceled, failed): fails the Sylius payment (triggering auto-creation of a new clean one) and copies the form-submitted details to it
  • If the old Mollie payment is still open with the same method: redirects to the existing Mollie checkout URL
  • If the customer changed payment method: fails the old Sylius payment and creates a new one

This works for the common cases, but we believe it does not fully prevent double charges. If the customer has the old Mollie checkout page still open in a browser tab and pays on it after a new Sylius payment has been created, Mollie
will accept both payments. The Sylius state machine naturally blocks the old payment from transitioning (failed → completed is not allowed), so Sylius won't reflect the duplicate — but the money is still collected by Mollie and requires
a manual refund.

We don't think this can be solved at the application level: the Mollie API only supports cancellation for bank transfers and SEPA direct debits. For most methods (PayPal, credit card, Klarna, iDEAL…), there is no way to programmatically
cancel an open payment.

Ideally, CaptureAction should handle the retry scenario natively to minimize the window for this edge case.

Environment

  • Sylius: 2.0
  • Mollie Plugin: v3.2.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions