Skip to content

Commit 4f2adce

Browse files
committed
fix: allow payment retry recovery for failed orders in classic checkout
- Add 'failed' status to allowed order statuses in verify_and_complete_order() - Enables order completion when users retry after initial payment failure - Fixes race condition where orders show unpaid after successful retry - Add null safety check for getNextAction() to prevent fatal errors - Add orderId to completeUrl for consistent order identification - Use PaymentStatus enum constants for type safety This fixes classic checkout payment retry flow where: 1. First payment attempt fails (intentional 3DS failure) 2. Order marked as "failed" 3. User retries from order-pay page 4. Payment succeeds but order stays "failed" The fix securely verifies payment via MONEI API with idempotency checks and amount validation before completing the order.
1 parent 134f866 commit 4f2adce

File tree

2 files changed

+46
-7
lines changed

2 files changed

+46
-7
lines changed

includes/class-wc-monei-redirect-hooks.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Monei\Core\ContainerProvider;
4+
use Monei\Model\PaymentStatus;
45
use Monei\Services\ApiKeyService;
56
use Monei\Services\payment\MoneiPaymentServices;
67
use Monei\Services\PaymentMethodFormatter;
@@ -28,8 +29,8 @@ class WC_Monei_Redirect_Hooks {
2829
*/
2930
public function __construct() {
3031
add_action( 'woocommerce_cancelled_order', array( $this, 'add_notice_monei_order_cancelled' ) );
31-
add_action( 'template_redirect', array( $this, 'add_notice_monei_order_failed' ) );
3232
add_action( 'wp', array( $this, 'save_payment_token' ) );
33+
add_action( 'template_redirect', array( $this, 'add_notice_monei_order_failed' ), 10 );
3334
//TODO use the container
3435
$apiKeyService = new ApiKeyService();
3536
$sdkClient = new MoneiSdkClientFactory( $apiKeyService );
@@ -206,19 +207,20 @@ private function verify_and_complete_order( $order_id, $payment ) {
206207
return;
207208
}
208209

210+
/** @var string $payment_status */
209211
$payment_status = $payment->getStatus();
210212
$order_status = $order->get_status();
211213

212214
WC_Monei_Logger::log( sprintf( '[MONEI] Redirect verification [payment_id=%s, order_id=%s, payment_status=%s, order_status=%s]', $payment->getId(), $order_id, $payment_status, $order_status ), 'debug' );
213215

214-
// Only process if order is still pending/on-hold and payment succeeded
215-
if ( ! in_array( $order_status, array( 'pending', 'on-hold' ), true ) ) {
216+
// Only process if order is still pending/on-hold/failed and payment succeeded
217+
if ( ! in_array( $order_status, array( 'pending', 'on-hold', 'failed' ), true ) ) {
216218
WC_Monei_Logger::log( sprintf( '[MONEI] Order already processed, skipping [order_id=%s, status=%s]', $order_id, $order_status ), 'debug' );
217219
return;
218220
}
219221

220222
// If payment is SUCCEEDED or AUTHORIZED, complete the order
221-
if ( 'SUCCEEDED' === $payment_status || 'AUTHORIZED' === $payment_status ) {
223+
if ( PaymentStatus::SUCCEEDED === $payment_status || PaymentStatus::AUTHORIZED === $payment_status ) {
222224
$amount = $payment->getAmount();
223225
$order_total = $order->get_total();
224226

@@ -250,7 +252,7 @@ private function verify_and_complete_order( $order_id, $payment ) {
250252
$order->update_meta_data( '_monei_payment_method_display', $payment_method_display );
251253
}
252254

253-
if ( 'AUTHORIZED' === $payment_status ) {
255+
if ( PaymentStatus::AUTHORIZED === $payment_status ) {
254256
$order->update_meta_data( '_payment_not_captured_monei', 1 );
255257
$order_note = __( 'Payment verified via redirect - <strong>Payment Authorized</strong>', 'monei' ) . '. <br><br>';
256258
$order_note .= __( 'MONEI Transaction id: ', 'monei' ) . $payment->getId() . '. <br><br>';

src/Gateways/Abstracts/WCMoneiPaymentGatewayComponent.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Exception;
66
use Monei\ApiException;
7+
use Monei\Model\PaymentStatus;
78
use WC_Geolocation;
89
use MoneiPaymentServices;
910
use WC_Order;
@@ -91,7 +92,33 @@ public function process_payment( $order_id, $allowed_payment_method = null ) {
9192
/**
9293
* Depends if we came in 1 step or 2.
9394
*/
94-
$next_action_redirect = ( $confirm_payment ) ? $confirm_payment->getNextAction()->getRedirectUrl() : $create_payment->getNextAction()->getRedirectUrl();
95+
$payment_result = $confirm_payment ?: $create_payment;
96+
// Get redirect URL from nextAction, or fall back to order received page
97+
$next_action = $payment_result->getNextAction();
98+
if ( $next_action && $next_action->getRedirectUrl() ) {
99+
$next_action_redirect = $next_action->getRedirectUrl();
100+
} else {
101+
// If no redirect URL from MONEI (e.g., immediately successful payment with saved card),
102+
// redirect to order received page
103+
$next_action_redirect = $this->get_return_url( $order );
104+
}
105+
106+
// Add payment ID and status to redirect URL for order verification (similar to blocks checkout)
107+
// This ensures order is marked as paid even if IPN hasn't arrived yet (race condition fix)
108+
/** @var string $payment_status */
109+
$payment_status = $payment_result->getStatus();
110+
if ( PaymentStatus::SUCCEEDED === $payment_status || PaymentStatus::AUTHORIZED === $payment_status || PaymentStatus::PENDING === $payment_status ) {
111+
$redirect_url = add_query_arg(
112+
array(
113+
'id' => $payment_result->getId(),
114+
'orderId' => $order_id,
115+
'status' => $payment_status,
116+
),
117+
$next_action_redirect
118+
);
119+
$next_action_redirect = $redirect_url;
120+
}
121+
95122
return array(
96123
'result' => 'success',
97124
'redirect' => $next_action_redirect,
@@ -151,7 +178,17 @@ public function create_payload( $order, $allowed_payment_method = null ) {
151178
/**
152179
* The URL the customer will be directed to after transaction completed (successful or failed).
153180
*/
154-
$complete_url = wp_sanitize_redirect( esc_url_raw( add_query_arg( 'utm_nooverride', '1', $this->get_return_url( $order ) ) ) );
181+
$complete_url = wp_sanitize_redirect(
182+
esc_url_raw(
183+
add_query_arg(
184+
array(
185+
'utm_nooverride' => '1',
186+
'orderId' => $order_id,
187+
),
188+
$this->get_return_url( $order )
189+
)
190+
)
191+
);
155192

156193
/**
157194
* Create Payment Payload

0 commit comments

Comments
 (0)