PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.58
Timetics – Appointment Booking Calendar & Scheduling v1.0.58
1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 All 62 releases
timetics / core / integrations / woocommerce / hooks.php

hooks.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.58, at core/integrations/woocommerce/hooks.php

784 lines 24.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Hooks Class
4 *
5 * @package Timetics
6 */
7 namespace Timetics\Core\Integrations\Woocommerce;
8
9 defined( 'ABSPATH' ) || exit;
10
11 use Timetics\Core\Appointments\Appointment;
12 use Timetics\Core\Bookings\Booking;
13 use Timetics\Core\Customers\Customer;
14 use Timetics\Utils\Singleton;
15 use Timetics\Core\Emails\New_Event_Email;
16 use Timetics\Core\Emails\New_Event_Customer_Email;
17 use Timetics\Core\Emails\Cancel_Event_Email;
18 use Timetics\Core\Emails\Cancel_Event_Customer_Email;
19 use Timetics\Core\Integrations\Woocommerce\Status_Mapper;
20
21 /**
22 * Class Hooks
23 */
24 class Hooks {
25 use Singleton;
26
27 /**
28 * Initialize
29 *
30 * @return void
31 */
32 public function init() {
33 Cart_Api::instance();
34
35 add_action( 'woocommerce_init', [$this, 'load_woocommerce_required_files'] );
36
37 add_action( 'woocommerce_thankyou', [$this, 'update_booking_payment_status'] );
38
39 // Sync booking status when WooCommerce order status changes
40 add_action( 'woocommerce_order_status_changed', [$this, 'sync_booking_status_from_order'], 10, 3 );
41
42 add_filter( 'timetics_get_settings', [$this, 'add_woocommerce_currency_support'] );
43
44 add_filter( 'woocommerce_product_query', [$this, 'modify_woocommerce_shop'] );
45
46 add_action( 'woocommerce_coupon_options_usage_restriction', [$this, 'ensure_meeting_products_exist'], 5, 2 );
47 add_action( 'woocommerce_coupon_options_usage_restriction', [$this, 'add_meeting_coupon_restriction_option'], 10, 2 );
48
49 add_action( 'timetics_meeting_after_insert', [$this, 'clear_meeting_products_cache'], 99, 2 );
50 add_action( 'before_delete_post', [$this, 'maybe_clear_cache_on_meeting_delete'], 10, 2 );
51
52 add_action( 'woocommerce_coupon_options_save', [$this, 'save_coupon_option'], 10, 2 );
53
54 add_filter( 'woocommerce_coupon_get_products', [$this, 'assign_meeting_coupon_products'], 10, 2 );
55
56 add_filter( 'timetics_settings', [$this, 'add_wc_checkout_url'] );
57
58 add_filter( 'woocommerce_checkout_fields', [$this, 'hide_checkout_fields'] );
59
60 add_filter( 'woocommerce_checkout_posted_data', [$this, 'modify_order_data'] );
61
62 add_action( 'timetics_after_booking_create', [$this, 'add_product_to_cart'], 10, 4 );
63
64 add_filter( 'timetics_currency', [ $this, 'support_woocommerce_currency' ] );
65
66 add_action( 'admin_init', [ $this, 'create_term_timetics_meeting' ] );
67
68 add_action( 'wp_head', [ $this, 'hide_regular_price_checkout_css' ] );
69
70 // Display meeting details on checkout page
71 add_action( 'woocommerce_review_order_before_payment', [ $this, 'display_meeting_details_on_checkout' ] );
72
73 // Sync order status when booking status changes (bidirectional sync)
74 add_action( 'transition_post_status', [ $this, 'sync_order_status_from_booking' ], 10, 3 );
75 }
76
77 /**
78 * Load all required files and functions
79 *
80 * @return void
81 */
82 public function load_woocommerce_required_files() {
83 if ( ! WC()->is_rest_api_request() ) {
84 return;
85 }
86
87 WC()->frontend_includes();
88
89 if ( null === WC()->cart && function_exists( 'wc_load_cart' ) ) {
90 wc_load_cart();
91 }
92 }
93
94 /**
95 * Add product data on woocommerce product data
96 *
97 * @return void
98 */
99 public function add_product_data_store( $stores ) {
100 $stores['product'] = 'Timetics\Core\Integrations\Woocommerce\Product_Data_Store';
101
102 return $stores;
103 }
104
105 /**
106 * Update booking payment status
107 *
108 * @param integer $order_id
109 *
110 * @return void
111 */
112 public function update_booking_payment_status( $order_id ) {
113
114 $order = wc_get_order( $order_id );
115
116 if ( ! $order->is_paid() ) {
117 // Mark booking as failed if payment was not completed
118 $session_data = WC()->session->get( 'timetics_data' );
119 if ( $session_data ) {
120 $booking = new Booking( $session_data['booking_id'] );
121 Status_Mapper::set_order_id_for_booking( $session_data['booking_id'], $order_id );
122 }
123 WC()->session->set( 'timetics_data', null );
124 return;
125 }
126
127 $session_data = WC()->session->get( 'timetics_data' );
128
129 if ( ! $session_data ) {
130 return;
131 }
132
133 $booking = new Booking( $session_data['booking_id'] );
134
135 // Get the default booking status (e.g., 'approved')
136 $default_booking_status = timetics_get_option( 'default_booking_status', 'approved' );
137
138 $booking->update( [
139 'post_status' => $default_booking_status,
140 'payment_status' => 'completed',
141 'payment_method' => 'woocommerce',
142 ] );
143
144 // Store the order ID in booking meta and booking id in woocommerce order meta for status syncing
145 Status_Mapper::set_order_id_for_booking( $session_data['booking_id'], $order_id );
146 Status_Mapper::set_booking_id_for_order( $order_id, $session_data['booking_id'] );
147
148 $booking->create_event();
149 $is_email_to_customer = timetics_get_option( 'booking_created_customer');
150 $is_email_to_host = timetics_get_option( 'booking_created_host');
151 if( $is_email_to_host ){
152 $new_event_email = new New_Event_Email( $booking );
153 $new_event_email->send();
154 }
155
156 if( $is_email_to_customer ){
157 $new_event_customer_email = new New_Event_Customer_Email( $booking );
158 $new_event_customer_email->send();
159 }
160
161 WC()->session->set( 'timetics_data', null );
162
163 }
164
165 /**
166 * Support woocommerce currency
167 *
168 * @param array $settings
169 *
170 * @return array
171 */
172 public function add_woocommerce_currency_support( $settings ) {
173
174 if ( ! function_exists( 'WC' ) ) {
175 return $settings;
176 }
177
178 $wc_integration = timetics_get_option( 'wc_integration' );
179
180 if ( ! $wc_integration ) {
181 return $settings;
182 }
183
184 $settings['currency'] = get_woocommerce_currency();
185
186 return $settings;
187 }
188
189 /**
190 * Hide timetics meeting from woocommerce shop page
191 *
192 * @param Object $query
193 *
194 * @return Object
195 */
196 public function modify_woocommerce_shop( $query ) {
197 $category_slug = 'timetics-meeting'; // Replace 'category-slug' with the desired category slug
198
199 // Get product category ID
200 $category = get_term_by( 'slug', $category_slug, 'product_cat' );
201
202 if ( $category ) {
203 $category_id = $category->term_id;
204
205 // Exclude products assigned to the category
206 $query->set( 'tax_query', array(
207 array(
208 'taxonomy' => 'product_cat',
209 'field' => 'term_id',
210 'terms' => $category_id,
211 'operator' => 'NOT IN',
212 ),
213 ) );
214 }
215
216 return $query;
217 }
218
219 /**
220 * Set coupon products
221 *
222 * @param integer $coupon_id
223 * @param Object $coupon
224 *
225 * @return void
226 */
227 public function save_coupon_option( $coupon_id, $coupon ) {
228 // Verify nonce for security
229 if ( ! isset( $_POST['timetics_coupon_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['timetics_coupon_nonce'] ) ), 'timetics_coupon_action' ) ) {
230 return;
231 }
232
233 $meeting_ids = ! empty( $_POST['timetics_meeting_ids'] ) ? array_map( 'intval', wp_unslash( $_POST['timetics_meeting_ids'] ) ) : [];
234
235 $coupon_products = $coupon->get_product_ids();
236 $coupon_products = array_merge( $coupon_products, $meeting_ids );
237 $coupon->update_meta_data( 'timetics_meeting_ids', $meeting_ids );
238
239 $coupon->set_product_ids( $coupon_products );
240 $coupon->save();
241 }
242
243 /**
244 * Add woocommerce coupon restriction option
245 *
246 * @param int $coupon_id
247 * @param Object $coupon
248 *
249 * @return void
250 */
251 public function add_meeting_coupon_restriction_option( $coupon_id, $coupon ) {
252 include_once TIMETICS_PLUGIN_DIR . '/templates/woocommerce/coupon-restriction-option.php';
253 }
254
255 /**
256 * Ensure all meeting products exist in WooCommerce
257 *
258 * @param int $coupon_id
259 * @param Object $coupon
260 *
261 * @return void
262 */
263 public function ensure_meeting_products_exist( $coupon_id, $coupon ) {
264 $cache_key = 'timetics_meeting_products_synced';
265 if ( get_transient( $cache_key ) ) {
266 return;
267 }
268
269 if ( ! term_exists( 'timetics-meeting', 'product_cat' ) ) {
270 timetics_add_woocommerce_product_cat();
271 }
272
273 $args = [
274 'post_type' => 'timetics-appointment',
275 'post_status' => 'publish',
276 'posts_per_page' => -1,
277 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering meetings by meta fields
278 'meta_query' => [
279 [
280 'key' => 'wc_product_id',
281 'compare' => 'NOT EXISTS',
282 ],
283 ],
284 ];
285
286 $meetings = get_posts( $args );
287
288 if ( empty( $meetings ) ) {
289 set_transient( $cache_key, true, DAY_IN_SECONDS );
290 return;
291 }
292
293 $product_creator = new Product();
294
295 foreach ( $meetings as $meeting ) {
296 $appointment = new Appointment( $meeting->ID );
297
298 if ( ! $appointment->get_id() ) {
299 continue;
300 }
301
302 if ( empty( $appointment->get_name() ) ) {
303 continue;
304 }
305
306 $product_id = $appointment->get_wc_product_id();
307 if ( ! $product_id ) {
308 $price = $appointment->get_price();
309 if ( is_array( $price ) ) {
310 $price = '';
311 }
312
313 $product_id = $product_creator->create( $appointment );
314 }
315 }
316
317 set_transient( $cache_key, true, DAY_IN_SECONDS );
318 }
319
320 /**
321 * Clear meeting products cache when new meeting is created
322 *
323 * @param mixed $appointment Appointment object or ID
324 * @param mixed $data Meeting data array
325 *
326 * @return void
327 */
328 public function clear_meeting_products_cache( $appointment = null, $data = null ) {
329 delete_transient( 'timetics_meeting_products_synced' );
330 }
331
332 /**
333 * Clear meeting products cache when a meeting is deleted
334 *
335 * @param int $post_id
336 * @param Object $post
337 *
338 * @return void
339 */
340 public function maybe_clear_cache_on_meeting_delete( $post_id, $post ) {
341 if ( 'timetics-appointment' === $post->post_type ) {
342 delete_transient( 'timetics_meeting_products_synced' );
343 }
344 }
345
346 /**
347 * Add checkout url on settings
348 *
349 * @param array $settings
350 *
351 * @return array
352 */
353 public function add_wc_checkout_url( $settings ) {
354 if ( ! timetics_is_woocommerce_active() ) {
355 return $settings;
356 }
357
358 $settings['wc_checkout_url'] = wc_get_checkout_url();
359
360 return $settings;
361 }
362
363 /**
364 * Hide all fields from checkout page
365 *
366 * @param array $fields
367 *
368 * @return array
369 */
370 public function hide_checkout_fields( $fields ) {
371 if ( ! function_exists( 'WC' ) || ! WC()->session ) {
372 return $fields;
373 }
374
375 $session_data = WC()->session->get( 'timetics_data' );
376
377 if ( ! $session_data ) {
378 return $fields;
379 }
380
381 $fields['billing'] = array();
382
383 // Hide all shipping fields
384 $fields['shipping'] = array();
385
386 $fields['order'] = array();
387
388 return $fields;
389 }
390
391 /**
392 * Modify order posted data
393 *
394 * @param array $data
395 *
396 * @return array
397 */
398 public function modify_order_data( $data ) {
399 if ( ! function_exists( 'WC' ) || ! WC()->session ) {
400 return $data;
401 }
402
403 $session_data = WC()->session->get( 'timetics_data' );
404
405 if ( ! $session_data ) {
406 return $data;
407 }
408
409 $booking = new Booking( $session_data['booking_id'] );
410 $customer = new Customer( $booking->get_customer_id() );
411
412 $first_name = $customer->get_first_name();
413 $last_name = $customer->get_last_name();
414 $email = $customer->get_email();
415 $phone = $customer->get_phone();
416
417 if ( $first_name ) {
418 $data['billing_first_name'] = $first_name;
419 }
420
421 if ( $last_name ) {
422 $data['billing_last_name'] = $last_name;
423 }
424
425 if ( $email ) {
426 $data['billing_email'] = $email;
427 }
428
429 if ( $phone ) {
430 $data['billing_phone'] = $phone;
431 }
432
433 return $data;
434 }
435
436 /**
437 * Add to cart timetics appointment
438 *
439 * @param integer $booking_id
440 * @param integer $customer_id
441 * @param integer $meeting_id
442 * @param array $data
443 *
444 * @return void
445 */
446 public function add_product_to_cart( $booking_id, $customer_id, $meeting_id, $data ) {
447 if ( 'woocommerce' !== $data['payment_method'] ) {
448 return;
449 }
450
451 $booking = new Booking( $booking_id );
452 $meeting_id = $booking->get_appointment();
453 if( !empty($data['seats']) && is_array( $data['seats'])) {
454 $quantity = count( $data['seats'] );
455 }else {
456 $quantity = 1;
457 }
458 $price = $booking->get_total();
459
460 // Get meeting details for checkout display
461 $appointment = new Appointment( $meeting_id );
462 $start_date = $booking->get_start_date();
463 $start_time = $booking->get_start_time();
464 $end_time = $booking->get_end_time();
465 $timezone = $booking->get_timezone();
466 $location = $booking->get_location();
467 $location_type = $booking->get_location_type();
468 $duration = $appointment->get_duration();
469 $meeting_timezone = $appointment->get_timezone();
470
471 // Set session for timetics data for woocommerce.
472 WC()->session->set( 'timetics_data', [
473 'booking_id' => $booking_id,
474 'meeting_id' => $meeting_id,
475 'price' => $price,
476 'start_date' => $start_date,
477 'start_time' => $start_time,
478 'end_time' => $end_time,
479 'timezone' => $timezone,
480 'meeting_timezone' => $meeting_timezone,
481 'location' => $location,
482 'location_type' => $location_type,
483 'duration' => $duration,
484 ] );
485
486 // Remove all items from cart.
487 WC()->cart->empty_cart();
488
489 // Preparing for add to cart.
490 $meeting = new Appointment( $meeting_id );
491 $product_id = $meeting->get_wc_product_id();
492
493
494 if ( ! $product_id ) {
495 $product = new Product();
496 $product_id = $product->create( $meeting );
497 }
498
499 $wc_product = wc_get_product( $product_id );
500 if ( ! $wc_product->get_price() ) {
501 update_post_meta( $product_id, '_regular_price', wc_format_decimal( $price ) );
502 update_post_meta( $product_id, '_price', wc_format_decimal( $price ) );
503 }
504
505 if ( ! $product_id ) {
506 return;
507 }
508
509 WC()->cart->add_to_cart( $product_id, $quantity );
510 }
511
512 /**
513 * Suport woocmmerce currency
514 *
515 * @param string $currency
516 *
517 * @return string
518 */
519 public function support_woocommerce_currency( $currency ) {
520 if ( ! timetics_is_woocommerce_active() ) {
521 return $currency;
522 }
523
524 return get_woocommerce_currency();
525 }
526
527 /**
528 * Create a category if woocommerce loaded
529 *
530 * @return void
531 */
532 public function create_term_timetics_meeting() {
533 if ( term_exists( 'timetics-meeting', 'product_cat' ) ) {
534 return;
535 }
536
537 // Create new woocommerce product category for timetics meeting
538 timetics_add_woocommerce_product_cat();
539 }
540
541 public function hide_regular_price_checkout_css() {
542 $session = WC()->session;
543 if ( (is_checkout() || is_cart()) && $session ) {
544 $timetics_data = $session->get('timetics_data');
545 if( isset($timetics_data) ) {
546 ?>
547 <style type="text/css">
548 .wc-block-components-order-summary .wc-block-components-order-summary-item__individual-prices {
549 display: none;
550 }
551 .wc-block-cart-item__prices,
552 .woocommerce-cart-form__cart-item.cart_item .product-price {
553 display: none;
554 }
555 .wc-block-cart-item__quantity,
556 .woocommerce-cart-form__cart-item.cart_item .product-quantity {
557 display: none;
558 }
559 </style>
560 <?php
561 }
562 }
563 }
564
565 /**
566 * Format and prepare meeting details
567 *
568 * @param array $session_data
569 *
570 * @return array|false
571 */
572 private function prepare_meeting_details( $session_data ) {
573 if ( ! $session_data || ! is_array( $session_data ) ) {
574 return false;
575 }
576
577 $start_date = isset( $session_data['start_date'] ) ? $session_data['start_date'] : '';
578 $start_time = isset( $session_data['start_time'] ) ? $session_data['start_time'] : '';
579 $customer_timezone = isset( $session_data['timezone'] ) ? $session_data['timezone'] : '';
580 $meeting_timezone = isset( $session_data['meeting_timezone'] ) ? $session_data['meeting_timezone'] : '';
581 $duration = isset( $session_data['duration'] ) ? $session_data['duration'] : '';
582 $location_type = isset( $session_data['location_type'] ) ? $session_data['location_type'] : '';
583
584 if ( ! $start_date || ! $start_time ) {
585 return false;
586 }
587
588 // Get WordPress date and time formats
589 $date_format = get_option( 'date_format', 'F j, Y' );
590 $time_format = get_option( 'time_format', 'g:i a' );
591
592 // Convert timezone if both customer and meeting timezones are available
593 $datetime_to_display = null;
594
595 if ( $customer_timezone && $meeting_timezone && timetics_is_valid_timezone( $customer_timezone ) && timetics_is_valid_timezone( $meeting_timezone ) ) {
596 $datetime_to_display = timetics_convert_timezone( $start_date . ' ' . $start_time, $customer_timezone, $meeting_timezone );
597 $display_timezone = $meeting_timezone;
598 } else {
599 $datetime_to_display = new \DateTime( $start_date . ' ' . $start_time );
600 $display_timezone = $customer_timezone ?: 'UTC';
601 }
602
603 $formatted_date = $datetime_to_display->format( $date_format );
604 $formatted_time = $datetime_to_display->format( $time_format );
605
606 // Build location type label
607 $location_label = '';
608 if ( $location_type ) {
609 $location_label = ucfirst( str_replace( '-', ' ', $location_type ) );
610 }
611
612 return [
613 'formatted_date' => $formatted_date,
614 'formatted_time' => $formatted_time,
615 'duration' => $duration,
616 'location_label' => $location_label,
617 'timezone' => $customer_timezone,
618 'display_timezone' => $display_timezone,
619 ];
620 }
621
622 /**
623 * Display meeting details on checkout page
624 *
625 * @return void
626 */
627 public function display_meeting_details_on_checkout() {
628 if ( ! is_checkout() ) {
629 return;
630 }
631
632 $session_data = WC()->session->get( 'timetics_data' );
633 $details = $this->prepare_meeting_details( $session_data );
634
635 if ( ! $details ) {
636 return;
637 }
638
639 $timetics_formatted_date = $details['formatted_date'];
640 $timetics_formatted_time = $details['formatted_time'];
641 $timetics_duration = $details['duration'];
642 $timetics_location_label = $details['location_label'];
643 $timetics_timezone = $details['timezone'];
644 $timetics_display_timezone = $details['display_timezone'];
645
646 include TIMETICS_PLUGIN_DIR . '/templates/woocommerce/meeting-details-checkout.php';
647 }
648
649 /**
650 * Sync booking status when WooCommerce order status changes
651 *
652 * @param int $order_id The WooCommerce order ID
653 * @param string $old_status The previous order status (without 'wc-' prefix)
654 * @param string $new_status The new order status (without 'wc-' prefix)
655 *
656 * @return void
657 */
658 public function sync_booking_status_from_order( $order_id, $old_status, $new_status ) {
659 // Get the booking ID from order meta
660 $booking_id = Status_Mapper::get_booking_id_from_order( $order_id );
661
662 if ( ! $booking_id ) {
663 return;
664 }
665
666 // Check if this pair is already syncing (prevents reverse sync infinite loop)
667 if ( Status_Mapper::is_syncing_pair( $booking_id, $order_id ) ) {
668 return;
669 }
670
671 // Check if sync is needed
672 if ( ! Status_Mapper::should_sync_status( $old_status, $new_status ) ) {
673 return;
674 }
675
676 // Mark this pair as syncing to prevent reverse sync
677 Status_Mapper::begin_sync_pair( $booking_id, $order_id );
678
679 try {
680 $booking = new Booking( $booking_id );
681
682 if ( ! $booking->is_booking() ) {
683 return;
684 }
685
686 // Map WooCommerce status to Timetics booking status
687 $booking_status = Status_Mapper::order_to_booking_status( $new_status );
688
689 // Get current booking status
690 $current_status = $booking->get_status();
691
692 // Only update if status has actually changed
693 if ( $current_status === $booking_status ) {
694 return;
695 }
696
697 // Update booking status
698 // This will trigger transition_post_status, but our pair lock prevents reverse sync
699 $booking->update( [ 'post_status' => $booking_status ] );
700
701 // Send cancellation emails if status changed to 'cancel'
702 if ( 'cancel' === $booking_status ) {
703 $booking->delete_event();
704
705 $is_email_to_customer = timetics_get_option( 'booking_canceled_customer' );
706 $is_email_to_host = timetics_get_option( 'booking_canceled_host' );
707
708 if ( $is_email_to_host ) {
709 $cancel_event_email = new Cancel_Event_Email( $booking );
710 $cancel_event_email->send();
711 }
712
713 if ( $is_email_to_customer ) {
714 $customer_cancel_event_email = new Cancel_Event_Customer_Email( $booking );
715 $customer_cancel_event_email->send();
716 }
717 }
718
719 } finally {
720 // Always clear the sync pair flag
721 Status_Mapper::end_sync_pair( $booking_id, $order_id );
722 }
723 }
724
725 /**
726 * Sync WooCommerce order status when booking status changes
727 *
728 * @param string $new_status The new booking status
729 * @param string $old_status The previous booking status
730 * @param WP_Post $post The booking post object
731 *
732 * @return void
733 */
734 public function sync_order_status_from_booking( $new_status, $old_status, $post ) {
735 // Only process timetics-booking posts
736 if ( 'timetics-booking' !== $post->post_type ) {
737 return;
738 }
739
740 $booking_id = $post->ID;
741
742 $order_id = Status_Mapper::get_order_id_from_booking( $booking_id );
743
744 if ( ! $order_id ) {
745 return;
746 }
747
748 if ( Status_Mapper::is_syncing_pair( $booking_id, $order_id ) ) {
749 return;
750 }
751
752 if ( ! Status_Mapper::should_sync_status( $old_status, $new_status ) ) {
753 return;
754 }
755
756 Status_Mapper::begin_sync_pair( $booking_id, $order_id );
757
758 try {
759 $order = wc_get_order( $order_id );
760
761 if ( ! $order ) {
762 return;
763 }
764
765 // Map Timetics booking status to WooCommerce order status
766 $wc_status = Status_Mapper::booking_to_order_status( $new_status );
767
768 // Get current order status (without 'wc-' prefix)
769 $current_wc_status = str_replace( 'wc-', '', $order->get_status() );
770
771 if ( $current_wc_status === $wc_status ) {
772 return;
773 }
774
775 $order->set_status( $wc_status );
776 $order->save();
777
778 } finally {
779 // Always clear the sync pair flag
780 Status_Mapper::end_sync_pair( $booking_id, $order_id );
781 }
782 }
783 }
784