Skip to content

Commit 32147f8

Browse files
committed
Improve checks in non-deferred-intent flow
1 parent 67ead54 commit 32147f8

File tree

4 files changed

+169
-14
lines changed

4 files changed

+169
-14
lines changed

includes/class-wc-stripe-helper.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1896,4 +1896,91 @@ public static function has_gateway_plugin_active( $plugin_id ) {
18961896
}
18971897
return false;
18981898
}
1899+
1900+
/**
1901+
* Checks if the given payment intent is valid for the order.
1902+
* This checks the currency, amount, and payment method types.
1903+
* The function will log a critical error if there is a mismatch.
1904+
*
1905+
* @param WC_Order $order The order to check.
1906+
* @param object|string $intent The payment intent to check, can either be an object or an intent ID.
1907+
* @param string|null $selected_payment_type The selected payment type, which is generally applicable for updates. If null, we will use the stored payment type for the order.
1908+
*
1909+
* @throws Exception Throws an exception if the intent is not valid for the order.
1910+
*/
1911+
public static function validate_intent_for_order( $order, $intent, ?string $selected_payment_type = null ): void {
1912+
$intent_id = null;
1913+
if ( is_string( $intent ) ) {
1914+
$intent_id = $intent;
1915+
$is_setup_intent = substr( $intent_id, 0, 4 ) === 'seti';
1916+
if ( $is_setup_intent ) {
1917+
$intent = WC_Stripe_API::retrieve( 'setup_intents/' . $intent_id . '?expand[]=payment_method' );
1918+
} else {
1919+
$intent = WC_Stripe_API::retrieve( 'payment_intents/' . $intent_id . '?expand[]=payment_method' );
1920+
}
1921+
}
1922+
1923+
if ( ! is_object( $intent ) ) {
1924+
throw new Exception( __( "We're not able to process this request. Please try again later.", 'woocommerce-gateway-stripe' ) );
1925+
}
1926+
1927+
if ( null === $intent_id ) {
1928+
$intent_id = $intent->id ?? null;
1929+
}
1930+
1931+
// Make sure we actually fetched the intent.
1932+
if ( ! empty( $intent->error ) ) {
1933+
WC_Stripe_Logger::error(
1934+
'Error: failed to fetch requested Stripe intent',
1935+
[
1936+
'intent_id' => $intent_id,
1937+
'error' => $intent->error,
1938+
]
1939+
);
1940+
throw new Exception( __( "We're not able to process this request. Please try again later.", 'woocommerce-gateway-stripe' ) );
1941+
}
1942+
1943+
if ( null === $selected_payment_type ) {
1944+
$selected_payment_type = $order->get_meta( '_stripe_upe_payment_type', true );
1945+
}
1946+
1947+
// If we don't have a selected payment type, that implies we have no stored value and a new payment type is permitted.
1948+
$is_valid_payment_type = empty( $selected_payment_type ) || ( ! empty( $intent->payment_method_types ) && in_array( $selected_payment_type, $intent->payment_method_types, true ) );
1949+
$order_currency = strtolower( $order->get_currency() );
1950+
$order_amount = WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $order->get_currency() );
1951+
$order_intent_id = self::get_intent_id_from_order( $order );
1952+
1953+
if ( 'payment_intent' === $intent->object ) {
1954+
$is_valid = $order_currency === $intent->currency
1955+
&& $is_valid_payment_type
1956+
&& $order_amount === $intent->amount
1957+
&& ( ! $order_intent_id || $order_intent_id === $intent->id );
1958+
} else {
1959+
// Setup intents don't have an amount or currency.
1960+
$is_valid = $is_valid_payment_type
1961+
&& ( ! $order_intent_id || $order_intent_id === $intent->id );
1962+
}
1963+
1964+
// Return early if we have a valid intent.
1965+
if ( $is_valid ) {
1966+
return;
1967+
}
1968+
1969+
$permitted_payment_types = implode( '/', $intent->payment_method_types );
1970+
WC_Stripe_Logger::critical(
1971+
"Error: Invalid payment intent for order. Intent: {$intent->currency} {$intent->amount} via {$permitted_payment_types}, Order: {$order_currency} {$order_amount} {$selected_payment_type}",
1972+
[
1973+
'order_id' => $order->get_id(),
1974+
'intent_id' => $intent->id,
1975+
'intent_currency' => $intent->currency,
1976+
'intent_amount' => $intent->amount,
1977+
'intent_payment_method_types' => $intent->payment_method_types,
1978+
'selected_payment_type' => $selected_payment_type,
1979+
'order_currency' => $order->get_currency(),
1980+
'order_total' => $order->get_total(),
1981+
]
1982+
);
1983+
1984+
throw new Exception( __( "We're not able to process this request. Please try again later.", 'woocommerce-gateway-stripe' ) );
1985+
}
18991986
}

includes/class-wc-stripe-intent-controller.php

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,20 @@ public function update_payment_intent_ajax() {
414414
throw new Exception( __( 'Unable to verify your request. Please reload the page and try again.', 'woocommerce-gateway-stripe' ) );
415415
}
416416

417-
wp_send_json_success( $this->update_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_upe_payment_type ), 200 );
417+
$update_intent_result = $this->update_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_upe_payment_type );
418+
419+
if ( ! ( $update_intent_result['success'] ?? false ) ) {
420+
$error_message = $update_intent_result['error'] ?? __( "We're not able to process this request. Please try again later.", 'woocommerce-gateway-stripe' );
421+
wp_send_json_error(
422+
[
423+
'error' => [
424+
'message' => $error_message,
425+
],
426+
]
427+
);
428+
} else {
429+
wp_send_json_success( $update_intent_result, 200 );
430+
}
418431
} catch ( Exception $e ) {
419432
// Send back error so it can be displayed to the customer.
420433
wp_send_json_error(
@@ -433,10 +446,10 @@ public function update_payment_intent_ajax() {
433446
* @since 5.6.0
434447
* @version 9.4.0
435448
*
436-
* @param {string} $intent_id The id of the payment intent or setup intent to update.
437-
* @param {int} $order_id The id of the order if intent created from Order.
438-
* @param {boolean} $save_payment_method True if saving the payment method.
439-
* @param {string} $selected_upe_payment_type The name of the selected UPE payment type or empty string.
449+
* @param string $intent_id The id of the payment intent or setup intent to update.
450+
* @param int $order_id The id of the order if intent created from Order.
451+
* @param boolean $save_payment_method True if saving the payment method.
452+
* @param string $selected_upe_payment_type The name of the selected UPE payment type or empty string.
440453
*
441454
* @throws Exception If the update intent call returns with an error.
442455
* @return array|null An array with result of the update, or nothing
@@ -445,9 +458,15 @@ public function update_intent( $intent_id = '', $order_id = null, $save_payment_
445458
$order = wc_get_order( $order_id );
446459

447460
if ( ! is_a( $order, 'WC_Order' ) ) {
448-
return;
461+
return [
462+
'success' => false,
463+
'error' => __( 'Unable to find a matching order.', 'woocommerce-gateway-stripe' ),
464+
];
449465
}
450466

467+
$selected_payment_type = '' !== $selected_upe_payment_type && is_string( $selected_upe_payment_type ) ? $selected_upe_payment_type : null;
468+
WC_Stripe_Helper::validate_intent_for_order( $order, $intent_id, $selected_payment_type );
469+
451470
$gateway = $this->get_upe_gateway();
452471
$amount = $order->get_total();
453472
$currency = $order->get_currency();
@@ -497,13 +516,42 @@ public function update_intent( $intent_id = '', $order_id = null, $save_payment_
497516

498517
// Use "setup_intents" endpoint if `$intent_id` starts with `seti_`.
499518
$endpoint = $is_setup_intent ? 'setup_intents' : 'payment_intents';
500-
WC_Stripe_API::request_with_level3_data(
519+
$result = WC_Stripe_API::request_with_level3_data(
501520
$request,
502521
"{$endpoint}/{$intent_id}",
503522
$level3_data,
504523
$order
505524
);
506525

526+
if ( ! empty( $result->error ) ) {
527+
if ( 'payment_intent_unexpected_state' === $result->error->code ) {
528+
WC_Stripe_Logger::critical(
529+
'Error: Failed to update intent due to invalid operation',
530+
[
531+
'intent_id' => $intent_id,
532+
'order_id' => $order_id,
533+
'error' => $result->error,
534+
]
535+
);
536+
537+
throw new Exception( __( "We're not able to process this request. Please try again later.", 'woocommerce-gateway-stripe' ) );
538+
}
539+
540+
WC_Stripe_Logger::error(
541+
'Error: Failed to update Stripe intent',
542+
[
543+
'intent_id' => $intent_id,
544+
'order_id' => $order_id,
545+
'error' => $result->error,
546+
]
547+
);
548+
549+
return [
550+
'success' => false,
551+
'error' => $result->error->message,
552+
];
553+
}
554+
507555
// Prevent any failures if updating the status of a subscription order.
508556
if ( ! $gateway->has_subscription( $order_id ) ) {
509557
$order->update_status( OrderStatus::PENDING, __( 'Awaiting payment.', 'woocommerce-gateway-stripe' ) );

includes/payment-methods/class-wc-stripe-upe-payment-gateway.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,11 @@ public function process_payment( $order_id, $retry = true, $force_save_source =
835835
if ( $payment_intent_id && ! $this->payment_methods[ $selected_payment_type ]->supports_deferred_intent() ) {
836836
// Adds customer and metadata to PaymentIntent.
837837
// These parameters cannot be added upon updating the intent via the `/confirm` API.
838-
$this->intent_controller->update_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_payment_type );
838+
try {
839+
$this->intent_controller->update_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_payment_type );
840+
} catch ( Exception $update_intent_exception ) {
841+
throw new Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
842+
}
839843
}
840844

841845
// Flag for using a deferred intent. To be removed.
@@ -1658,6 +1662,13 @@ public function process_order_for_confirmed_intent( $order, $intent_id, $save_pa
16581662
throw new WC_Stripe_Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
16591663
}
16601664

1665+
// Validates the intent can be applied to the order.
1666+
try {
1667+
WC_Stripe_Helper::validate_intent_for_order( $order, $intent );
1668+
} catch ( Exception $e ) {
1669+
throw new Exception( __( "We're not able to process this payment. Please try again later.", 'woocommerce-gateway-stripe' ) );
1670+
}
1671+
16611672
list( $payment_method_type, $payment_method_details ) = $this->get_payment_method_data_from_intent( $intent );
16621673

16631674
if ( ! isset( $this->payment_methods[ $payment_method_type ] ) ) {

tests/phpunit/PaymentMethods/WC_Stripe_UPE_Payment_Gateway_Test.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ class WC_Stripe_UPE_Payment_Gateway_Test extends WC_Mock_Stripe_API_Unit_Test_Ca
125125
],
126126
],
127127
],
128+
'payment_method_types' => [
129+
WC_Stripe_Payment_Methods::CARD,
130+
WC_Stripe_Payment_Methods::LINK,
131+
],
128132
];
129133

130134
/**
@@ -299,7 +303,7 @@ private function get_order_details( $order ) {
299303
'signature' => sprintf( '%d:%s', $order->get_id(), md5( implode( '-', [ absint( $order->get_id() ), $order->get_order_key(), $order->get_customer_id(), $amount ] ) ) ),
300304
'tax_amount' => WC_Stripe_Helper::get_stripe_amount( $total_tax, strtolower( $currency ) ),
301305
];
302-
return [ $amount, $description, $metadata ];
306+
return [ $amount, $description, $metadata, strtolower( $currency ) ];
303307
}
304308

305309
/**
@@ -931,7 +935,7 @@ public function test_process_redirect_payment_returns_valid_response() {
931935
$order = WC_Helper_Order::create_order();
932936
$order_id = $order->get_id();
933937

934-
list( $amount, $description, $metadata ) = $this->get_order_details( $order );
938+
list( $amount, $description, $metadata, $currency ) = $this->get_order_details( $order );
935939
$order->set_payment_method( WC_Stripe_UPE_Payment_Gateway::ID );
936940
$order->save();
937941

@@ -943,6 +947,7 @@ public function test_process_redirect_payment_returns_valid_response() {
943947
$payment_intent_mock = self::MOCK_CARD_PAYMENT_INTENT_TEMPLATE;
944948
$payment_intent_mock['id'] = $payment_intent_id;
945949
$payment_intent_mock['amount'] = $amount;
950+
$payment_intent_mock['currency'] = $currency;
946951
$payment_intent_mock['last_payment_error'] = [];
947952
$payment_intent_mock['payment_method'] = $payment_method_mock;
948953
$payment_intent_mock['latest_charge'] = 'ch_mock';
@@ -999,7 +1004,7 @@ public function test_process_redirect_payment_only_runs_once() {
9991004
$order = WC_Helper_Order::create_order();
10001005
$order_id = $order->get_id();
10011006

1002-
list( $amount, $description, $metadata ) = $this->get_order_details( $order );
1007+
list( $amount, $description, $metadata, $currency ) = $this->get_order_details( $order );
10031008
$order->set_payment_method( WC_Stripe_UPE_Payment_Gateway::ID );
10041009
$order->save();
10051010

@@ -1011,6 +1016,7 @@ public function test_process_redirect_payment_only_runs_once() {
10111016
$payment_intent_mock = self::MOCK_CARD_PAYMENT_INTENT_TEMPLATE;
10121017
$payment_intent_mock['id'] = $payment_intent_id;
10131018
$payment_intent_mock['amount'] = $amount;
1019+
$payment_intent_mock['currency'] = $currency;
10141020
$payment_intent_mock['last_payment_error'] = [];
10151021
$payment_intent_mock['payment_method'] = $payment_method_mock;
10161022
$payment_intent_mock['latest_charge'] = 'ch_mock';
@@ -1166,7 +1172,7 @@ public function test_checkout_saves_payment_method_to_order() {
11661172
$order = WC_Helper_Order::create_order();
11671173
$order_id = $order->get_id();
11681174

1169-
list( $amount, $description, $metadata ) = $this->get_order_details( $order );
1175+
list( $amount, $description, $metadata, $currency ) = $this->get_order_details( $order );
11701176
$order->set_payment_method( WC_Stripe_UPE_Payment_Gateway::ID );
11711177
$order->save();
11721178

@@ -1178,6 +1184,7 @@ public function test_checkout_saves_payment_method_to_order() {
11781184
$payment_intent_mock = self::MOCK_CARD_PAYMENT_INTENT_TEMPLATE;
11791185
$payment_intent_mock['id'] = $payment_intent_id;
11801186
$payment_intent_mock['amount'] = $amount;
1187+
$payment_intent_mock['currency'] = $currency;
11811188
$payment_intent_mock['last_payment_error'] = [];
11821189
$payment_intent_mock['payment_method'] = $payment_method_mock;
11831190
$payment_intent_mock['latest_charge'] = 'ch_mock';
@@ -1229,7 +1236,7 @@ public function test_checkout_saves_sepa_generated_payment_method_to_order() {
12291236
$order = WC_Helper_Order::create_order();
12301237
$order_id = $order->get_id();
12311238

1232-
list( $amount, $description, $metadata ) = $this->get_order_details( $order );
1239+
list( $amount, $description, $metadata, $currency ) = $this->get_order_details( $order );
12331240
$order->set_payment_method( WC_Stripe_UPE_Payment_Gateway::ID );
12341241
$order->save();
12351242

@@ -1243,6 +1250,7 @@ public function test_checkout_saves_sepa_generated_payment_method_to_order() {
12431250
$payment_intent_mock = self::MOCK_CARD_PAYMENT_INTENT_TEMPLATE;
12441251
$payment_intent_mock['id'] = $payment_intent_id;
12451252
$payment_intent_mock['amount'] = $amount;
1253+
$payment_intent_mock['currency'] = $currency;
12461254
$payment_intent_mock['last_payment_error'] = [];
12471255
$payment_intent_mock['payment_method'] = $payment_method_mock;
12481256
$payment_intent_mock['latest_charge'] = 'ch_mock';
@@ -2248,7 +2256,7 @@ public function test_pre_order_payment_is_successful() {
22482256
$order = WC_Helper_Order::create_order();
22492257
$order_id = $order->get_id();
22502258

2251-
list( $amount, $description, $metadata ) = $this->get_order_details( $order );
2259+
list( $amount, $description, $metadata, $currency ) = $this->get_order_details( $order );
22522260
$order->set_payment_method( WC_Stripe_UPE_Payment_Gateway::ID );
22532261
$order->save();
22542262

@@ -2260,6 +2268,7 @@ public function test_pre_order_payment_is_successful() {
22602268
$payment_intent_mock = self::MOCK_CARD_PAYMENT_INTENT_TEMPLATE;
22612269
$payment_intent_mock['id'] = $payment_intent_id;
22622270
$payment_intent_mock['amount'] = $amount;
2271+
$payment_intent_mock['currency'] = $currency;
22632272
$payment_intent_mock['last_payment_error'] = [];
22642273
$payment_intent_mock['payment_method'] = $payment_method_mock;
22652274
$payment_intent_mock['latest_charge'] = 'ch_mock';

0 commit comments

Comments
 (0)