Fix the PrestaShop Stripe Double-Charge Bug
A real double charge is a payment incident, not an SEO topic. This guide starts from the payment path: Stripe event IDs, PrestaShop carts, order_payment rows, webhook delivery, redirect confirmation, and the exact guardrails a safe PR should add.
Short answer
If a PrestaShop Stripe customer reports a double charge, do not start by guessing. First decide whether you have two real Stripe charges, one Stripe charge with duplicate PrestaShop records, or duplicate-looking order references caused by PrestaShop order splitting or race conditions.
- Compare Stripe payment IDs, PrestaShop order IDs, cart IDs, customer IDs, timestamps, and
order_paymentrows. - Check whether both the customer redirect path and the Stripe webhook path can create or validate the same order.
- Add idempotency or a local payment lock before creating Stripe objects or calling
validateOrder(). - Because this touches payments, treat the fix as a PR that gets human review before merge.
Research notes before the fix
| Source | What matters for this bug |
|---|---|
| PrestaShop payment module docs | External payment modules can receive both a customer return and a server-to-server notification. The docs also emphasize checking id_cart, amount source, and callback signature before order validation. |
| Stripe idempotent requests | Stripe supports idempotency keys on POST requests so a retry does not accidentally create a second object or operation. |
| Stripe PrestaShop configuration | The Stripe PrestaShop module exposes payment-flow, capture, logging, and refund settings that change how the order and charge lifecycle behaves. |
| PrestaShop duplicate reference issue | Duplicate-looking orders are not always duplicate payments. One long-running PrestaShop issue documents several different causes, including legitimate split orders and race-condition style behavior. |
Step 1: classify the incident
Use the payment provider as the source of truth for money movement, and PrestaShop as the source of truth for order state. The dangerous mistake is refunding or patching before you know which system duplicated what.
| Pattern | Likely meaning | First action |
|---|---|---|
| Two Stripe payment IDs for one intended cart | Real duplicate charge or duplicate checkout session. | Refund the duplicate after matching amount, customer, and timestamps. Then add idempotency or submit-lock protection. |
| One Stripe payment ID, two PrestaShop order rows | Order creation or confirmation path duplicated the local record. | Inspect module confirmation code, webhook logs, and order_payment. |
| Same PrestaShop reference across multiple rows | May be split order, address/shipping edge case, invoice race, or a real bug. | Check cart contents, carriers, addresses, warehouse settings, and invoice number behavior. |
| Pending order in PrestaShop, paid in Stripe | Webhook or return handling failed after the charge. | Enable module logging, inspect Stripe webhook delivery, and reconcile the order status. |
Step 2: inspect the duplicate path
The common integration bug is not "Stripe randomly charged twice." It is usually one of these local paths:
Double submit
The customer clicks Place Order twice, the browser retries, or a slow checkout leaves the button active. Fix: disable the submit button after the first attempt and re-enable only on a real payment error.
Webhook plus redirect
The redirect controller and webhook handler both call order confirmation logic. Fix: one path creates or validates the order; the other only reconciles status after checking the existing payment lock.
Missing idempotency
A retry creates a second Stripe object. Fix: use an idempotency key derived from the cart and intended payment attempt when the module creates a PaymentIntent or Checkout Session.
Duplicate local display
PrestaShop may show duplicate-looking orders or payments even when only one charge exists. Fix: verify order_payment, orders, and Stripe IDs before touching money.
Step 3: add a safe guard in the module
The exact module path depends on your Stripe connector, but the guard belongs before order validation. The shape is simple: for a cart/payment pair, one process wins; every later callback sees the existing record and exits safely.
// Pseudocode for a custom PrestaShop payment guard.
$cartId = (int) $cart->id;
$paymentIntent = $stripePayload['payment_intent'] ?? null;
if (!$paymentIntent) {
throw new PaymentException('Missing Stripe payment intent');
}
if ($this->paymentAttemptExists($cartId, $paymentIntent)) {
$this->logDuplicateCallback($cartId, $paymentIntent, $stripeEventId);
return $this->redirectToExistingOrder($cartId, $paymentIntent);
}
$this->createPaymentAttemptLock($cartId, $paymentIntent, $stripeEventId);
$this->validateOrder($cartId, $paidAmountFromStripe, $paymentIntent);
Step 4: what SimpleReview should open as a PR
A good PR for this issue is small and auditable. It should not rewrite checkout. It should add observability and prevent the duplicate path.
- Add a submit-state guard in the checkout template if the button can be clicked twice.
- Add a server-side guard keyed by
id_cartplus Stripe payment intent or checkout session. - Log Stripe event IDs so webhook retries are visible and searchable.
- Verify callback signatures or tokens before trusting payment status.
- Use Stripe idempotency keys where the module creates Stripe objects through POST requests.
- Add a rollback note: how to disable the guard or revert the module override.
How to test before production
- Run the checkout on staging with Stripe test mode and module extended logging enabled.
- Double-click Place Order, refresh after submit, and use browser back to retry the payment step.
- Replay the same webhook event or trigger a retry from Stripe test tooling if available.
- Confirm there is one order for one intended payment attempt, or one existing order redirect on duplicate callback.
- Confirm refunds still work from PrestaShop and Stripe dashboards.
- Test mobile checkout, cached checkout page behavior, and guest checkout.
FAQ
Is the Stripe module always responsible?
No. The issue can be theme JavaScript, a custom checkout module, a third-party payment module, webhook retries, multishop configuration, or a PrestaShop order-display edge case. Start from payment IDs and logs.
Should the fix live in an override?
Prefer the smallest maintainable path. If the bug is in custom code, patch custom code. If it is in a vendor module, avoid editing licensed source directly unless you own the maintenance path. A wrapper, hook, or custom module may be safer.
Can this be solved only with JavaScript?
No. Disabling the button helps user double-clicks, but webhooks, retries, and server races still need a server-side guard.
Make the payment fix reviewable
Let SimpleReview draft the narrow diff, then use Vibers human review before merging payment code. This is the right split: AI for file discovery and PR scaffolding, human review for money movement.
Sources
More PrestaShop resources: PrestaShop hub · PrestaShop fix guide · SimpleReview