PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.59
Timetics – Appointment Booking Calendar & Scheduling v1.0.59
1.0.64 1.0.62 1.0.63 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 All 65 releases
timetics / core / integrations / woocommerce / hooks.php

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

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