PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
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
← All changes | core/integrations/woocommerce/hooks.php +140 -18 1.0.57 → 1.0.64 View file →
@@ -7,8 +7,9 @@
7 7 namespace Timetics\Core\Integrations\Woocommerce;
8 8
9 9 defined( 'ABSPATH' ) || exit;
10 10
11 +use Timetics\Core\Admin\Notification;
11 12 use Timetics\Core\Appointments\Appointment;
12 13 use Timetics\Core\Bookings\Booking;
13 14 use Timetics\Core\Customers\Customer;
14 15 use Timetics\Utils\Singleton;
@@ -66,11 +67,14 @@
66 67 add_action( 'admin_init', [ $this, 'create_term_timetics_meeting' ] );
67 68
68 69 add_action( 'wp_head', [ $this, 'hide_regular_price_checkout_css' ] );
69 70
70 - // Display meeting details on checkout page
71 - add_action( 'woocommerce_review_order_before_payment', [ $this, 'display_meeting_details_on_checkout' ] );
71 + // Show meeting details under the product in the cart and on checkout
72 + add_filter( 'woocommerce_get_item_data', [ $this, 'add_meeting_details_to_cart_item' ], 10, 2 );
72 73
74 + // Store meeting details on the order line item ( thank you page, emails, my account, admin )
75 + add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'add_meeting_details_to_order_item' ], 10, 4 );
76 +
73 77 // Sync order status when booking status changes (bidirectional sync)
74 78 add_action( 'transition_post_status', [ $this, 'sync_order_status_from_booking' ], 10, 3 );
75 79 }
76 80
@@ -116,9 +120,8 @@
116 120 if ( ! $order->is_paid() ) {
117 121 // Mark booking as failed if payment was not completed
118 122 $session_data = WC()->session->get( 'timetics_data' );
119 123 if ( $session_data ) {
120 - $booking = new Booking( $session_data['booking_id'] );
121 124 Status_Mapper::set_order_id_for_booking( $session_data['booking_id'], $order_id );
122 125 }
123 126 WC()->session->set( 'timetics_data', null );
124 127 return;
@@ -157,8 +160,10 @@
157 160 $new_event_customer_email = new New_Event_Customer_Email( $booking );
158 161 $new_event_customer_email->send();
159 162 }
160 163
164 + do_action( 'timetics_gln_hook', 'booking_created', Notification::get_hook_data( $booking ) );
165 +
161 166 WC()->session->set( 'timetics_data', null );
162 167
163 168 }
164 169
@@ -619,32 +624,127 @@
619 624 ];
620 625 }
621 626
622 627 /**
623 - * Display meeting details on checkout page
628 + * Meeting details as label => value pairs, ready to display.
624 629 *
625 - * @return void
630 + * Shared by the cart / checkout item data and the order line item meta so the
631 + * customer sees the same details before and after paying.
632 + *
633 + * @param array $session_data
634 + *
635 + * @return array
626 636 */
627 - public function display_meeting_details_on_checkout() {
628 - if ( ! is_checkout() ) {
629 - return;
637 + private function get_meeting_detail_pairs( $session_data ) {
638 + $details = $this->prepare_meeting_details( $session_data );
639 +
640 + if ( ! $details ) {
641 + return [];
630 642 }
631 643
632 - $session_data = WC()->session->get( 'timetics_data' );
633 - $details = $this->prepare_meeting_details( $session_data );
644 + $time = $details['formatted_time'];
634 645
635 - if ( ! $details ) {
646 + if ( $time && $details['display_timezone'] ) {
647 + $time .= ' (' . $details['display_timezone'] . ')';
648 + }
649 +
650 + return array_filter( [
651 + __( 'Date', 'timetics' ) => $details['formatted_date'],
652 + __( 'Time', 'timetics' ) => $time,
653 + __( 'Duration', 'timetics' ) => $details['duration'],
654 + __( 'Location', 'timetics' ) => $details['location_label'],
655 + __( 'Timezone', 'timetics' ) => $details['timezone'],
656 + ] );
657 + }
658 +
659 + /**
660 + * Whether the given product is the meeting product this session booked.
661 + *
662 + * The cart can hold other products, so the details must only be attached to
663 + * the meeting line.
664 + *
665 + * @param array $session_data
666 + * @param integer $product_id
667 + *
668 + * @return bool
669 + */
670 + private function is_meeting_product( $session_data, $product_id ) {
671 + if ( empty( $session_data['meeting_id'] ) ) {
672 + return false;
673 + }
674 +
675 + $meeting = new Appointment( $session_data['meeting_id'] );
676 +
677 + return (int) $meeting->get_wc_product_id() === (int) $product_id;
678 + }
679 +
680 + /**
681 + * Show meeting details under the product in the cart and on checkout.
682 + *
683 + * WooCommerce runs this filter for both the classic templates and the Store API,
684 + * so one filter covers the classic and the block based cart / checkout. The block
685 + * checkout is the default since WooCommerce 8.3, and the old
686 + * woocommerce_review_order_before_payment hook this replaced never ran there.
687 + *
688 + * @param array $item_data
689 + * @param array $cart_item
690 + *
691 + * @return array
692 + */
693 + public function add_meeting_details_to_cart_item( $item_data, $cart_item ) {
694 + $session_data = WC()->session ? WC()->session->get( 'timetics_data' ) : null;
695 +
696 + if ( ! $session_data || ! $this->is_meeting_product( $session_data, $cart_item['product_id'] ) ) {
697 + return $item_data;
698 + }
699 +
700 + foreach ( $this->get_meeting_detail_pairs( $session_data ) as $label => $value ) {
701 + $item_data[] = [
702 + 'key' => $label,
703 + 'value' => $value,
704 + ];
705 + }
706 +
707 + return $item_data;
708 + }
709 +
710 + /**
711 + * Store meeting details as order line item meta.
712 + *
713 + * The booking details only live in the WooCommerce session, which is cleared
714 + * once the order is placed. Copying them onto the line item makes WooCommerce
715 + * render them everywhere an order is shown: the thank you page, the customer
716 + * and admin order emails, My Account -> View order, and the admin order screen.
717 + *
718 + * @param \WC_Order_Item_Product $item
719 + * @param string $cart_item_key
720 + * @param array $values
721 + * @param \WC_Order $order
722 + *
723 + * @return void
724 + */
725 + public function add_meeting_details_to_order_item( $item, $cart_item_key, $values, $order ) {
726 + $session_data = WC()->session ? WC()->session->get( 'timetics_data' ) : null;
727 +
728 + if ( ! $session_data || ! $this->is_meeting_product( $session_data, $item->get_product_id() ) ) {
636 729 return;
637 730 }
638 731
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'];
732 + foreach ( $this->get_meeting_detail_pairs( $session_data ) as $label => $value ) {
733 + $item->add_meta_data( $label, $value, true );
734 + }
645 735
646 - include TIMETICS_PLUGIN_DIR . '/templates/woocommerce/meeting-details-checkout.php';
736 + // Link the order back to the booking here rather than on woocommerce_thankyou.
737 + // The thank you page only runs for a customer who actually lands on it and, for
738 + // an unpaid order, only stored the booking -> order half of the link. Without the
739 + // order -> booking half, sync_booking_status_from_order() finds no booking and an
740 + // admin later marking the order paid or cancelled silently does nothing.
741 + //
742 + // The order is not saved here on purpose: in the classic checkout the order has no
743 + // ID yet at this point. WooCommerce saves it right after building the line items.
744 + if ( ! empty( $session_data['booking_id'] ) ) {
745 + $order->update_meta_data( '_tt_booking_id', $session_data['booking_id'] );
746 + }
647 747 }
648 748
649 749 /**
650 750 * Sync booking status when WooCommerce order status changes
@@ -688,8 +788,28 @@
688 788
689 789 // Get current booking status
690 790 $current_status = $booking->get_status();
691 791
792 + // A cancelled / refunded order is terminal, so release the slot it was
793 + // holding and let it appear as free again.
794 + //
795 + // We deliberately do NOT release on a 'failed' order. A WooCommerce
796 + // booking is created in the 'failed' state while awaiting payment, and
797 + // a failed order is retryable — the customer can re-pay. Releasing on
798 + // 'failed' would free the slot, and a successful retry
799 + // ( failed -> processing/approved ) would then revive the booking
800 + // without re-holding the slot, causing an overbooking. A genuinely
801 + // abandoned order is cancelled by WooCommerce's own unpaid-order
802 + // handling ( hold-stock timeout ), which maps to 'cancel' and releases
803 + // here.
804 + //
805 + // This runs before the "status unchanged" short-circuit below so that a
806 + // refund after an already-cancelled booking ( cancel -> cancel,
807 + // unchanged ) still guarantees a release. release_slot() is idempotent.
808 + if ( 'cancel' === $booking_status ) {
809 + $booking->release_slot();
810 + }
811 +
692 812 // Only update if status has actually changed
693 813 if ( $current_status === $booking_status ) {
694 814 return;
695 815 }
@@ -713,8 +833,10 @@
713 833 if ( $is_email_to_customer ) {
714 834 $customer_cancel_event_email = new Cancel_Event_Customer_Email( $booking );
715 835 $customer_cancel_event_email->send();
716 836 }
837 +
838 + do_action( 'timetics_gln_hook', 'booking_canceled', Notification::get_hook_data( $booking ) );
717 839 }
718 840
719 841 } finally {
720 842 // Always clear the sync pair flag