PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.38
Timetics – Appointment Booking Calendar & Scheduling v1.0.38
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 1.0.22 All 64 releases
← All changes | core/integrations/woocommerce/hooks.php +5 -401 1.0.62 → 1.0.38 View file →
@@ -5,11 +5,8 @@
5 5 * @package Timetics
6 6 */
7 7 namespace Timetics\Core\Integrations\Woocommerce;
8 8
9 -defined( 'ABSPATH' ) || exit;
10 -
11 -use Timetics\Core\Admin\Notification;
12 9 use Timetics\Core\Appointments\Appointment;
13 10 use Timetics\Core\Bookings\Booking;
14 11 use Timetics\Core\Customers\Customer;
15 12 use Timetics\Utils\Singleton;
@@ -14,11 +11,8 @@
14 11 use Timetics\Core\Customers\Customer;
15 12 use Timetics\Utils\Singleton;
16 13 use Timetics\Core\Emails\New_Event_Email;
17 14 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 15
22 16 /**
23 17 * Class Hooks
24 18 */
@@ -36,21 +30,14 @@
36 30 add_action( 'woocommerce_init', [$this, 'load_woocommerce_required_files'] );
37 31
38 32 add_action( 'woocommerce_thankyou', [$this, 'update_booking_payment_status'] );
39 33
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 34 add_filter( 'timetics_get_settings', [$this, 'add_woocommerce_currency_support'] );
44 35
45 36 add_filter( 'woocommerce_product_query', [$this, 'modify_woocommerce_shop'] );
46 37
47 - add_action( 'woocommerce_coupon_options_usage_restriction', [$this, 'ensure_meeting_products_exist'], 5, 2 );
48 38 add_action( 'woocommerce_coupon_options_usage_restriction', [$this, 'add_meeting_coupon_restriction_option'], 10, 2 );
49 39
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 40 add_action( 'woocommerce_coupon_options_save', [$this, 'save_coupon_option'], 10, 2 );
54 41
55 42 add_filter( 'woocommerce_coupon_get_products', [$this, 'assign_meeting_coupon_products'], 10, 2 );
56 43
@@ -67,13 +54,8 @@
67 54 add_action( 'admin_init', [ $this, 'create_term_timetics_meeting' ] );
68 55
69 56 add_action( 'wp_head', [ $this, 'hide_regular_price_checkout_css' ] );
70 57
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 58 }
77 59
78 60 /**
79 61 * Load all required files and functions
@@ -114,14 +96,8 @@
114 96
115 97 $order = wc_get_order( $order_id );
116 98
117 99 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 100 WC()->session->set( 'timetics_data', null );
125 101 return;
126 102 }
127 103
@@ -132,21 +108,14 @@
132 108 }
133 109
134 110 $booking = new Booking( $session_data['booking_id'] );
135 111
136 - // Get the default booking status (e.g., 'approved')
137 - $default_booking_status = timetics_get_option( 'default_booking_status', 'approved' );
138 -
139 112 $booking->update( [
140 - 'post_status' => $default_booking_status,
113 + 'post_status' => 'completed',
141 114 'payment_status' => 'completed',
142 115 'payment_method' => 'woocommerce',
143 116 ] );
144 117
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 118 $booking->create_event();
150 119 $is_email_to_customer = timetics_get_option( 'booking_created_customer');
151 120 $is_email_to_host = timetics_get_option( 'booking_created_host');
152 121 if( $is_email_to_host ){
@@ -158,10 +127,8 @@
158 127 $new_event_customer_email = new New_Event_Customer_Email( $booking );
159 128 $new_event_customer_email->send();
160 129 }
161 130
162 - do_action( 'timetics_gln_hook', 'booking_created', Notification::get_hook_data( $booking ) );
163 -
164 131 WC()->session->set( 'timetics_data', null );
165 132
166 133 }
167 134
@@ -227,15 +194,10 @@
227 194 *
228 195 * @return void
229 196 */
230 197 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 - }
198 + $meeting_ids = ! empty( $_POST['timetics_meeting_ids'] ) ? array_map( 'intval', $_POST['timetics_meeting_ids'] ) : [];
235 199
236 - $meeting_ids = ! empty( $_POST['timetics_meeting_ids'] ) ? array_map( 'intval', wp_unslash( $_POST['timetics_meeting_ids'] ) ) : [];
237 -
238 200 $coupon_products = $coupon->get_product_ids();
239 201 $coupon_products = array_merge( $coupon_products, $meeting_ids );
240 202 $coupon->update_meta_data( 'timetics_meeting_ids', $meeting_ids );
241 203
@@ -255,99 +217,8 @@
255 217 include_once TIMETICS_PLUGIN_DIR . '/templates/woocommerce/coupon-restriction-option.php';
256 218 }
257 219
258 220 /**
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 221 * Add checkout url on settings
351 222 *
352 223 * @param array $settings
353 224 *
@@ -370,12 +241,8 @@
370 241 *
371 242 * @return array
372 243 */
373 244 public function hide_checkout_fields( $fields ) {
374 - if ( ! function_exists( 'WC' ) || ! WC()->session ) {
375 - return $fields;
376 - }
377 -
378 245 $session_data = WC()->session->get( 'timetics_data' );
379 246
380 247 if ( ! $session_data ) {
381 248 return $fields;
@@ -398,12 +265,8 @@
398 265 *
399 266 * @return array
400 267 */
401 268 public function modify_order_data( $data ) {
402 - if ( ! function_exists( 'WC' ) || ! WC()->session ) {
403 - return $data;
404 - }
405 -
406 269 $session_data = WC()->session->get( 'timetics_data' );
407 270
408 271 if ( ! $session_data ) {
409 272 return $data;
@@ -459,32 +322,14 @@
459 322 $quantity = 1;
460 323 }
461 324 $price = $booking->get_total();
462 325
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 326
474 327 // Set session for timetics data for woocommerce.
475 328 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,
329 + 'booking_id' => $booking_id,
330 + 'meeting_id' => $meeting_id,
331 + 'price' => $price,
487 332 ] );
488 333
489 334 // Remove all items from cart.
490 335 WC()->cart->empty_cart();
@@ -561,248 +406,7 @@
561 406 }
562 407 </style>
563 408 <?php
564 409 }
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 410 }
807 411 }
808 412 }