PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / trunk
Timetics – Appointment Booking Calendar & Scheduling vtrunk
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 trunk, at core/integrations/woocommerce/hooks.php

809 lines 25.8 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 // A cancelled / refunded order is terminal, so release the slot it was
696 // holding and let it appear as free again.
697 //
698 // We deliberately do NOT release on a 'failed' order. A WooCommerce
699 // booking is created in the 'failed' state while awaiting payment, and
700 // a failed order is retryable — the customer can re-pay. Releasing on
701 // 'failed' would free the slot, and a successful retry
702 // ( failed -> processing/approved ) would then revive the booking
703 // without re-holding the slot, causing an overbooking. A genuinely
704 // abandoned order is cancelled by WooCommerce's own unpaid-order
705 // handling ( hold-stock timeout ), which maps to 'cancel' and releases
706 // here.
707 //
708 // This runs before the "status unchanged" short-circuit below so that a
709 // refund after an already-cancelled booking ( cancel -> cancel,
710 // unchanged ) still guarantees a release. release_slot() is idempotent.
711 if ( 'cancel' === $booking_status ) {
712 $booking->release_slot();
713 }
714
715 // Only update if status has actually changed
716 if ( $current_status === $booking_status ) {
717 return;
718 }
719
720 // Update booking status
721 // This will trigger transition_post_status, but our pair lock prevents reverse sync
722 $booking->update( [ 'post_status' => $booking_status ] );
723
724 // Send cancellation emails if status changed to 'cancel'
725 if ( 'cancel' === $booking_status ) {
726 $booking->delete_event();
727
728 $is_email_to_customer = timetics_get_option( 'booking_canceled_customer' );
729 $is_email_to_host = timetics_get_option( 'booking_canceled_host' );
730
731 if ( $is_email_to_host ) {
732 $cancel_event_email = new Cancel_Event_Email( $booking );
733 $cancel_event_email->send();
734 }
735
736 if ( $is_email_to_customer ) {
737 $customer_cancel_event_email = new Cancel_Event_Customer_Email( $booking );
738 $customer_cancel_event_email->send();
739 }
740
741 do_action( 'timetics_gln_hook', 'booking_canceled', Notification::get_hook_data( $booking ) );
742 }
743
744 } finally {
745 // Always clear the sync pair flag
746 Status_Mapper::end_sync_pair( $booking_id, $order_id );
747 }
748 }
749
750 /**
751 * Sync WooCommerce order status when booking status changes
752 *
753 * @param string $new_status The new booking status
754 * @param string $old_status The previous booking status
755 * @param WP_Post $post The booking post object
756 *
757 * @return void
758 */
759 public function sync_order_status_from_booking( $new_status, $old_status, $post ) {
760 // Only process timetics-booking posts
761 if ( 'timetics-booking' !== $post->post_type ) {
762 return;
763 }
764
765 $booking_id = $post->ID;
766
767 $order_id = Status_Mapper::get_order_id_from_booking( $booking_id );
768
769 if ( ! $order_id ) {
770 return;
771 }
772
773 if ( Status_Mapper::is_syncing_pair( $booking_id, $order_id ) ) {
774 return;
775 }
776
777 if ( ! Status_Mapper::should_sync_status( $old_status, $new_status ) ) {
778 return;
779 }
780
781 Status_Mapper::begin_sync_pair( $booking_id, $order_id );
782
783 try {
784 $order = wc_get_order( $order_id );
785
786 if ( ! $order ) {
787 return;
788 }
789
790 // Map Timetics booking status to WooCommerce order status
791 $wc_status = Status_Mapper::booking_to_order_status( $new_status );
792
793 // Get current order status (without 'wc-' prefix)
794 $current_wc_status = str_replace( 'wc-', '', $order->get_status() );
795
796 if ( $current_wc_status === $wc_status ) {
797 return;
798 }
799
800 $order->set_status( $wc_status );
801 $order->save();
802
803 } finally {
804 // Always clear the sync pair flag
805 Status_Mapper::end_sync_pair( $booking_id, $order_id );
806 }
807 }
808 }
809