Skip to content

Commit dd538d9

Browse files
committed
fix: resolve redirect mode and race condition issues for Bizum/PayPal
- Fix redirect mode not working in Blocks checkout for Bizum/PayPal - Backend now checks redirect_flow property to distinguish modes - Frontend checks paymentId existence before confirmPayment() - Fix race condition where order shows unpaid on arrival - Implement verify_and_complete_order() to fetch payment status from API - Complete order immediately if payment SUCCEEDED/AUTHORIZED - Use _monei_payment_id_processed meta for idempotency - Matches PrestaShop's approach - Add monei_token_exits() mock to PHPStan bootstrap - Remove redundant empty() check in save_payment_token()
1 parent 8d544ae commit dd538d9

File tree

5 files changed

+192
-70
lines changed

5 files changed

+192
-70
lines changed

assets/js/monei-block-checkout-bizum.js

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -246,41 +246,45 @@
246246
const unsubscribeSuccess = onCheckoutSuccess(
247247
( { processingResponse } ) => {
248248
const { paymentDetails } = processingResponse;
249-
if ( paymentDetails && paymentDetails.paymentId ) {
250-
const paymentId = paymentDetails.paymentId;
251-
const tokenValue = paymentDetails.token;
252-
monei
253-
.confirmPayment( {
254-
paymentId,
255-
paymentToken: tokenValue,
256-
} )
257-
.then( ( result ) => {
258-
if (
259-
result.nextAction &&
260-
result.nextAction.mustRedirect
261-
) {
262-
window.location.assign(
263-
result.nextAction.redirectUrl
264-
);
265-
}
266-
if ( result.status === 'FAILED' ) {
267-
window.location.href = `${ paymentDetails.failUrl }&status=FAILED`;
268-
} else {
269-
window.location.href =
270-
paymentDetails.completeUrl;
271-
}
272-
} )
273-
.catch( ( error ) => {
274-
console.error(
275-
'Error during payment confirmation:',
276-
error
277-
);
278-
window.location.href = paymentDetails.failUrl;
279-
} );
280-
} else {
281-
console.error( 'No paymentId found in paymentDetails' );
249+
250+
// In redirect mode, backend returns redirect URL and no paymentId
251+
// WooCommerce Blocks handles redirect automatically
252+
if ( ! paymentDetails?.paymentId ) {
253+
return false;
282254
}
283255

256+
// Component mode: confirm payment with token
257+
const paymentId = paymentDetails.paymentId;
258+
const tokenValue = paymentDetails.token;
259+
monei
260+
.confirmPayment( {
261+
paymentId,
262+
paymentToken: tokenValue,
263+
} )
264+
.then( ( result ) => {
265+
if (
266+
result.nextAction &&
267+
result.nextAction.mustRedirect
268+
) {
269+
window.location.assign(
270+
result.nextAction.redirectUrl
271+
);
272+
}
273+
if ( result.status === 'FAILED' ) {
274+
window.location.href = `${ paymentDetails.failUrl }&status=FAILED`;
275+
} else {
276+
window.location.href =
277+
paymentDetails.completeUrl;
278+
}
279+
} )
280+
.catch( ( error ) => {
281+
console.error(
282+
'Error during payment confirmation:',
283+
error
284+
);
285+
window.location.href = paymentDetails.failUrl;
286+
} );
287+
284288
// Return true to indicate that the checkout is successful
285289
return true;
286290
}

assets/js/monei-block-checkout-paypal.js

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -144,40 +144,45 @@
144144
const unsubscribeSuccess = onCheckoutSuccess(
145145
( { processingResponse } ) => {
146146
const { paymentDetails } = processingResponse;
147-
if ( paymentDetails && paymentDetails.paymentId ) {
148-
const paymentId = paymentDetails.paymentId;
149-
const tokenValue = paymentDetails.token;
150-
monei
151-
.confirmPayment( {
152-
paymentId,
153-
paymentToken: tokenValue,
154-
} )
155-
.then( ( result ) => {
156-
if (
157-
result.nextAction &&
158-
result.nextAction.mustRedirect
159-
) {
160-
window.location.assign(
161-
result.nextAction.redirectUrl
162-
);
163-
}
164-
if ( result.status === 'FAILED' ) {
165-
window.location.href = `${ paymentDetails.failUrl }&status=FAILED`;
166-
} else {
167-
window.location.href =
168-
paymentDetails.completeUrl;
169-
}
170-
} )
171-
.catch( ( error ) => {
172-
console.error(
173-
'Error during payment confirmation:',
174-
error
175-
);
176-
window.location.href = paymentDetails.failUrl;
177-
} );
178-
} else {
179-
console.error( 'No paymentId found in paymentDetails' );
147+
148+
// In redirect mode, backend returns redirect URL and no paymentId
149+
// WooCommerce Blocks handles redirect automatically
150+
if ( ! paymentDetails?.paymentId ) {
151+
return false;
180152
}
153+
154+
// Component mode: confirm payment with token
155+
const paymentId = paymentDetails.paymentId;
156+
const tokenValue = paymentDetails.token;
157+
monei
158+
.confirmPayment( {
159+
paymentId,
160+
paymentToken: tokenValue,
161+
} )
162+
.then( ( result ) => {
163+
if (
164+
result.nextAction &&
165+
result.nextAction.mustRedirect
166+
) {
167+
window.location.assign(
168+
result.nextAction.redirectUrl
169+
);
170+
}
171+
if ( result.status === 'FAILED' ) {
172+
window.location.href = `${ paymentDetails.failUrl }&status=FAILED`;
173+
} else {
174+
window.location.href =
175+
paymentDetails.completeUrl;
176+
}
177+
} )
178+
.catch( ( error ) => {
179+
console.error(
180+
'Error during payment confirmation:',
181+
error
182+
);
183+
window.location.href = paymentDetails.failUrl;
184+
} );
185+
181186
// Return true to indicate that the checkout is successful
182187
return true;
183188
}

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

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ public function add_notice_monei_order_cancelled( $order_id ) {
9191
* We trigger this same behaviour in order received page. After a successful payment in MONEI we are redirected
9292
* to order_received_page. If there is a token available, we need to save it.
9393
* We don't do this at IPN level, since right now, token doesn't come thru.
94+
*
95+
* Also, we verify the payment status from the API to complete the order if the IPN hasn't processed yet (race condition).
96+
*
9497
* todo: refactor and split code for is_add_payment_method_page and is_order_received_page to make it more readable.
9598
*/
9699
public function save_payment_token() {
@@ -125,9 +128,15 @@ public function save_payment_token() {
125128
$payment = $this->moneiPaymentServices->get_payment( $payment_id );
126129
$payment_token = $payment->getPaymentToken();
127130

131+
// Verify payment status and complete order if needed (race condition fix)
132+
// If user arrives before IPN webhook processes, we complete the order here
133+
if ( $order_id && is_order_received_page() ) {
134+
$this->verify_and_complete_order( $order_id, $payment );
135+
}
136+
128137
// A payment can come without token, user didn't check on save payment method.
129138
// We just ignore it then and do nothing.
130-
if ( ! $payment_token || empty( $payment_token ) ) {
139+
if ( ! $payment_token ) {
131140
return;
132141
}
133142

@@ -168,6 +177,91 @@ public function save_payment_token() {
168177
WC_Monei_Logger::log( $e->getMessage(), 'error' );
169178
}
170179
}
180+
181+
/**
182+
* Verify payment status and complete order if needed (race condition fix).
183+
* When user returns from MONEI before IPN webhook processes, we need to complete the order here.
184+
*
185+
* @param int $order_id Order ID.
186+
* @param object $payment MONEI payment object.
187+
* @return void
188+
*/
189+
private function verify_and_complete_order( $order_id, $payment ) {
190+
$order = wc_get_order( $order_id );
191+
if ( ! $order ) {
192+
return;
193+
}
194+
195+
// Check if payment was already processed (prevent duplicate processing)
196+
$processed_payment_id = $order->get_meta( '_monei_payment_id_processed', true );
197+
if ( $processed_payment_id === $payment->getId() ) {
198+
WC_Monei_Logger::log( sprintf( '[MONEI] Payment already processed via IPN [payment_id=%s, order_id=%s]', $payment->getId(), $order_id ), 'debug' );
199+
return;
200+
}
201+
202+
$payment_status = $payment->getStatus();
203+
$order_status = $order->get_status();
204+
205+
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' );
206+
207+
// Only process if order is still pending/on-hold and payment succeeded
208+
if ( ! in_array( $order_status, array( 'pending', 'on-hold' ), true ) ) {
209+
WC_Monei_Logger::log( sprintf( '[MONEI] Order already processed, skipping [order_id=%s, status=%s]', $order_id, $order_status ), 'debug' );
210+
return;
211+
}
212+
213+
// If payment is SUCCEEDED or AUTHORIZED, complete the order
214+
if ( 'SUCCEEDED' === $payment_status || 'AUTHORIZED' === $payment_status ) {
215+
$amount = $payment->getAmount();
216+
$order_total = $order->get_total();
217+
218+
// Verify amounts match (with 1 cent exception for subscriptions)
219+
if ( ( (int) $amount !== monei_price_format( $order_total ) ) && ( 1 !== $amount ) ) {
220+
$order->update_status(
221+
'on-hold',
222+
sprintf(
223+
/* translators: 1: Order amount, 2: Payment amount */
224+
__( 'Validation error: Order vs. Payment amounts do not match (order: %1$s - received: %2$s).', 'monei' ),
225+
monei_price_format( $order_total ),
226+
$amount
227+
)
228+
);
229+
WC_Monei_Logger::log( sprintf( '[MONEI] Amount mismatch [order_id=%s, order_amount=%s, payment_amount=%s]', $order_id, monei_price_format( $order_total ), $amount ), 'error' );
230+
return;
231+
}
232+
233+
// Mark payment as processed to prevent duplicate processing by IPN
234+
$order->update_meta_data( '_monei_payment_id_processed', $payment->getId() );
235+
$order->update_meta_data( '_payment_order_number_monei', $payment->getId() );
236+
$order->update_meta_data( '_payment_order_status_monei', $payment_status );
237+
$order->update_meta_data( '_payment_order_status_code_monei', $payment->getStatusCode() );
238+
$order->update_meta_data( '_payment_order_status_message_monei', $payment->getStatusMessage() );
239+
240+
if ( 'AUTHORIZED' === $payment_status ) {
241+
$order->update_meta_data( '_payment_not_captured_monei', 1 );
242+
$order_note = __( 'Payment verified via redirect - <strong>Payment Authorized</strong>', 'monei' ) . '. <br><br>';
243+
$order_note .= __( 'MONEI Transaction id: ', 'monei' ) . $payment->getId() . '. <br><br>';
244+
$order_note .= __( 'MONEI Status Message: ', 'monei' ) . $payment->getStatusMessage();
245+
$order->add_order_note( $order_note );
246+
$order->update_status( 'on-hold', __( 'Order On-Hold by MONEI', 'monei' ) );
247+
} else {
248+
// SUCCEEDED
249+
$order_note = __( 'Payment verified via redirect - <strong>Payment Completed</strong>', 'monei' ) . '. <br><br>';
250+
$order_note .= __( 'MONEI Transaction id: ', 'monei' ) . $payment->getId() . '. <br><br>';
251+
$order_note .= __( 'MONEI Status Message: ', 'monei' ) . $payment->getStatusMessage();
252+
$order->add_order_note( $order_note );
253+
$order->payment_complete();
254+
255+
$payment_method_woo_id = $order->get_payment_method();
256+
if ( 'completed' === monei_get_settings( 'orderdo', $payment_method_woo_id ) ) {
257+
$order->update_status( 'completed', __( 'Order Completed by MONEI', 'monei' ) );
258+
}
259+
}
260+
261+
$order->save();
262+
WC_Monei_Logger::log( sprintf( '[MONEI] Order completed via redirect verification [order_id=%s, payment_status=%s]', $order_id, $payment_status ), 'debug' );
263+
}
264+
}
171265
}
172266

173267
new WC_Monei_Redirect_Hooks();

src/Gateways/Abstracts/WCMoneiPaymentGatewayHosted.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,25 @@ public function process_payment( $order_id, $allowed_payment_method = null ) {
137137
$this->log( $payment, 'debug' );
138138
do_action( 'wc_gateway_monei_process_payment_success', $payload, $payment, $order );
139139

140-
if ( $this->isBlockCheckout() ) {
140+
// Block checkout with component mode (Bizum/PayPal button)
141+
// Return paymentId for frontend confirmation
142+
$redirect_flow = property_exists( $this, 'redirect_flow' ) ? $this->redirect_flow : true;
143+
$has_token = $this->get_frontend_generated_token();
144+
$is_block = $this->isBlockCheckout();
145+
146+
if ( $is_block && ! $redirect_flow && $has_token ) {
141147
return array(
142148
'result' => 'success',
143149
'redirect' => false,
144-
'paymentId' => $payment->getId(), // Send the paymentId back to the client
145-
'token' => $this->get_frontend_generated_token(), // Send the token back to the client
150+
'paymentId' => $payment->getId(),
151+
'token' => $has_token,
146152
'completeUrl' => $payload['completeUrl'],
147153
'failUrl' => $payload['failUrl'],
148154
'orderId' => $order_id,
149155
);
150156
}
157+
// Classic checkout or Block checkout in redirect mode
158+
// Return redirect URL to MONEI hosted page
151159
return array(
152160
'result' => 'success',
153161
'redirect' => $payment->getNextAction()->getRedirectUrl(),

tests/phpstan-bootstrap.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,14 @@ public static function get_ip_address() {
198198
}
199199
}
200200
}
201+
202+
if ( ! function_exists( 'monei_token_exits' ) ) {
203+
/**
204+
* @param string $monei_token
205+
* @param string $gateway_id
206+
* @return bool
207+
*/
208+
function monei_token_exits( $monei_token, $gateway_id ) {
209+
return false;
210+
}
211+
}

0 commit comments

Comments
 (0)