Skip to content

Commit e545176

Browse files
Mayishawjrosa
andauthored
Fix: Unable to process payment for $0 subscription with recurring coupon (#4429)
* update logic for calculating the `amount` for mandate option - use subtotal if total is 0 due to free trial or coupon - use subtotal only during a setup intent creation * use the mandate logic from subscription trait * update test * add changelog * mock the filter --------- Co-authored-by: Wesley Rosa <[email protected]>
1 parent 1c26a12 commit e545176

8 files changed

+88
-31
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
* Tweak - Remove Payment Method Configurations fallback cache
4343
* Tweak - Update deprecation notice message to specify that legacy checkout experience has been deprecated since version 9.6.0
4444
* Update - Remove legacy checkout checkbox from settings
45+
* Fix - Fix payment processing for $0 subscription with recurring coupon
4546
* Dev - Add e2e tests to cover Affirm purchase flow
4647

4748
= 9.5.3 - 2025-06-23 =

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

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -848,30 +848,13 @@ private function maybe_add_mandate_options( $request, $payment_method_type, $is_
848848
}
849849

850850
if ( WC_Stripe_Payment_Methods::CARD === $payment_method_type && $order && $is_setup_intent ) {
851-
$currency = $order->get_currency();
852-
// We don't need to add mandate options if the currency is not supported for Indian recurring payment mandates.
853-
if ( ! WC_Stripe_Helper::is_currency_supported_for_indian_recurring_payment_mandate( $currency ) ) {
854-
return $request;
855-
}
856-
857-
$mandate_options = [
858-
'currency' => strtolower( $currency ), // Currency is required for mandate options when creating a setup intent for card payment methods.
859-
'reference' => $order->get_id(),
860-
'amount_type' => 'fixed',
861-
'amount' => WC_Stripe_Helper::get_stripe_amount( $order->get_total(), $currency ),
862-
'start_date' => time(),
863-
'interval' => 'sporadic',
864-
'supported_types' => [ 'india' ],
865-
];
866-
867-
$request['payment_method_options'][ WC_Stripe_Payment_Methods::CARD ]['mandate_options'] = $mandate_options;
868-
869851
// Run the necessary filter to make sure correct mandate information is added for recurring card payments for subscriptions.
870852
$request = apply_filters(
871853
'wc_stripe_generate_create_intent_request',
872854
$request,
873855
$order,
874-
null // $prepared_source parameter is not necessary for adding mandate information.
856+
null, // $prepared_source parameter is not necessary for adding mandate information.
857+
true // $is_setup_intent parameter is true for setup intents.
875858
);
876859
}
877860

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function maybe_init_subscriptions() {
8989
add_filter( 'wc_stripe_display_save_payment_method_checkbox', [ $this, 'display_save_payment_method_checkbox' ] );
9090

9191
// Add the necessary information to create a mandate to the payment intent.
92-
add_filter( 'wc_stripe_generate_create_intent_request', [ $this, 'add_subscription_information_to_intent' ], 10, 3 );
92+
add_filter( 'wc_stripe_generate_create_intent_request', [ $this, 'add_subscription_information_to_intent' ], 10, 4 );
9393

9494
/*
9595
* WC subscriptions hooks into the "template_redirect" hook with priority 100.
@@ -758,7 +758,7 @@ public function validate_subscription_payment_meta( $payment_method_id, $payment
758758
* @param WC_Order $order The renewal order.
759759
* @param object $prepared_source The source object.
760760
*/
761-
public function add_subscription_information_to_intent( $request, $order, $prepared_source ) {
761+
public function add_subscription_information_to_intent( $request, $order, $prepared_source, $is_setup_intent = false ) {
762762
// Just in case the order doesn't contain a subscription we return the base request.
763763
if ( ! $this->has_subscription( $order->get_id() ) ) {
764764
return $request;
@@ -797,7 +797,7 @@ public function add_subscription_information_to_intent( $request, $order, $prepa
797797

798798
// Add mandate options to request to create new mandate if mandate id does not already exist in a previous renewal or parent order.
799799
// Note: This is for backwards compatibility if `_stripe_mandate_id` is not set.
800-
$mandate_options = $this->create_mandate_options_for_order( $order, $subscriptions_for_renewal_order );
800+
$mandate_options = $this->create_mandate_options_for_order( $order, $subscriptions_for_renewal_order, $is_setup_intent );
801801
if ( ! empty( $mandate_options ) ) {
802802
if ( ! isset( $request['payment_method_options']['card']['mandate_options'] ) ) {
803803
$request['payment_method_options']['card']['mandate_options'] = [];
@@ -845,9 +845,10 @@ private function get_mandate_for_subscription( $order, $payment_method ) {
845845
*
846846
* @param WC_Order $order The renewal order.
847847
* @param WC_Subscription[] $subscriptions Subscriptions for the renewal order.
848+
* @param bool $is_setup_intent Whether the intent is a setup intent.
848849
* @return array the mandate_options for the subscription order.
849850
*/
850-
private function create_mandate_options_for_order( $order, $subscriptions ) {
851+
private function create_mandate_options_for_order( $order, $subscriptions, $is_setup_intent = false ) {
851852
$mandate_options = [];
852853
$currency = strtolower( $order->get_currency() );
853854

@@ -895,6 +896,14 @@ private function create_mandate_options_for_order( $order, $subscriptions ) {
895896
$sub_amount += WC_Stripe_Helper::get_stripe_amount( $sub->get_total(), $currency );
896897
}
897898

899+
// If the total amount is 0 and it's a setup intent, then calculate the amount from the subscription subtotal.
900+
// The total could be 0 if the subscription has a free trial or a coupon is used.
901+
if ( 0 === $sub_amount && $is_setup_intent ) {
902+
foreach ( $subscriptions as $sub ) {
903+
$sub_amount += WC_Stripe_Helper::get_stripe_amount( $sub->get_subtotal(), $currency );
904+
}
905+
}
906+
898907
// Get the first subscription associated with this order.
899908
$sub = reset( $subscriptions );
900909

@@ -920,6 +929,11 @@ private function create_mandate_options_for_order( $order, $subscriptions ) {
920929
$mandate_options['interval'] = 'sporadic';
921930
}
922931

932+
// Currency is required for mandate options when creating a setup intent for card payment methods.
933+
if ( $is_setup_intent ) {
934+
$mandate_options['currency'] = $currency;
935+
}
936+
923937
$mandate_options['amount'] = $sub_amount;
924938
$mandate_options['reference'] = $order->get_id();
925939
$mandate_options['start_date'] = time();

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
154154
* Dev - Add Klarna e2e tests
155155
* Tweak - Update deprecation notice message to specify that legacy checkout experience has been deprecated since version 9.6.0
156156
* Update - Remove legacy checkout checkbox from settings
157+
* Fix - Fix payment processing for $0 subscription with recurring coupon
157158
* Dev - Add e2e tests to cover Affirm purchase flow
158159

159160
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

tests/phpunit/Helpers/WC_Subscription.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ public function get_type() {
4949
return $this->order_type;
5050
}
5151

52+
/**
53+
* Get billing period.
54+
*
55+
* @return string
56+
*/
57+
public function get_billing_period() {
58+
return 'month';
59+
}
60+
61+
/**
62+
* Get billing interval.
63+
*
64+
* @return int
65+
*/
66+
public function get_billing_interval() {
67+
return 1;
68+
}
69+
5270
/**
5371
* Generates a URL to add or change the subscription's payment method from the my account page.
5472
*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/**
4+
* A helper class for setting up mocks for WC_Subscriptions_Switcher functions.
5+
*/
6+
class WC_Subscriptions_Switcher {
7+
8+
/**
9+
* Mock value for cart_contains_switches.
10+
*
11+
* @var array
12+
*/
13+
public static $cart_contains_switches = false;
14+
15+
public static function cart_contains_switches() {
16+
return self::$cart_contains_switches;
17+
}
18+
}

tests/phpunit/WC_Stripe_Intent_Controller_Test.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use WC_Stripe_Intent_Controller;
99
use WC_Stripe_Payment_Methods;
1010
use WC_Stripe_UPE_Payment_Gateway;
11+
use WC_Subscription;
12+
use WC_Subscriptions_Switcher;
13+
use WC_Subscriptions_Helpers;
1114
use WooCommerce\Stripe\Tests\Helpers\WC_Helper_Order;
1215
use WP_UnitTestCase;
1316

@@ -49,7 +52,7 @@ public function set_up() {
4952
$this->order = WC_Helper_Order::create_order();
5053
$this->gateway = $this->getMockBuilder( 'WC_Stripe_UPE_Payment_Gateway' )
5154
->setConstructorArgs( [ $mock_account ] )
52-
->setMethods( [ 'maybe_process_upe_redirect' ] )
55+
->setMethods( [ 'maybe_process_upe_redirect', 'has_subscription' ] )
5356
->getMock();
5457
$this->mock_controller = $this->getMockBuilder( 'WC_Stripe_Intent_Controller' )
5558
->disableOriginalConstructor()
@@ -58,6 +61,9 @@ public function set_up() {
5861
$this->mock_controller->expects( $this->any() )
5962
->method( 'get_gateway' )
6063
->willReturn( $this->gateway );
64+
$this->gateway->expects( $this->any() )
65+
->method( 'has_subscription' )
66+
->willReturn( true );
6167
}
6268

6369
public function test_wether_default_capture_method_is_set_in_the_intent() {
@@ -479,17 +485,32 @@ public function test_create_and_confirm_setup_intent_error() {
479485
}
480486

481487
/**
482-
* Test mandate options for card payment method in setup intent.
488+
* Test mandate options for card payment method in setup intent for subscription.
483489
*/
484-
public function test_mandate_options_for_card_setup_intent() {
490+
public function test_mandate_options_for_card_setup_intent_for_subscription() {
491+
// create a subscription
492+
$subscription = new WC_Subscription();
493+
$subscription->set_status( 'active' );
494+
$subscription->set_total( 100 );
495+
$subscription->set_currency( 'USD' );
496+
$subscription->set_customer_id( 'cus_mock' );
497+
$subscription->set_payment_method( 'pm_mock' );
498+
$subscription->save();
499+
500+
WC_Subscriptions_Switcher::$cart_contains_switches = false;
501+
WC_Subscriptions_Helpers::$wcs_get_subscriptions_for_order = [ $subscription ];
502+
503+
// Manually add the subscription filter that would normally be added by maybe_init_subscriptions()
504+
add_filter( 'wc_stripe_generate_create_intent_request', [ $this->gateway, 'add_subscription_information_to_intent' ], 10, 4 );
505+
485506
$payment_information = [
486507
'payment_method' => 'pm_mock',
487-
'customer' => 'cus_mock',
508+
'customer' => 'cus_mock',
488509
'selected_payment_type' => WC_Stripe_Payment_Methods::CARD,
489-
'payment_method_types' => [ WC_Stripe_Payment_Methods::CARD ],
490-
'return_url' => 'https://example.com/return',
491-
'order' => $this->order,
492-
'use_stripe_sdk' => 'true',
510+
'payment_method_types' => [ WC_Stripe_Payment_Methods::CARD ],
511+
'return_url' => 'https://example.com/return',
512+
'order' => $subscription,
513+
'use_stripe_sdk' => 'true',
493514
];
494515

495516
$test_request = function ( $preempt, $parsed_args, $url ) {

tests/phpunit/bootstrap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,4 @@ function _manually_load_plugin() {
5858
require_once __DIR__ . '/Helpers/WC_Subscriptions.php';
5959
require_once __DIR__ . '/Helpers/WC_Subscriptions_Cart.php';
6060
require_once __DIR__ . '/Helpers/WC_Subscriptions_Helpers.php';
61+
require_once __DIR__ . '/Helpers/WC_Subscriptions_Switcher.php';

0 commit comments

Comments
 (0)