Skip to content

Commit 70664a0

Browse files
authored
Add ACSS support for WC Subscriptions (#4051)
* Add ACSS payment tokenization * Add support for ACSS payment tokenization * Fix mandate using saved payment method at checkout * Add support for payment method type in setup intent initialization * Add unit tests for WC_Payment_Token_ACSS class * Fix unit tests * Fix "init_setup_intent" * Add ACSS support for WC Subscriptions * Handle mandate ID for renewal orders * Refactor update_payment_intent method to support setup intents * Add mandate ID support for additional payment methods in renewal orders * Add support for free trial subscriptions in blocks checkout * Update ACSS payment method options to support combined payment schedules * Add changelog * Fix changing payment method for subscription * Remove duplicate switch case * Refactor update_intent method to use consistent parameter naming for intent
1 parent d0a08eb commit 70664a0

File tree

8 files changed

+65
-22
lines changed

8 files changed

+65
-22
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
* Fix - Fix subscription renewal issues for Amazon Pay.
3333
* Tweak - SPE: Remove radio buttons
3434
* Update - Hide express checkout buttons when no product variation is selected.
35+
* Add - Add ACSS support for WC Subscriptions.
3536

3637
= 9.3.1 - 2025-03-14 =
3738
* Fix - Temporarily disables the subscriptions detached notice feature due to long loading times on stores with many subscriptions.

client/blocks/upe/upe-deferred-intent-creation/payment-elements.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ const PaymentElements = ( {
5252

5353
async function createIntent() {
5454
try {
55-
const response = await api.createIntent(
56-
getBlocksConfiguration()?.orderId,
57-
paymentMethodId
58-
);
55+
const paymentNeeded = getBlocksConfiguration()?.isPaymentNeeded;
56+
const response = paymentNeeded
57+
? await api.createIntent(
58+
getBlocksConfiguration()?.orderId,
59+
paymentMethodId
60+
)
61+
: await api.initSetupIntent( paymentMethodId );
5962

6063
setClientSecret( response.client_secret );
6164
setPaymentIntentId( response.id );

includes/abstracts/abstract-wc-stripe-payment-gateway.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,8 +558,13 @@ public function process_response( $response, $order ) {
558558
$this->update_fees( $order, is_string( $response->balance_transaction ) ? $response->balance_transaction : $response->balance_transaction->id );
559559
}
560560

561+
// TODO: Refactor and add mandate ID support for other payment methods, if necessary.
562+
// The mandate ID is not available for the intent object, so we need to fetch the charge.
563+
// Mandate ID is necessary for renewal payments for certain payment methods and Indian cards.
561564
if ( isset( $response->payment_method_details->card->mandate ) ) {
562565
$order->update_meta_data( '_stripe_mandate_id', $response->payment_method_details->card->mandate );
566+
} elseif ( isset( $response->payment_method_details->acss_debit->mandate ) ) {
567+
$order->update_meta_data( '_stripe_mandate_id', $response->payment_method_details->acss_debit->mandate );
563568
}
564569

565570
if ( isset( $response->payment_method, $response->payment_method_details ) ) {
@@ -1631,14 +1636,23 @@ public function save_intent_to_order( $order, $intent ) {
16311636
if ( 'payment_intent' === $intent->object ) {
16321637
WC_Stripe_Helper::add_payment_intent_to_order( $intent->id, $order );
16331638

1634-
// Add the mandate id necessary for renewal payments with Indian cards if it's present.
1639+
// TODO: Refactor and add mandate ID support for other payment methods, if necessary.
1640+
// The mandate ID is not available for the intent object, so we need to fetch the charge.
1641+
// Mandate ID is necessary for renewal payments for certain payment methods and Indian cards.
16351642
$charge = $this->get_latest_charge_from_intent( $intent );
16361643

16371644
if ( isset( $charge->payment_method_details->card->mandate ) ) {
16381645
$order->update_meta_data( '_stripe_mandate_id', $charge->payment_method_details->card->mandate );
1646+
} elseif ( isset( $charge->payment_method_details->acss_debit->mandate ) ) {
1647+
$order->update_meta_data( '_stripe_mandate_id', $charge->payment_method_details->acss_debit->mandate );
16391648
}
16401649
} elseif ( 'setup_intent' === $intent->object ) {
16411650
$order->update_meta_data( '_stripe_setup_intent', $intent->id );
1651+
1652+
// Add mandate for free trial subscriptions.
1653+
if ( isset( $intent->mandate ) ) {
1654+
$order->update_meta_data( '_stripe_mandate_id', $intent->mandate );
1655+
}
16421656
}
16431657

16441658
if ( is_callable( [ $order, 'save' ] ) ) {

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

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

416-
wp_send_json_success( $this->update_payment_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_upe_payment_type ), 200 );
416+
wp_send_json_success( $this->update_intent( $payment_intent_id, $order_id, $save_payment_method, $selected_upe_payment_type ), 200 );
417417
} catch ( Exception $e ) {
418418
// Send back error so it can be displayed to the customer.
419419
wp_send_json_error(
@@ -427,19 +427,20 @@ public function update_payment_intent_ajax() {
427427
}
428428

429429
/**
430-
* Updates payment intent to be able to save payment method.
430+
* Updates payment intent or setup intent to be able to save payment method.
431431
*
432432
* @since 5.6.0
433+
* @version x.x.x
433434
*
434-
* @param {string} $payment_intent_id The id of the payment intent to update.
435+
* @param {string} $intent_id The id of the payment intent or setup intent to update.
435436
* @param {int} $order_id The id of the order if intent created from Order.
436437
* @param {boolean} $save_payment_method True if saving the payment method.
437438
* @param {string} $selected_upe_payment_type The name of the selected UPE payment type or empty string.
438439
*
439440
* @throws Exception If the update intent call returns with an error.
440441
* @return array|null An array with result of the update, or nothing
441442
*/
442-
public function update_payment_intent( $payment_intent_id = '', $order_id = null, $save_payment_method = false, $selected_upe_payment_type = '' ) {
443+
public function update_intent( $intent_id = '', $order_id = null, $save_payment_method = false, $selected_upe_payment_type = '' ) {
443444
$order = wc_get_order( $order_id );
444445

445446
if ( ! is_a( $order, 'WC_Order' ) ) {
@@ -451,16 +452,20 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null
451452
$currency = $order->get_currency();
452453
$customer = new WC_Stripe_Customer( wp_get_current_user()->ID );
453454

454-
if ( $payment_intent_id ) {
455-
455+
if ( $intent_id ) {
456456
$request = [
457-
'amount' => WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) ),
458-
'currency' => strtolower( $currency ),
459457
'metadata' => $gateway->get_metadata_from_order( $order ),
460458
/* translators: 1) blog name 2) order number */
461459
'description' => sprintf( __( '%1$s - Order %2$s', 'woocommerce-gateway-stripe' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), $order->get_order_number() ),
462460
];
463461

462+
$is_setup_intent = substr( $intent_id, 0, 4 ) === 'seti';
463+
if ( ! $is_setup_intent ) {
464+
// These parameters are only supported for payment intents.
465+
$request['amount'] = WC_Stripe_Helper::get_stripe_amount( $amount, strtolower( $currency ) );
466+
$request['currency'] = strtolower( $currency );
467+
}
468+
464469
if ( '' !== $selected_upe_payment_type ) {
465470
// Only update the payment_method_types if we have a reference to the payment type the customer selected.
466471
$request['payment_method_types'] = [ $selected_upe_payment_type ];
@@ -488,16 +493,21 @@ public function update_payment_intent( $payment_intent_id = '', $order_id = null
488493

489494
$level3_data = $gateway->get_level3_data_from_order( $order );
490495

496+
// Use "setup_intents" endpoint if `$intent_id` starts with `seti_`.
497+
$endpoint = $is_setup_intent ? 'setup_intents' : 'payment_intents';
491498
WC_Stripe_API::request_with_level3_data(
492499
$request,
493-
"payment_intents/{$payment_intent_id}",
500+
"{$endpoint}/{$intent_id}",
494501
$level3_data,
495502
$order
496503
);
497504

498-
$order->update_status( OrderStatus::PENDING, __( 'Awaiting payment.', 'woocommerce-gateway-stripe' ) );
505+
// Prevent any failures if updating the status of a subscription order.
506+
if ( ! $gateway->has_subscription( $order_id ) ) {
507+
$order->update_status( OrderStatus::PENDING, __( 'Awaiting payment.', 'woocommerce-gateway-stripe' ) );
508+
}
499509
$order->save();
500-
WC_Stripe_Helper::add_payment_intent_to_order( $payment_intent_id, $order );
510+
WC_Stripe_Helper::add_payment_intent_to_order( $intent_id, $order );
501511
}
502512

503513
return [
@@ -822,8 +832,8 @@ private function maybe_add_mandate_options( $request, $payment_method_type, $is_
822832
$request['payment_method_options'] = [
823833
WC_Stripe_Payment_Methods::ACSS_DEBIT => [
824834
'mandate_options' => [
825-
'payment_schedule' => 'interval',
826-
'interval_description' => __( 'One-time payment', 'woocommerce-gateway-stripe' ), // TODO: Change to cadence if purchasing a subscription.
835+
'payment_schedule' => 'combined',
836+
'interval_description' => __( 'Payments as per agreement', 'woocommerce-gateway-stripe' ),
827837
'transaction_type' => 'personal',
828838
],
829839
],

includes/compat/trait-wc-stripe-subscriptions.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -741,9 +741,9 @@ public function validate_subscription_payment_meta( $payment_method_id, $payment
741741
* mandates for 3DS payments in India. It's ok to apply this across the board; Stripe will
742742
* take care of handling any authorizations.
743743
*
744-
* @param Array $request The HTTP request that will be sent to Stripe to create the payment intent.
744+
* @param array $request The HTTP request that will be sent to Stripe to create the payment intent.
745745
* @param WC_Order $order The renewal order.
746-
* @param Array $prepared_source The source object.
746+
* @param object $prepared_source The source object.
747747
*/
748748
public function add_subscription_information_to_intent( $request, $order, $prepared_source ) {
749749
// Just in case the order doesn't contain a subscription we return the base request.
@@ -783,6 +783,7 @@ public function add_subscription_information_to_intent( $request, $order, $prepa
783783
}
784784

785785
// Add mandate options to request to create new mandate if mandate id does not already exist in a previous renewal or parent order.
786+
// Note: This is for backwards compatibility if `_stripe_mandate_id` is not set.
786787
$mandate_options = $this->create_mandate_options_for_order( $order, $subscriptions_for_renewal_order );
787788
if ( ! empty( $mandate_options ) ) {
788789
$request['payment_method_options']['card']['mandate_options'] = $mandate_options;
@@ -1000,12 +1001,20 @@ public function maybe_render_subscription_payment_method( $payment_method_to_dis
10001001
break 3;
10011002
case WC_Stripe_Payment_Methods::ACH:
10021003
$payment_method_to_display = sprintf(
1003-
/* translators: account type (checking, savings), last 4 digits of account. */
1004+
/* translators: 1) account type (checking, savings), 2) last 4 digits of account. */
10041005
__( 'Via %1$s Account ending in %2$s', 'woocommerce-gateway-stripe' ),
10051006
ucfirst( $source->us_bank_account->account_type ),
10061007
$source->us_bank_account->last4
10071008
);
10081009
break 3;
1010+
case WC_Stripe_Payment_Methods::ACSS_DEBIT:
1011+
$payment_method_to_display = sprintf(
1012+
/* translators: 1) bank name, 2) last 4 digits of account. */
1013+
__( 'Via %1$s ending in %2$s', 'woocommerce-gateway-stripe' ),
1014+
$source->acss_debit->bank_name,
1015+
$source->acss_debit->last4
1016+
);
1017+
break 3;
10091018
case WC_Stripe_Payment_Methods::BACS_DEBIT:
10101019
/* translators: 1) the Bacs Direct Debit payment method's last 4 numbers */
10111020
$payment_method_to_display = sprintf( __( 'Via Bacs Direct Debit ending in (%1$s)', 'woocommerce-gateway-stripe' ), $source->bacs_debit->last4 );

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ public function process_payment( $order_id, $retry = true, $force_save_source =
731731
if ( $payment_intent_id && ! $this->payment_methods[ $selected_payment_type ]->supports_deferred_intent() ) {
732732
// Adds customer and metadata to PaymentIntent.
733733
// These parameters cannot be added upon updating the intent via the `/confirm` API.
734-
$this->intent_controller->update_payment_intent( $payment_intent_id, $order_id );
734+
$this->intent_controller->update_intent( $payment_intent_id, $order_id );
735735
}
736736

737737
// Flag for using a deferred intent. To be removed.

includes/payment-methods/class-wc-stripe-upe-payment-method-acss.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* The Canadian Pre-Authorized Debit (ACSS Debit) Payment Method class extending UPE base class
88
*/
99
class WC_Stripe_UPE_Payment_Method_ACSS extends WC_Stripe_UPE_Payment_Method {
10+
use WC_Stripe_Subscriptions_Trait;
1011

1112
const STRIPE_ID = WC_Stripe_Payment_Methods::ACSS_DEBIT;
1213

@@ -27,6 +28,10 @@ public function __construct() {
2728
);
2829
$this->supports_deferred_intent = false;
2930
$this->supports[] = 'tokenization';
31+
$this->supports[] = 'subscriptions';
32+
33+
// Check if subscriptions are enabled and add support for them.
34+
$this->maybe_init_subscriptions();
3035
}
3136

3237
/**

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,5 +142,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
142142
* Fix - Fix subscription renewal issues for Amazon Pay.
143143
* Tweak - SPE: Remove radio buttons
144144
* Update - Hide express checkout buttons when no product variation is selected.
145+
* Add - Add ACSS support for WC Subscriptions.
145146

146147
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

0 commit comments

Comments
 (0)