Skip to content

Commit d1473df

Browse files
committed
fix rate selection
1 parent a2c7a51 commit d1473df

File tree

5 files changed

+147
-9
lines changed

5 files changed

+147
-9
lines changed

client/express-checkout/compatibility/__tests__/wc-subscriptions.test.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,34 @@ describe( 'ECE WC Subscriptions compatibility', () => {
190190
).toBe( 1999 );
191191
} );
192192

193+
it( 'includes shipping from selected rate in recurring total', () => {
194+
const cart = buildTrialCart( {
195+
subscriptions: [
196+
buildSubscriptionSchedule( {
197+
totalPrice: '2499',
198+
totalItems: '1999',
199+
shippingRates: [
200+
{
201+
package_id: 'sub_month_0',
202+
shipping_rates: [
203+
{
204+
rate_id: 'flat_rate:1',
205+
price: '500',
206+
taxes: '0',
207+
selected: true,
208+
},
209+
],
210+
},
211+
],
212+
} ),
213+
],
214+
} );
215+
216+
expect(
217+
applyFilters( 'wcpay.express-checkout.total-amount', 0, cart )
218+
).toBe( 2499 );
219+
} );
220+
193221
it( 'sums recurring totals across multiple subscription schedules', () => {
194222
const cart = buildTrialCart( {
195223
items: [
@@ -391,6 +419,40 @@ describe( 'ECE WC Subscriptions compatibility', () => {
391419
expect( result.items[ 1 ].totals.line_subtotal ).toBe( '1500' );
392420
} );
393421

422+
it( 'includes shipping from selected rate in totals', () => {
423+
const cart = buildTrialCart( {
424+
subscriptions: [
425+
buildSubscriptionSchedule( {
426+
totalPrice: '2499',
427+
totalItems: '1999',
428+
shippingRates: [
429+
{
430+
package_id: 'sub_month_0',
431+
shipping_rates: [
432+
{
433+
rate_id: 'flat_rate:1',
434+
name: 'Standard',
435+
price: '500',
436+
taxes: '0',
437+
selected: true,
438+
},
439+
],
440+
},
441+
],
442+
} ),
443+
],
444+
} );
445+
446+
const result = applyFilters(
447+
'wcpay.express-checkout.map-line-items',
448+
cart
449+
);
450+
451+
expect( result.totals.total_price ).toBe( '2499' );
452+
expect( result.totals.total_shipping ).toBe( '500' );
453+
expect( result.totals.total_items ).toBe( '1999' );
454+
} );
455+
394456
it( 'handles multiple billing periods and aggregates totals', () => {
395457
const cart = buildTrialCart( {
396458
items: [

client/express-checkout/compatibility/wc-subscriptions.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,27 @@ const getRecurringCartTotal = ( cartData ) => {
8181
totalRecurring += parseInt( subscription.totals.total_price, 10 );
8282
totalItems += parseInt( subscription.totals.total_items || '0', 10 );
8383
totalTax += parseInt( subscription.totals.total_tax || '0', 10 );
84-
totalShipping += parseInt(
85-
subscription.totals.total_shipping || '0',
86-
10
87-
);
88-
totalShippingTax += parseInt(
89-
subscription.totals.total_shipping_tax || '0',
90-
10
84+
85+
// During free trials, subscription.totals.total_shipping may be 0
86+
// even after a shipping rate is selected, because shipping is deferred.
87+
// Read the selected rate's price from the extension's shipping_rates instead.
88+
const selectedRate = subscription.shipping_rates?.[ 0 ]?.shipping_rates?.find(
89+
( r ) => r.selected
9190
);
91+
if ( selectedRate ) {
92+
totalShipping += parseInt( selectedRate.price || '0', 10 );
93+
totalShippingTax += parseInt( selectedRate.taxes || '0', 10 );
94+
} else {
95+
totalShipping += parseInt(
96+
subscription.totals.total_shipping || '0',
97+
10
98+
);
99+
totalShippingTax += parseInt(
100+
subscription.totals.total_shipping_tax || '0',
101+
10
102+
);
103+
}
104+
92105
currencyMinorUnit =
93106
subscription.totals.currency_minor_unit ?? currencyMinorUnit;
94107

client/express-checkout/event-handlers.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from './transformers/wc-to-stripe';
3030

3131
let lastSelectedAddress = null;
32+
let lastCartData = null;
3233
let cartApi = new ExpressCheckoutCartApi();
3334
export const setCartApiHandler = ( handler ) => ( cartApi = handler );
3435
export const getCartApiHandler = () => cartApi;
@@ -69,6 +70,8 @@ export const shippingAddressChangeHandler = async ( event, elements ) => {
6970
),
7071
} );
7172

73+
lastCartData = cartData;
74+
7275
event.resolve( {
7376
shippingRates,
7477
lineItems: transformCartDataForDisplayItems( cartData ),
@@ -83,19 +86,27 @@ export const shippingRateChangeHandler = async (
8386
elements,
8487
currentCartData = null
8588
) => {
89+
// Use the most recent cart data from a previous address/rate change,
90+
// falling back to the caller-provided data. This ensures we have
91+
// up-to-date subscription extension data (e.g., shipping rates for
92+
// the current address) when resolving the shipping package ID.
93+
const effectiveCartData = lastCartData || currentCartData;
94+
8695
try {
8796
const cartData = await cartApi.selectShippingRate( {
8897
// Apply filter to get the correct package ID (e.g., for trial subscriptions
8998
// where shipping is in subscription extensions, not main cart)
9099
package_id: applyFilters(
91100
'wcpay.express-checkout.shipping-package-id',
92101
0,
93-
currentCartData,
102+
effectiveCartData,
94103
event.shippingRate.id
95104
),
96105
rate_id: event.shippingRate.id,
97106
} );
98107

108+
lastCartData = cartData;
109+
99110
elements.update( {
100111
// Apply filter to allow modifications (e.g., for trial subscriptions with $0 initial payment)
101112
amount: applyFilters(

includes/express-checkout/class-wc-payments-express-checkout-button-helper.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,11 @@ public function has_subscription_product() {
284284
* This is used to determine if ECE buttons should be shown even when the cart
285285
* total is $0, as the customer will still need to authorize the recurring payment.
286286
*
287+
* Only returns true when the cart needs shipping, because Express Checkout
288+
* collects a shipping address for physical products — which also provides the
289+
* billing information needed to calculate taxes correctly. Virtual-only carts
290+
* don't trigger address collection, so the displayed price could be wrong.
291+
*
287292
* @return boolean True if cart is zero total with a trial subscription that has a recurring amount.
288293
*/
289294
public function is_cart_zero_total_with_trial_subscription() {
@@ -300,6 +305,14 @@ public function is_cart_zero_total_with_trial_subscription() {
300305
return false;
301306
}
302307

308+
// Only allow when the cart needs shipping — Express Checkout collects
309+
// a shipping address for physical products, giving us the billing info
310+
// required for correct tax calculation. Virtual-only carts skip address
311+
// collection so the price shown could be inaccurate.
312+
if ( ! WC()->cart->needs_shipping() ) {
313+
return false;
314+
}
315+
303316
// Check if cart contains subscriptions.
304317
if ( ! WC_Subscriptions_Cart::cart_contains_subscription() ) {
305318
return false;
@@ -841,6 +854,14 @@ private function is_product_supported() {
841854
|| ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported.
842855
|| ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page.
843856
|| ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page.
857+
// Virtual subscriptions with a free trial are not supported because Express
858+
// Checkout won't collect a shipping address, so we can't calculate taxes.
859+
|| (
860+
class_exists( 'WC_Subscriptions_Product' )
861+
&& WC_Subscriptions_Product::is_subscription( $product )
862+
&& ! $product->needs_shipping()
863+
&& WC_Subscriptions_Product::get_trial_length( $product ) > 0
864+
)
844865
) {
845866
$is_supported = false;
846867
} elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) {

tests/unit/express-checkout/test-class-wc-payments-express-checkout-button-helper.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,6 @@ public function test_is_cart_zero_total_with_trial_subscription_returns_false_wh
762762
'name' => 'Regular Subscription',
763763
'regular_price' => 0,
764764
'price' => 0,
765-
'virtual' => true,
766765
]
767766
);
768767
$product->save();
@@ -784,6 +783,38 @@ public function test_is_cart_zero_total_with_trial_subscription_returns_false_wh
784783
$this->assertFalse( $helper->is_cart_zero_total_with_trial_subscription() );
785784
}
786785

786+
public function test_is_cart_zero_total_with_trial_subscription_returns_false_when_cart_is_virtual_only() {
787+
// Virtual-only carts should not qualify because Express Checkout
788+
// won't collect a shipping address, so we can't calculate taxes.
789+
WC()->cart->empty_cart();
790+
$product = new WC_Product_Simple();
791+
$product->set_props(
792+
[
793+
'name' => 'Virtual Trial Subscription',
794+
'regular_price' => 0,
795+
'price' => 0,
796+
'virtual' => true,
797+
]
798+
);
799+
$product->save();
800+
WC()->cart->add_to_cart( $product->get_id(), 1 );
801+
WC()->cart->calculate_totals();
802+
803+
WC_Subscriptions_Product::$is_subscription = true;
804+
WC_Subscriptions_Product::$trial_length = 1;
805+
WC_Subscriptions_Cart::set_cart_contains_subscription( true );
806+
807+
$helper = $this->getMockBuilder( WC_Payments_Express_Checkout_Button_Helper::class )
808+
->setConstructorArgs( [ $this->mock_wcpay_gateway, $this->mock_wcpay_account ] )
809+
->onlyMethods( [ 'is_cart', 'is_checkout' ] )
810+
->getMock();
811+
812+
$helper->method( 'is_cart' )->willReturn( true );
813+
$helper->method( 'is_checkout' )->willReturn( false );
814+
815+
$this->assertFalse( $helper->is_cart_zero_total_with_trial_subscription() );
816+
}
817+
787818
public function test_should_show_express_checkout_button_with_zero_total_trial_subscription() {
788819
// Set up a zero-total cart.
789820
WC()->cart->empty_cart();

0 commit comments

Comments
 (0)