PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 2.3.2
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v2.3.2
2.3.2 2.3.1 2.3.0 2.2.8 2.2.7 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.11.0 1.11.1 1.11.2 1.6 1.7 1.8 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 1.9.10 1.9.11 All 60 releases
← All changes | inc/modules/pre-orders/class-pre-orders-main-functionality.php +413 -214 1.10.42.3.2 View file →
@@ -34,8 +34,13 @@
34 34 */
35 35 private $is_pre_order_filter_on = false;
36 36
37 37 /**
38 + * @var string The module ID.
39 + */
40 + private $module_id = 'pre-orders';
41 +
42 + /**
38 43 * Init.
39 44 *
40 45 * @return void
41 46 */
@@ -41,10 +46,12 @@
41 46 */
42 47 public function init() {
43 48 add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'allow_one_type_only' ), 99, 2 );
44 49 add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 4 );
50 + add_filter( 'woocommerce_add_cart_item_data', array( $this, 'record_add_to_cart_event' ), 11, 2 );
51 + add_action( 'woocommerce_after_calculate_totals', array( $this, 'update_analytics' ) );
45 52 add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) );
46 - add_action( 'woocommerce_add_order_item_meta', array( $this, 'add_order_item_meta' ), 10, 2 );
53 + add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_order_item_meta' ), 10, 4 );
47 54
48 55 // Cronjob.
49 56 if ( ! wp_next_scheduled( 'check_for_released_preorders' ) ) {
50 57 wp_schedule_event( time(), 'twicedaily', 'check_for_released_preorders' );
@@ -53,18 +60,31 @@
53 60 add_action( 'check_for_released_preorders', array( $this, 'check_for_pre_orders_and_maybe_update_status' ) );
54 61
55 62 add_filter( 'woocommerce_product_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
56 63 add_filter( 'woocommerce_product_single_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
57 - add_filter( 'woocommerce_available_variation', array( $this, 'change_button_text_for_variable_products' ), 10, 3 );
64 + add_filter( 'woocommerce_available_variation', array( $this, 'attach_pre_order_data_to_variations' ), 10, 3 );
58 65 add_action( 'woocommerce_before_add_to_cart_form', array( $this, 'additional_information_before_cart_form' ) );
59 66 add_action( 'woocommerce_after_add_to_cart_form', array( $this, 'additional_information_after_cart_form' ) );
67 + add_action( 'woocommerce_before_add_to_cart_form', array( $this, 'record_pre_order_impression' ) );
60 68
61 69 // Cart
62 70 add_filter( 'woocommerce_get_item_data', array( $this, 'cart_message_handler' ), 10, 2 );
63 71
72 + /**
73 + * Hook 'merchant_is_pre_order_product' Checks if the product is a pre-order.
74 + *
75 + * @param int $product_id The product ID.
76 + *
77 + * @since 2.1.9
78 + */
79 + add_filter('merchant_is_pre_order_product', array( $this, 'is_pre_order' ), 10, 1 );
80 +
64 81 add_action( 'woocommerce_order_item_meta_end', array( $this, 'order_item_meta_end' ), 10, 4 );
65 82 add_action( 'woocommerce_shop_loop_item_title', array( $this, 'shop_loop_item_title' ) );
66 83
84 + add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'create_order_line_item' ), 10, 3 );
85 + add_action( 'woocommerce_checkout_order_created', array( $this, 'log_order_event' ) );
86 +
67 87 // Products block.
68 88 add_filter( 'render_block_context', array( $this, 'add_block_title_filter' ), 10, 1 );
69 89 add_filter( 'woocommerce_blocks_product_grid_item_html', array( $this, 'override_product_grid_block' ), PHP_INT_MAX, 3 );
70 90
@@ -135,15 +155,68 @@
135 155 * @return string The human readable date.
136 156 */
137 157 private function convert_timestamp_to_human_readable( $timestamp ) {
138 158 $timezone = new DateTimeZone( merchant_timezone() );
139 - $date = new \DateTime( 'now', $timezone );
140 - $date->setTimestamp( $timestamp );
159 + $date = new \DateTime( '@' . $timestamp ); // Create from UTC timestamp
160 + $date->setTimezone( $timezone ); // Convert to site timezone
141 161
142 162 return $date->format( self::DATE_TIME_FORMAT );
143 163 }
144 164
145 165 /**
166 + * Record add to cart event.
167 + *
168 + * @param $cart_item_data array Cart item data
169 + * @param $product_id int Product ID
170 + *
171 + * @return mixed
172 + */
173 + public function record_add_to_cart_event( $cart_item_data, $product_id ) {
174 + $user_id = WC()->session->get_customer_id();
175 + $analytics = new Merchant_Analytics_Logger();
176 + $analytics->set_user_id( $user_id );
177 + $module_id = $this->module_id;
178 + $impression_event = $analytics->get_product_module_last_impression( $module_id, $product_id );
179 + $existing_add_to_cart_event = $analytics->get_event_by(
180 + array(
181 + 'event_type' => 'add_to_cart',
182 + 'source_product_id' => $product_id,
183 + 'customer_id' => $user_id,
184 + 'module_id' => $this->module_id,
185 + )
186 + );
187 +
188 + $args = array(
189 + 'event_type' => 'add_to_cart',
190 + 'source_product_id' => $product_id,
191 + 'customer_id' => $user_id,
192 + 'campaign_id' => isset( $impression_event['campaign_id'] ) ? $impression_event['campaign_id'] : '',
193 + 'related_event_id' => isset( $impression_event['id'] ) ? $impression_event['id'] : '',
194 + 'module_id' => $this->module_id,
195 + 'meta_data' => isset( $impression_event['meta_data'] ) ? $impression_event['meta_data'] : '',
196 + );
197 +
198 + if ( null !== $existing_add_to_cart_event ) {
199 + // Check if this appeared in order event
200 + $appeared_in_order = $analytics->get_event_by( array(
201 + 'event_type' => 'order',
202 + 'source_product_id' => $product_id,
203 + 'customer_id' => $user_id,
204 + 'module_id' => $this->module_id,
205 + 'related_event_id' => $existing_add_to_cart_event['id'],
206 + ) );
207 +
208 + // Set the pre_order_add_to_cart_event_id based on whether the event exists
209 + $cart_item_data['pre_order_add_to_cart_event_id'] = null !== $appeared_in_order ? $analytics->log_event( $args ) : $existing_add_to_cart_event['id'];
210 + } else {
211 + // Log a new add to cart event if no previous event exists
212 + $cart_item_data['pre_order_add_to_cart_event_id'] = $analytics->log_event( $args );
213 + }
214 +
215 + return $cart_item_data;
216 + }
217 +
218 + /**
146 219 * Add offer details to the product.
147 220 *
148 221 * @param $cart_item_data array The cart item data.
149 222 * @param $product_id int The product ID.
@@ -166,22 +239,51 @@
166 239 return $cart_item_data;
167 240 }
168 241
169 242 /**
243 + * Update analytics.
244 + *
245 + * @param $cart WC_Cart Cart object
246 + *
247 + * @return void
248 + */
249 + public function update_analytics( $cart ) {
250 + $cart_items = $cart->get_cart();
251 + $user_id = WC()->session->get_customer_id();
252 + $analytics = new Merchant_Analytics_Logger();
253 + $analytics->set_user_id( $user_id );
254 + foreach ( $cart_items as $cart_item_key => $cart_item_data ) {
255 + if (
256 + isset( $cart_item_data['pre_order_add_to_cart_event_id'], $cart_item_data['_merchant_pre_order'] )
257 + && $analytics->event_exists( $cart_item_data['pre_order_add_to_cart_event_id'] )
258 + ) {
259 + $analytics->update_event( $cart_item_data['pre_order_add_to_cart_event_id'], array(
260 + 'campaign_id' => $cart_item_data['_merchant_pre_order']['flexible_id'],
261 + 'campaign_cost' => $cart_item_data['line_total'],
262 + 'meta_data' => wp_json_encode( $cart_item_data['_merchant_pre_order'] ),
263 + ) );
264 + }
265 + }
266 + }
267 +
268 + /**
170 269 * Add pre-order data to order item meta.
171 270 *
172 - * @param $item_id int The item ID.
271 + * @param $item WC_Order_Item The order item object.
272 + * @param $cart_item_key string The cart item key.
173 273 * @param $values array The values.
274 + * @param $order WC_Order The order object.
174 275 *
175 276 * @return void
277 + * @throws Exception
176 278 */
177 - public function add_order_item_meta( $item_id, $values ) {
279 + public function add_order_item_meta( $item, $cart_item_key, $values, $order ) {
178 280 if ( isset( $values['_merchant_pre_order'] ) ) {
179 - wc_add_order_item_meta( $item_id, '_merchant_pre_order', $values['_merchant_pre_order'] );
281 + $item->update_meta_data( '_merchant_pre_order', $values['_merchant_pre_order'] );
180 282 }
181 283
182 284 if ( isset( $values['_merchant_pre_order_shipping_date'] ) ) {
183 - wc_add_order_item_meta( $item_id, '_merchant_pre_order_shipping_date', $values['_merchant_pre_order_shipping_date'] );
285 + $item->update_meta_data( '_merchant_pre_order_shipping_date', $values['_merchant_pre_order_shipping_date'] );
184 286 }
185 287 }
186 288
187 289 /**
@@ -488,20 +590,25 @@
488 590 if ( apply_filters( 'merchant_pre_order_disable_sale_price_html', false, $html_price, $product ) ) {
489 591 return $html_price;
490 592 }
491 593
492 - if ( ! is_admin() && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
594 + if ( ( ! is_admin() || wp_doing_ajax() ) && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
493 595 if ( '' === $product->get_price() ) {
494 596 return $html_price;
495 597 }
496 598
497 599 $offer = self::available_product_rule( $product->get_id() );
600 +
498 601 if ( empty( $offer ) && $product->is_type( 'variation' ) ) {
499 602 $offer = self::available_product_rule( $product->get_parent_id() );
603 + $is_excluded = Merchant_Pre_Orders_Rules_Repository::is_product_excluded( $product->get_id(), $offer );
604 + if ( $is_excluded ) {
605 + $offer = array();
606 + }
500 607 }
501 608
502 609 if ( $product->is_type( 'variable' ) ) {
503 - $html_price = $this->variable_product_price_html( $product );
610 + $html_price = $this->variable_product_price_html( $product, $offer, $html_price );
504 611 } else {
505 612 $html_price = $this->simple_product_price_html( $product, $offer, $html_price );
506 613 }
507 614 }
@@ -515,43 +622,85 @@
515 622 * @param WC_Product $product The product object.
516 623 *
517 624 * @return string The price html.
518 625 */
519 - private function variable_product_price_html( $product ) {
626 + private function variable_product_price_html( $product, $offer, $html_price ) {
627 + $sale = Merchant_Pre_Orders_Rules_Repository::get_rule_sale( $offer );
628 + if ( ! $sale ) {
629 + return $html_price;
630 + }
631 +
520 632 $prices = array();
521 633 $variations = $product->get_children();
634 +
635 + $regular_prices = array();
636 + $sale_prices = array();
637 +
522 638 foreach ( $variations as $variation_id ) {
523 639 $variation = wc_get_product( $variation_id );
524 640 $variation_offer = self::available_product_rule( $variation_id );
525 - $regular_price = $variation->get_regular_price();
641 + $regular_price = $variation->get_regular_price();
642 +
643 + $regular_prices[] = $regular_price;
644 +
526 645 if ( empty( $variation_offer ) ) {
646 + $variation_offer = self::available_product_rule( $product->get_id() );
647 +
648 + $is_excluded = Merchant_Pre_Orders_Rules_Repository::is_product_excluded( $variation_id, $variation_offer );
649 + if ( $is_excluded ) {
650 + $prices[] = $regular_price;
651 + $sale_prices[] = $regular_price;
652 + continue;
653 + }
654 + }
655 +
656 + if ( empty( $variation_offer ) ) {
527 657 if ( $variation->is_on_sale() ) {
528 - $sale_price = $variation->get_sale_price();
658 + $sale_price = wc_get_price_to_display( $variation );
529 659 } else {
530 660 $sale_price = $regular_price;
531 661 }
532 - $prices[] = $sale_price;
662 + $prices[] = $sale_price;
663 + $sale_prices[] = $sale_price;
533 664 continue;
534 665 }
666 +
535 667 $sale_price = $this->calculate_discounted_price( $regular_price, $variation_offer, $variation );
536 668 if ( $sale_price <= 0 ) {
537 669 // If the price is less than 0, set it to the regular/sale price
538 670 if ( $variation->is_on_sale() ) {
539 - $sale_price = $variation->get_sale_price();
671 + $sale_price = wc_get_price_to_display( $variation );
540 672 } else {
541 673 $sale_price = $regular_price;
542 674 }
543 675 }
544 - $prices[] = $sale_price;
676 + $prices[] = $sale_price;
677 + $sale_prices[] = $sale_price;
545 678 }
546 679
547 - $min_price = min( $prices );
548 - $max_price = max( $prices );
549 - if ( $min_price !== $max_price ) {
550 - return wc_format_price_range( $min_price, $max_price );
680 + $regular_prices = array_unique( $regular_prices );
681 + $sale_prices = array_unique( $sale_prices );
682 +
683 + if ( 1 === count( $regular_prices ) && 1 === count( $sale_prices ) ) {
684 + $regular_price = reset( $regular_prices );
685 + $sale_price = reset( $sale_prices );
686 + return wc_format_sale_price( $regular_price, $sale_price );
551 687 }
552 688
553 - return wc_price( $min_price );
689 + // Ensure $prices is not empty
690 + if ( ! empty( $prices ) ) {
691 + $min_price = min( $prices );
692 + $max_price = max( $prices );
693 +
694 + if ( $min_price !== $max_price ) {
695 + return wc_format_price_range( $min_price, $max_price );
696 + }
697 +
698 + return wc_price( $min_price );
699 + }
700 +
701 + // If $prices is empty.
702 + return '';
554 703 }
555 704
556 705 /**
557 706 * Simple product price html.
@@ -561,13 +710,13 @@
561 710 *
562 711 * @return string The price html.
563 712 */
564 713 private function simple_product_price_html( $product, $offer, $html_price ) {
565 - $sale = self::get_rule_sale( $offer );
714 + $sale = Merchant_Pre_Orders_Rules_Repository::get_rule_sale( $offer );
566 715 if ( ! $sale ) {
567 716 return $html_price;
568 717 }
569 - $regular_price = $product->get_regular_price();
718 + $regular_price = wc_get_price_to_display( $product, array( 'price' => $product->get_regular_price() ) );
570 719 $discounted_price = $this->calculate_discounted_price( $regular_price, $offer, $product );
571 720
572 721 return wc_format_sale_price( $regular_price, $discounted_price );
573 722 }
@@ -581,9 +730,9 @@
581 730 *
582 731 * @return float The discounted price.
583 732 */
584 733 public function calculate_discounted_price( $price, $offer, $product = null ) {
585 - $sale = self::get_rule_sale( $offer );
734 + $sale = Merchant_Pre_Orders_Rules_Repository::get_rule_sale( $offer );
586 735 $discount_type = $discount_value = '';
587 736 if ( $sale ) {
588 737 $discount_type = $offer['discount_type'];
589 738 $discount_value = $offer['discount_amount'];
@@ -637,10 +786,12 @@
637 786 continue;
638 787 }
639 788 $product_id = $cart_item['data']->get_id();
640 789 $product = wc_get_product( $product_id );
641 - if ( ! is_admin() && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
790 +
791 + if ( ( ! is_admin() || wp_doing_ajax() ) && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
642 792 $offer = self::available_product_rule( $product->get_id() );
793 +
643 794 if ( $product->is_type( 'variable' ) ) {
644 795 $product_id = $cart_item['variation_id'];
645 796 $product = wc_get_product( $product_id );
646 797 // check if the variation has an offer
@@ -645,8 +796,18 @@
645 796 $product = wc_get_product( $product_id );
646 797 // check if the variation has an offer
647 798 $offer = self::available_product_rule( $product_id );
648 799 }
800 +
801 + if ( empty( $offer ) && $product->is_type( 'variation' ) ) {
802 + $offer = self::available_product_rule( $product->get_parent_id() );
803 +
804 + $is_excluded = Merchant_Pre_Orders_Rules_Repository::is_product_excluded( $product_id, $offer );
805 + if ( $is_excluded ) {
806 + continue;
807 + }
808 + }
809 +
649 810 if ( empty( $offer ) ) {
650 811 continue;
651 812 }
652 813 if ( ! isset( $offer['discount_toggle'] ) ) {
@@ -653,8 +814,23 @@
653 814 continue;
654 815 }
655 816 $regular_price = $product->get_regular_price();
656 817 $sale_price = $this->calculate_discounted_price( $regular_price, $offer, $product );
818 + /**
819 + * Filter the discounted price.
820 + *
821 + * @param float $sale_price The discounted price.
822 + * @param array $cart_item The cart item.
823 + * @param string $cart_item_key The cart item key.
824 + *
825 + * @since 2.1.3
826 + */
827 + $sale_price = apply_filters(
828 + 'merchant_pre_order_cart_sale_price',
829 + $sale_price,
830 + $cart_item,
831 + $cart_item_key
832 + );
657 833 $cart_item['data']->set_price( $sale_price );
658 834 $cart_item['data']->set_meta_data( array( '_merchant_pre_order_sale' => $offer ) );
659 835 }
660 836 }
@@ -770,18 +946,22 @@
770 946
771 947 /**
772 948 * Change pre-order button text for variable products.
773 949 *
774 - * @param array $data
775 - * @param object $product
776 - * @param object $variation
950 + * @param array $data variation data.
951 + * @param WC_Product $product The product object.
952 + * @param WC_Product_Variation $variation The variation object.
777 953 *
778 954 * @return array
779 955 */
780 - public function change_button_text_for_variable_products( $data, $product, $variation ) {
781 - if ( $this->is_pre_order( $variation->get_id() ) ) {
782 - $pre_order_rule = self::available_product_rule( $variation->get_id() );
783 - $data['is_pre_order'] = true;
956 + public function attach_pre_order_data_to_variations( $data, $product, $variation ) {
957 + $pre_order_rule = self::available_product_rule( $variation->get_id() );
958 + if ( empty( $pre_order_rule ) ) {
959 + $pre_order_rule = self::available_product_rule( $product->get_id() );
960 + }
961 + if ( ! empty( $pre_order_rule ) ) {
962 + $data['is_pre_order'] = true;
963 + $data['pre_order_placement'] = isset( $pre_order_rule['placement'] ) ? $pre_order_rule['placement'] : 'before';
784 964
785 965 $additional_text = $pre_order_rule['additional_text'] ? Merchant_Translator::translate( $pre_order_rule['additional_text'] )
786 966 : esc_html__( 'Ships on {date}.', 'merchant' );
787 967 $time_format = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
@@ -839,8 +1019,68 @@
839 1019 }
840 1020 }
841 1021
842 1022 /**
1023 + * Record pre-order impression on product page view.
1024 + *
1025 + * Hooked to `woocommerce_before_add_to_cart_form` independently of placement,
1026 + * so impressions are tracked regardless of where the pre-order message renders.
1027 + *
1028 + * @return void
1029 + */
1030 + public function record_pre_order_impression() {
1031 + $input_post_data = array(
1032 + 'product_id' => filter_input( INPUT_POST, 'product_id', FILTER_SANITIZE_NUMBER_INT ),
1033 + );
1034 +
1035 + global $post, $product;
1036 +
1037 + $_post = $post;
1038 + $_product = $product;
1039 +
1040 + if ( ! $_post && isset( $input_post_data['product_id'] ) ) {
1041 + $_post = get_post( absint( $input_post_data['product_id'] ) );
1042 + $_product = wc_get_product( $_post->ID );
1043 + }
1044 +
1045 + if ( ! $_post || ! $_product ) {
1046 + return;
1047 + }
1048 +
1049 + if ( $this->is_pre_order( $_post->ID ) ) {
1050 + $pre_order_rule = self::available_product_rule( $_product->get_id() );
1051 + if ( ! empty( $pre_order_rule ) ) {
1052 + $this->record_impression( $_product->get_id(), $pre_order_rule );
1053 + }
1054 + }
1055 + }
1056 +
1057 + /**
1058 + * Record impression.
1059 + *
1060 + * @return void
1061 + */
1062 + public function record_impression( $product_id, $campaign ) {
1063 + $analytics = new Merchant_Analytics_Logger();
1064 + $campaign_id = isset( $campaign['flexible_id'] ) ? $campaign['flexible_id'] : 0;
1065 + $module_id = $this->module_id;
1066 + $user_id = WC()->session->get_customer_id();
1067 + $analytics->set_user_id( $user_id );
1068 + $args = array(
1069 + 'source_product_id' => $product_id,
1070 + 'event_type' => 'impression',
1071 + 'related_event_id' => 0,
1072 + 'module_id' => $module_id,
1073 + 'campaign_id' => $campaign_id,
1074 + 'meta_data' => wp_json_encode( $campaign ),
1075 + );
1076 +
1077 + if ( $analytics->get_product_module_last_impression( $module_id, $product_id ) === null ) {
1078 + $analytics->log_event( $args );
1079 + }
1080 + }
1081 +
1082 + /**
843 1083 * Display pre order additional information after add to cart form.
844 1084 *
845 1085 * @return void
846 1086 */
@@ -938,10 +1178,18 @@
938 1178 */
939 1179 public function cart_message_handler( $item_data, $cart_item ) {
940 1180 $product_id = $cart_item['product_id'];
941 1181 if ( $cart_item['data']->is_type( 'variation' ) ) {
942 - $product_id = $cart_item['variation_id'];
1182 + $variation_id = $cart_item['variation_id'];
1183 + $parent_id = $cart_item['data']->get_parent_id();
1184 +
1185 + if ( ! $this->is_pre_order( $variation_id ) && $this->is_pre_order( $parent_id ) ) {
1186 + $product_id = $parent_id;
1187 + } else {
1188 + $product_id = $variation_id;
1189 + }
943 1190 }
1191 +
944 1192 if ( $this->is_pre_order( $product_id ) ) {
945 1193 $pre_order_rule = self::available_product_rule( $product_id );
946 1194 if ( $pre_order_rule ) {
947 1195 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
@@ -969,9 +1217,11 @@
969 1217 *
970 1218 * @return string
971 1219 */
972 1220 public function order_item_meta_end( $item_id, $item, $order, $plain_text ) {
973 - echo $this->get_pre_order_text( $item->get_product()->get_id(), $plain_text ? '' : 'dl' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1221 + if( $item->get_product() ) {
1222 + echo $this->get_pre_order_text( $item->get_product()->get_id(), $plain_text ? '' : 'dl' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1223 + }
974 1224 }
975 1225
976 1226 /**
977 1227 * Render pre order text.
@@ -980,8 +1230,72 @@
980 1230 echo $this->get_pre_order_text( get_the_ID(), 'span' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
981 1231 }
982 1232
983 1233 /**
1234 + * Log order event.
1235 + * Only runs if bundle main product.
1236 + *
1237 + * @param $order WC_Order instance
1238 + *
1239 + * @return void
1240 + */
1241 + public function log_order_event( $order ) {
1242 + $module_id = $this->module_id;
1243 + $user_id = WC()->session->get_customer_id();
1244 + $analytics = new Merchant_Analytics_Logger();
1245 + $analytics->set_user_id( $user_id );
1246 + foreach ( $order->get_items() as $order_item ) {
1247 + $analytics_id = $order_item->get_meta( '_pre_order_add_to_cart_event_id' );
1248 + if ( ! empty( $analytics_id ) ) {
1249 + $add_to_cart_event_id = $analytics_id;
1250 + $event_data = $analytics->get_event( $add_to_cart_event_id );
1251 + $campaign_cost = ! empty( $event_data['campaign_cost'] ) ? $event_data['campaign_cost'] : 0;
1252 + $args = array(
1253 + 'source_product_id' => $order_item->get_product_id(),
1254 + 'event_type' => 'order',
1255 + 'related_event_id' => $add_to_cart_event_id,
1256 + 'campaign_cost' => $campaign_cost,
1257 + 'campaign_id' => ! empty( $event_data['campaign_id'] ) ? $event_data['campaign_id'] : 0,
1258 + 'module_id' => $module_id,
1259 + 'order_id' => $order->get_id(),
1260 + 'order_subtotal' => $order->get_subtotal(),
1261 + 'meta_data' => ! empty( $event_data['meta_data'] ) ? $event_data['meta_data'] : '',
1262 + );
1263 + if (
1264 + null === $analytics->get_event_by(
1265 + array(
1266 + 'source_product_id' => $order_item->get_product_id(),
1267 + 'event_type' => 'order',
1268 + 'related_event_id' => $add_to_cart_event_id,
1269 + 'module_id' => $module_id,
1270 + 'order_id' => $order->get_id(),
1271 + 'customer_id' => $user_id,
1272 + )
1273 + )
1274 + ) {
1275 + $analytics->log_event( $args );
1276 + }
1277 + }
1278 + }
1279 + }
1280 +
1281 + /**
1282 + * Add order item meta data.
1283 + *
1284 + * @param $order_item WC_Order_Item_Product instance
1285 + * @param $cart_item_key string Cart item key
1286 + * @param $values array Cart item values
1287 + *
1288 + * @return void
1289 + */
1290 + public function create_order_line_item( $order_item, $cart_item_key, $values ) {
1291 + if ( isset( $values['pre_order_add_to_cart_event_id'] ) ) {
1292 + // use _ to hide the data
1293 + $order_item->update_meta_data( '_pre_order_add_to_cart_event_id', $values['pre_order_add_to_cart_event_id'] );
1294 + }
1295 + }
1296 +
1297 + /**
984 1298 * Add the_title filter for block rendering.
985 1299 *
986 1300 * @param array $context Default context.
987 1301 *
@@ -987,9 +1301,9 @@
987 1301 *
988 1302 * @return array
989 1303 */
990 1304 public function add_block_title_filter( $context ) {
991 - if ( ! empty( $context['postType'] ) && 'product' === $context['postType'] && ! $this->is_pre_order_filter_on ) {
1305 + if ( ! empty( $context['postType'] ) && 'product' === $context['postType'] && ! $this->is_pre_order_filter_on && ! is_singular( 'product' ) ) {
992 1306 $this->is_pre_order_filter_on = true;
993 1307 add_filter( 'the_title', array( $this, 'block_add_the_title_filter' ), 10, 2 );
994 1308 add_filter( 'render_block', array( $this, 'block_remove_the_title_filter' ), 10, 3 );
995 1309 }
@@ -1054,9 +1368,18 @@
1054 1368 * @return string
1055 1369 */
1056 1370 private function get_pre_order_text( $product_id, $render_type = '' ) {
1057 1371 if ( ! $this->is_pre_order( $product_id ) ) {
1058 - return '';
1372 + $product = wc_get_product( $product_id );
1373 + if ( $product && $product->is_type( 'variation' ) ) {
1374 + $product_id = $product->get_parent_id();
1375 +
1376 + if ( ! $this->is_pre_order( $product_id ) ) {
1377 + return '';
1378 + }
1379 + } else {
1380 + return '';
1381 + }
1059 1382 }
1060 1383
1061 1384 $pre_order_rule = self::available_product_rule( $product_id );
1062 1385 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
@@ -1102,215 +1425,90 @@
1102 1425 ) );
1103 1426 }
1104 1427
1105 1428 /**
1106 - * Get the pre order rules.
1429 + * Get the pre-order rules.
1107 1430 *
1108 1431 * @param array $rule The rule to get.
1109 1432 *
1110 - * @return array|false The pre order rules or false if there are no rule sale.
1433 + * @return array|false The pre-order rules or false if there are no rule sale.
1111 1434 */
1112 - private static function get_rule_sale( $rule ) {
1113 - $sale = false;
1114 - if ( isset( $rule['discount_toggle'] ) && $rule['discount_toggle'] ) {
1115 - $discount_type = $rule['discount_type'];
1116 - $discount_amount = $rule['discount_amount'];
1117 -
1118 - $sale = array(
1119 - 'discount_type' => $discount_type,
1120 - 'discount_amount' => $discount_amount,
1121 - );
1122 - }
1123 -
1124 - /**
1125 - * Filter the pre order sale.
1126 - *
1127 - * @param array $sale The pre order sale.
1128 - * @param array $rule The pre order rule.
1129 - *
1130 - * @since 1.9.9
1131 - */
1132 - return apply_filters( 'merchant_pre_order_rule_sale', $sale, $rule );
1133 - }
1134 -
1135 1435 /**
1136 - * Get the pre order rules.
1436 + * Get the available product rule.
1137 1437 *
1138 - * @return array The pre order rules.
1139 - */
1140 - private static function pre_order_rules() {
1141 - return Merchant_Admin_Options::get( self::MODULE_ID, 'rules', array() );
1142 - }
1143 -
1144 - /**
1145 - * Check if the rule is valid.
1438 + * @param int $product_id The product ID.
1146 1439 *
1147 - * @param array $rule The rule to check.
1148 - *
1149 - * @return boolean True if the rule is valid, false otherwise.
1440 + * @return array The available product rule.
1150 1441 */
1151 - private static function is_valid_rule( $rule ) {
1152 - if ( ! isset( $rule['trigger_on'] ) ) {
1153 - return false;
1154 - }
1442 + public static function available_product_rule( $product_id ) {
1443 + static $cache = array();
1155 1444
1156 - if ( 'product' === $rule['trigger_on'] && empty( $rule['product_ids'] ) ) {
1157 - return false;
1445 + if ( isset( $cache[ $product_id ] ) ) {
1446 + return $cache[ $product_id ];
1158 1447 }
1159 1448
1160 - if ( 'category' === $rule['trigger_on'] && empty( $rule['category_slugs'] ) ) {
1161 - return false;
1162 - }
1449 + $cache[ $product_id ] = Merchant_Pre_Orders_Rules_Repository::get_rule_for_product( $product_id );
1163 1450
1164 - if ( 'tags' === $rule['trigger_on'] && empty( $rule['tag_slugs'] ) ) {
1165 - return false;
1166 - }
1167 -
1168 - if ( isset( $rule['discount_toggle'] ) && $rule['discount_toggle'] === true ) {
1169 - if ( ! isset( $rule['discount_type'] ) ) {
1170 - return false;
1171 - }
1172 - if ( ! isset( $rule['discount_amount'] ) ) {
1173 - return false;
1174 - }
1175 - }
1176 -
1177 - if ( isset( $rule['partial_payment_toggle'] ) && $rule['partial_payment_toggle'] === true ) {
1178 - if ( ! isset( $rule['partial_payment_type'] ) ) {
1179 - return false;
1180 - }
1181 - if ( ! isset( $rule['partial_payment_amount'] ) ) {
1182 - return false;
1183 - }
1184 - }
1185 -
1186 - if ( ! isset( $rule['user_condition'] ) ) {
1187 - return false;
1188 - }
1189 -
1190 - if ( ( 'customers' === $rule['user_condition'] ) && empty( $rule['user_condition_users'] ) ) {
1191 - return false;
1192 - }
1193 -
1194 - if ( ( 'roles' === $rule['user_condition'] ) && empty( $rule['user_condition_roles'] ) ) {
1195 - return false;
1196 - }
1197 -
1198 - if ( empty( $rule['shipping_date'] ) ) {
1199 - return false;
1200 - }
1201 -
1202 - if ( empty( $rule['button_text'] ) ) {
1203 - return false;
1204 - }
1205 -
1206 - if ( empty( $rule['placement'] ) ) {
1207 - return false;
1208 - }
1209 -
1210 - return true;
1451 + return $cache[ $product_id ];
1211 1452 }
1212 1453
1213 1454 /**
1214 - * Prepare the rule fields.
1455 + * Migrate exclusion fields from old format to new individual toggles.
1215 1456 *
1216 - * @param array $rule The rule to prepare.
1217 - *
1218 - * @return array The prepared rule.
1457 + * @return void
1219 1458 */
1220 - private static function prepare_rule( $rule ) {
1221 - if ( 'product' === $rule['trigger_on'] ) {
1222 - $rule['product_ids'] = array_map( 'intval', explode( ',', $rule['product_ids'] ) );
1459 + public static function migrate_exclusion_fields() {
1460 + $option_name = 'merchant_' . self::MODULE_ID . '_ex_fields';
1461 + $migrated = get_option( $option_name, false );
1462 +
1463 + if ( $migrated ) {
1464 + return;
1223 1465 }
1224 1466
1225 - if ( ! empty( $rule['pre_order_start'] ) ) {
1226 - $rule['pre_order_start'] = merchant_convert_date_to_timestamp( $rule['pre_order_start'], self::DATE_TIME_FORMAT );
1227 - }
1467 + $rules = Merchant_Pre_Orders_Rules_Repository::get_global_rules();
1468 + if ( empty( $rules ) ) {
1469 + update_option( $option_name, true );
1228 1470
1229 - if ( ! empty( $rule['pre_order_end'] ) ) {
1230 - $rule['pre_order_end'] = merchant_convert_date_to_timestamp( $rule['pre_order_end'], self::DATE_TIME_FORMAT );
1471 + return;
1231 1472 }
1232 1473
1233 - $rule['shipping_timestamp'] = merchant_convert_date_to_timestamp( $rule['shipping_date'], self::DATE_TIME_FORMAT );
1474 + $needs_migration = false;
1234 1475
1235 - return $rule;
1236 - }
1476 + foreach ( $rules as $rule_key => $rule ) {
1477 + $n = 0;
1237 1478
1238 - /**
1239 - * Get the available product rule.
1240 - *
1241 - * @param string $product_id The product ID.
1242 - *
1243 - * @return array The available product rule.
1244 - */
1245 - public static function available_product_rule( $product_id ) {
1246 - $available_rule = array();
1247 - $rules = self::pre_order_rules();
1248 - $current_time = merchant_get_current_timestamp();
1249 - $current_user = wp_get_current_user();
1250 - foreach ( $rules as $rule ) {
1251 - if ( self::is_valid_rule( $rule ) ) {
1252 - $rule = self::prepare_rule( $rule );
1479 + // Check if individual toggles don't exist yet
1480 + if ( ! isset( $rule['exclude_products_toggle'] ) ) {
1481 + $rules[ $rule_key ]['exclude_products_toggle'] = ! empty( $rule['excluded_products'] );
1482 + ++ $n;
1483 + }
1253 1484
1254 - // check if pre-order start date is set and if it is not in the future
1255 - if ( ! empty( $rule['pre_order_start'] ) && $rule['pre_order_start'] > $current_time ) {
1256 - continue;
1257 - }
1485 + if ( ! isset( $rule['exclude_categories_toggle'] ) ) {
1486 + $rules[ $rule_key ]['exclude_categories_toggle'] = ! empty( $rule['excluded_categories'] );
1487 + ++ $n;
1488 + }
1258 1489
1259 - // check if pre-order end date is set and if it is in the past
1260 - if ( ! empty( $rule['pre_order_end'] ) && $rule['pre_order_end'] < $current_time ) {
1261 - continue;
1262 - }
1490 + if ( ! isset( $rule['exclude_tags_toggle'] ) ) {
1491 + $rules[ $rule_key ]['exclude_tags_toggle'] = ! empty( $rule['excluded_tags'] );
1492 + ++ $n;
1493 + }
1263 1494
1264 - if ( 'roles' === $rule['user_condition'] ) {
1265 - $allowed_roles = $rule['user_condition_roles'];
1266 - $user_roles = $current_user->roles;
1267 - $intersect = array_intersect( $allowed_roles, $user_roles );
1268 - if ( empty( $intersect ) ) {
1269 - continue;
1270 - }
1271 - }
1495 + if ( ! isset( $rule['exclude_brands_toggle'] ) ) {
1496 + $rules[ $rule_key ]['exclude_brands_toggle'] = ! empty( $rule['excluded_brands'] );
1497 + ++ $n;
1498 + }
1272 1499
1273 - if ( 'customers' === $rule['user_condition'] ) {
1274 - $allowed_users = $rule['user_condition_users'];
1275 - $current_user_id = $current_user->ID;
1276 - if ( ! in_array( $current_user_id, $allowed_users, true ) ) {
1277 - continue;
1278 - }
1279 - }
1500 + if ( $n > 0 ) {
1501 + $needs_migration = true; // Set to true if any iteration needs migration
1502 + }
1503 + }
1280 1504
1281 - if ( 'product' === $rule['trigger_on'] && in_array( $product_id, $rule['product_ids'], true ) ) {
1282 - $available_rule = $rule;
1283 - break;
1284 - }
1285 - if ( 'category' === $rule['trigger_on'] || 'tags' === $rule['trigger_on'] ) {
1286 - $taxonomy = $rule['trigger_on'] === 'category' ? 'product_cat' : 'product_tag';
1287 - $slugs = $rule['trigger_on'] === 'category' ? ( $rule['category_slugs'] ?? array() ) : ( $rule['tag_slugs'] ?? array() );
1288 -
1289 - $terms = get_the_terms( $product_id, $taxonomy );
1290 - if ( ! empty( $terms ) ) {
1291 - foreach ( $terms as $term ) {
1292 - if ( in_array( $term->slug, $slugs, true ) ) {
1293 - $available_rule = $rule;
1294 - break;
1295 - }
1296 - }
1297 - }
1298 - }
1299 - }
1505 + if ( $needs_migration ) {
1506 + Merchant_Admin_Options::set( self::MODULE_ID, 'rules', $rules );
1300 1507 }
1301 1508
1302 - /**
1303 - * Filter the available product rule.
1304 - *
1305 - * @param array $available_rule The available product rule.
1306 - * @param int $product_id The product ID.
1307 - *
1308 - * @return array The available product rule.
1309 - *
1310 - * @since 1.9.9
1311 - */
1312 - return apply_filters( 'merchant_pre_order_available_rule', $available_rule, $product_id );
1509 + // Mark migration as completed
1510 + update_option( $option_name, true );
1313 1511 }
1314 1512
1315 1513 /**
1316 1514 * Migrate old data to the new module storage.
@@ -1347,9 +1545,9 @@
1347 1545 $migrated = get_option( 'merchant_pre_orders_migrated' );
1348 1546 if ( ! $migrated ) {
1349 1547 $products = new WP_Query( $args );
1350 1548 if ( $products->have_posts() ) {
1351 - $rules = self::pre_order_rules();
1549 + $rules = Merchant_Pre_Orders_Rules_Repository::get_global_rules();
1352 1550 while ( $products->have_posts() ) {
1353 1551 $products->the_post();
1354 1552 $product_id = get_the_ID();
1355 1553 $pre_order_date = get_post_meta( $product_id, '_pre_order_date', true );
@@ -1378,5 +1576,6 @@
1378 1576 }
1379 1577 }
1380 1578 }
1381 1579
1382 -add_action( 'init', 'Merchant_Pre_Orders_Main_Functionality::data_migration' );
1580 +add_action( 'init', 'Merchant_Pre_Orders_Main_Functionality::data_migration' );
1581 +add_action( 'init', 'Merchant_Pre_Orders_Main_Functionality::migrate_exclusion_fields' );