Skip to content

Commit 89e0ced

Browse files
wjrosadaledupreezCopilot
authored
Fix booking multiple slots with ECE (#4529)
* Fix booking multiple slots with ECE * Fix cart slots not being released * Unit test * Fix tests * Changelog and readme entries * Update includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php Co-authored-by: daledupreez <[email protected]> * Update includes/payment-methods/class-wc-stripe-express-checkout-helper.php Co-authored-by: Copilot <[email protected]> * Update includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php Co-authored-by: daledupreez <[email protected]> * Update includes/payment-methods/class-wc-stripe-express-checkout-helper.php Co-authored-by: daledupreez <[email protected]> * Unit tests --------- Co-authored-by: daledupreez <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 19b7d19 commit 89e0ced

File tree

6 files changed

+178
-67
lines changed

6 files changed

+178
-67
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
*** Changelog ***
22

33
= 9.8.0 - xxxx-xx-xx =
4+
* Fix - Fixes issues related to booking multiple slots with express checkout payment methods enabled
45
* Fix - Update the Optimized Checkout promotional inbox note to link to the relevant section in the Stripe settings page
56
* Add - Makes the Optimized Checkout feature available for all merchants by default
67
* Add - Adds a new bulk action option to the subscriptions listing screen to check for detached payment methods

client/entrypoints/express-checkout/index.js

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@ jQuery( function ( $ ) {
649649
*/
650650
addToCart: async () => {
651651
let productId = $( '.single_add_to_cart_button' ).val();
652+
let emptyCartParams = {};
652653

653654
const data = {
654655
qty: $( quantityInputSelector ).val(),
@@ -663,6 +664,9 @@ jQuery( function ( $ ) {
663664

664665
if ( $( '.wc-bookings-booking-form' ).length ) {
665666
productId = $( '.wc-booking-product-id' ).val();
667+
emptyCartParams = {
668+
bookingId: productId,
669+
};
666670
}
667671

668672
// Add extension data to the POST body
@@ -701,7 +705,7 @@ jQuery( function ( $ ) {
701705
// do not interfere with computed totals.
702706
// Use the non-StoreAPI method as it is faster; Stripe requires
703707
// the click event to be resolved within 1 second.
704-
await api.expressCheckoutEmptyCartLegacy( {} );
708+
await api.expressCheckoutEmptyCartLegacy( emptyCartParams );
705709

706710
return api.expressCheckoutAddToCart( data );
707711
},
@@ -902,48 +906,4 @@ jQuery( function ( $ ) {
902906
$( document.body ).on( 'updated_checkout', () => {
903907
wcStripeECE.init();
904908
} );
905-
906-
// Handle bookable products on the product page.
907-
let wcBookingFormChanged = false;
908-
909-
$( document.body )
910-
.off( 'wc_booking_form_changed' )
911-
.on( 'wc_booking_form_changed', () => {
912-
wcBookingFormChanged = true;
913-
} );
914-
915-
// Listen for the WC Bookings wc_bookings_calculate_costs event to complete
916-
// and add the bookable product to the cart, using the response to update the
917-
// payment request request params with correct totals.
918-
$( document ).ajaxComplete( function ( event, xhr, settings ) {
919-
if ( wcBookingFormChanged ) {
920-
if (
921-
settings.url === window.booking_form_params.ajax_url &&
922-
settings.data.includes( 'wc_bookings_calculate_costs' ) &&
923-
xhr.responseText.includes( 'SUCCESS' )
924-
) {
925-
wcStripeECE.blockExpressCheckoutButton();
926-
wcBookingFormChanged = false;
927-
928-
return wcStripeECE.addToCart().then( ( response ) => {
929-
getExpressCheckoutData( 'product' ).total = response.total;
930-
getExpressCheckoutData( 'product' ).displayItems =
931-
response.displayItems;
932-
933-
// Empty the cart to avoid having 2 products in the cart when payment request is not used.
934-
if ( useLegacyCartEndpoints ) {
935-
api.expressCheckoutEmptyCartLegacy( {
936-
bookingId: response.bookingId,
937-
} );
938-
} else {
939-
api.expressCheckoutEmptyCart( response.bookingId );
940-
}
941-
942-
wcStripeECE.init();
943-
944-
wcStripeECE.unblockExpressCheckoutButton();
945-
} );
946-
}
947-
}
948-
} );
949909
} );

includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,23 @@ public function ajax_add_to_cart() {
9292
$product = wc_get_product( $product_id );
9393
$product_type = $product->get_type();
9494

95+
$booking_ids = [];
96+
if ( 'booking' === $product_type ) {
97+
$booking_ids = $this->express_checkout_helper->get_booking_ids_from_cart();
98+
}
99+
95100
// First empty the cart to prevent wrong calculation.
96101
WC()->cart->empty_cart();
97102

103+
// When a bookable product is added to the cart, a 'booking' is created with status 'in-cart'.
104+
// This status is used to prevent the booking from being booked by another customer
105+
// and should be removed when the cart is emptied for ECE purposes.
106+
if ( has_action( 'wc-booking-remove-inactive-cart' ) ) { // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
107+
foreach ( $booking_ids as $booking_id ) {
108+
do_action( 'wc-booking-remove-inactive-cart', $booking_id ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
109+
}
110+
}
111+
98112
if ( ( ProductType::VARIABLE === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) {
99113
$attributes = wc_clean( wp_unslash( $_POST['attributes'] ) );
100114

@@ -112,14 +126,6 @@ public function ajax_add_to_cart() {
112126
$data += $this->express_checkout_helper->build_display_items();
113127
$data['result'] = 'success';
114128

115-
if ( 'booking' === $product_type ) {
116-
$booking_id = $this->express_checkout_helper->get_booking_id_from_cart();
117-
118-
if ( ! empty( $booking_id ) ) {
119-
$data['bookingId'] = $booking_id;
120-
}
121-
}
122-
123129
// @phpstan-ignore-next-line (return statement is added)
124130
wp_send_json( $data );
125131
}
@@ -135,9 +141,9 @@ public function ajax_clear_cart() {
135141
WC()->cart->empty_cart();
136142

137143
if ( $booking_id ) {
138-
// When a bookable product is added to the cart, a 'booking' is create with status 'in-cart'.
144+
// When a bookable product is added to the cart, a 'booking' is created with status 'in-cart'.
139145
// This status is used to prevent the booking from being booked by another customer
140-
// and should be removed when the cart is emptied for PRB purposes.
146+
// and should be removed when the cart is emptied for express checkout purposes.
141147
do_action( 'wc-booking-remove-inactive-cart', $booking_id ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
142148
}
143149

includes/payment-methods/class-wc-stripe-express-checkout-helper.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,18 +1669,38 @@ public function cart_prices_include_tax() {
16691669
* Used to remove the booking from WC Bookings in-cart status.
16701670
*
16711671
* @return int|false
1672+
*
1673+
* @deprecated 9.8.0 Use `get_booking_ids_from_cart()` instead.
16721674
*/
16731675
public function get_booking_id_from_cart() {
1674-
$cart = WC()->cart->get_cart();
1675-
$cart_item = reset( $cart );
1676-
1677-
if ( $cart_item && isset( $cart_item['booking']['_booking_id'] ) ) {
1678-
return $cart_item['booking']['_booking_id'];
1676+
$booking_ids = $this->get_booking_ids_from_cart();
1677+
if ( ! empty( $booking_ids ) ) {
1678+
return $booking_ids[0];
16791679
}
16801680

16811681
return false;
16821682
}
16831683

1684+
/**
1685+
* Gets a list of booking ids from the cart.
1686+
*
1687+
* Used to remove the booking from WC Bookings in-cart status.
1688+
*
1689+
* @return array
1690+
*/
1691+
public function get_booking_ids_from_cart() {
1692+
$cart = WC()->cart->get_cart();
1693+
$booking_ids = [];
1694+
1695+
foreach ( $cart as $item ) {
1696+
if ( ! empty( $item['booking']['_booking_id'] ) ) {
1697+
$booking_ids[] = $item['booking']['_booking_id'];
1698+
}
1699+
}
1700+
1701+
return array_unique( $booking_ids );
1702+
}
1703+
16841704
/**
16851705
* Check if the current request is an express checkout context.
16861706
*

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
111111
== Changelog ==
112112

113113
= 9.8.0 - xxxx-xx-xx =
114+
* Fix - Fixes issues related to booking multiple slots with express checkout payment methods enabled
114115
* Fix - Update the Optimized Checkout promotional inbox note to link to the relevant section in the Stripe settings page
115116
* Add - Makes the Optimized Checkout feature available for all merchants by default
116117
* Add - Adds a new bulk action option to the subscriptions listing screen to check for detached payment methods

tests/phpunit/PaymentMethods/WC_Stripe_Express_Checkout_Helper_Test.php

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ public function test_is_express_checkout_context( $is_store_api, $has_express_he
544544
*/
545545
public function provide_test_is_express_checkout_context() {
546546
return [
547-
'Not Store API request' => [
547+
'Not Store API request' => [
548548
'is_store_api' => false,
549549
'has_express_header' => true,
550550
'has_nonce_header' => true,
@@ -619,19 +619,19 @@ public function test_is_request_to_store_api( $rest_route, $expected ) {
619619
*/
620620
public function provide_test_is_request_to_store_api() {
621621
return [
622-
'No rest_route set' => [
622+
'No rest_route set' => [
623623
'rest_route' => '',
624624
'expected' => false,
625625
],
626-
'Store API checkout route' => [
626+
'Store API checkout route' => [
627627
'rest_route' => '/wc/store/v1/checkout',
628628
'expected' => true,
629629
],
630630
'Different Store API route' => [
631631
'rest_route' => '/wc/store/v1/cart',
632632
'expected' => false,
633633
],
634-
'Non-Store API route' => [
634+
'Non-Store API route' => [
635635
'rest_route' => '/wp/v2/posts',
636636
'expected' => false,
637637
],
@@ -661,7 +661,7 @@ public function test_get_stripe_currency_decimals( $currency, $expected ) {
661661
public function provide_test_get_stripe_currency_decimals() {
662662
return [
663663
// No decimal currencies - should return 0
664-
'Japanese Yen (no decimals)' => [
664+
'Japanese Yen (no decimals)' => [
665665
'currency' => 'JPY',
666666
'expected' => 0,
667667
],
@@ -671,14 +671,137 @@ public function provide_test_get_stripe_currency_decimals() {
671671
'expected' => 3,
672672
],
673673
// Default currencies - should return 2
674-
'US Dollar (default)' => [
674+
'US Dollar (default)' => [
675675
'currency' => 'USD',
676676
'expected' => 2,
677677
],
678-
'Euro (default)' => [
678+
'Euro (default)' => [
679679
'currency' => 'EUR',
680680
'expected' => 2,
681681
],
682682
];
683683
}
684+
685+
/**
686+
* Tests for `get_booking_ids_from_cart`.
687+
*
688+
* @param array $cart_contents Cart contents.
689+
* @param array $expected Expected booking IDs.
690+
* @return void
691+
*
692+
* @dataProvider provide_test_get_booking_ids_from_cart
693+
*/
694+
public function test_get_booking_ids_from_cart( $cart_contents, $expected ) {
695+
WC()->session->init();
696+
WC()->cart->empty_cart();
697+
698+
WC()->cart->cart_contents = $cart_contents;
699+
700+
$helper = new WC_Stripe_Express_Checkout_Helper();
701+
$actual = $helper->get_booking_ids_from_cart();
702+
703+
// Clean up.
704+
WC()->session->cleanup_sessions();
705+
WC()->cart->empty_cart();
706+
707+
$this->assertSame( $expected, $actual );
708+
}
709+
710+
/**
711+
* Provider for `test_get_booking_ids_from_cart`.
712+
*
713+
* @return array
714+
*/
715+
public function provide_test_get_booking_ids_from_cart() {
716+
$product_1 = WC_Helper_Product::create_simple_product();
717+
$product_1->save();
718+
719+
$product_2 = WC_Helper_Product::create_simple_product();
720+
$product_2->save();
721+
722+
$product_3 = WC_Helper_Product::create_simple_product();
723+
$product_3->save();
724+
725+
return [
726+
'no products' => [
727+
'cart contents' => [],
728+
'expected' => [],
729+
],
730+
'single product' => [
731+
'cart contents' => [
732+
[
733+
'product_id' => $product_1->get_id(),
734+
'booking' => [
735+
'_booking_id' => $product_1->get_id(),
736+
],
737+
],
738+
],
739+
'expected' => [
740+
$product_1->get_id(),
741+
],
742+
],
743+
'multiple products' => [
744+
'cart contents' => [
745+
[
746+
'product_id' => $product_1->get_id(),
747+
'booking' => [
748+
'_booking_id' => $product_1->get_id(),
749+
],
750+
],
751+
[
752+
'product_id' => $product_2->get_id(),
753+
'booking' => [
754+
'_booking_id' => $product_2->get_id(),
755+
],
756+
],
757+
],
758+
'expected' => [
759+
$product_1->get_id(),
760+
$product_2->get_id(),
761+
],
762+
],
763+
'multiple products, same ID' => [
764+
'cart contents' => [
765+
[
766+
'product_id' => $product_1->get_id(),
767+
'booking' => [
768+
'_booking_id' => $product_1->get_id(),
769+
],
770+
],
771+
[
772+
'product_id' => $product_1->get_id(),
773+
'booking' => [
774+
'_booking_id' => $product_1->get_id(),
775+
],
776+
],
777+
],
778+
'expected' => [
779+
$product_1->get_id(),
780+
],
781+
],
782+
'mixed products (booking data not always present)' => [
783+
'cart contents' => [
784+
[
785+
'product_id' => $product_1->get_id(),
786+
'booking' => [
787+
'_booking_id' => $product_1->get_id(),
788+
],
789+
],
790+
[
791+
'product_id' => $product_2->get_id(),
792+
],
793+
[
794+
'product_id' => $product_3->get_id(),
795+
'booking' => [
796+
'_booking_id' => $product_3->get_id(),
797+
],
798+
],
799+
],
800+
'expected' => [
801+
$product_1->get_id(),
802+
$product_3->get_id(),
803+
],
804+
],
805+
];
806+
}
684807
}

0 commit comments

Comments
 (0)