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 +473 -229 1.9.102.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
@@ -74,9 +94,9 @@
74 94 add_filter( 'woocommerce_post_class', array( $this, 'pre_orders_post_class' ), 10, 3 );
75 95
76 96 add_filter( 'woocommerce_get_price_html', array( $this, 'dynamic_discount_price_html' ), 10, 2 );
77 97 add_action( 'woocommerce_before_calculate_totals', array( $this, 'dynamic_discount_cart_price' ) );
78 - add_action( 'woocommerce_checkout_order_created', array( $this, 'splitting_orders' ) );
98 + add_action( 'woocommerce_thankyou', array( $this, 'splitting_orders' ) );
79 99 add_filter( 'manage_woocommerce_page_wc-orders_columns', array( $this, 'shop_order_column' ), 11 );
80 100 add_filter( 'manage_edit-shop_order_columns', array( $this, 'shop_order_column' ), 11 );
81 101 add_action( 'manage_woocommerce_page_wc-orders_custom_column', array( $this, 'shop_order_column_content' ), 10, 2 );
82 102 add_action( 'manage_shop_order_posts_custom_column', array( $this, 'shop_order_column_content' ), 10, 2 );
@@ -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 /**
@@ -186,21 +288,25 @@
186 288
187 289 /**
188 290 * Splitting orders.
189 291 *
190 - * @param $order WC_Order The order object.
292 + * @param $order_id int The order ID.
191 293 *
192 294 * @return void
193 295 */
194 - public function splitting_orders( $order ) {
195 - $mode = Merchant_Admin_Options::get( self::MODULE_ID, 'modes', 'unified_order' );
196 - if ( 'unified_order' === $mode || 'only_pre_orders' === $mode ) {
197 - $this->mark_whole_order_as_pre_order( $order );
198 - } elseif ( 'group_pre_order_into_one_order' === $mode ) {
199 - $this->group_pre_order_into_one_order( $order );
200 - } elseif ( 'separate_order_for_pre_orders' === $mode ) {
201 - $this->separate_order_for_pre_orders( $order );
202 - }
296 + public function splitting_orders( $order_id ) {
297 + $order = wc_get_order( $order_id );
298 + $mode = Merchant_Admin_Options::get( self::MODULE_ID, 'modes', 'unified_order' );
299 +
300 + $this->mark_whole_order_as_pre_order( $order );
301 +
302 +// if ( 'unified_order' === $mode || 'only_pre_orders' === $mode ) {
303 +// $this->mark_whole_order_as_pre_order( $order );
304 +// } elseif ( 'group_pre_order_into_one_order' === $mode ) {
305 +// $this->group_pre_order_into_one_order( $order );
306 +// } elseif ( 'separate_order_for_pre_orders' === $mode ) {
307 +// $this->separate_order_for_pre_orders( $order );
308 +// }
203 309 }
204 310
205 311 /**
206 312 * Mark whole order as pre-order if it contains pre-order products.
@@ -216,9 +322,8 @@
216 322 $product_id = $item->get_product_id();
217 323 if ( $this->is_pre_order( $product_id ) ) {
218 324 $shipping_dates[] = $item->get_meta( '_merchant_pre_order_shipping_date' );
219 325 ++ $has_pre_order;
220 - break;
221 326 }
222 327 }
223 328
224 329 if ( $has_pre_order ) {
@@ -225,8 +330,9 @@
225 330 $order->set_status( 'wc-pre-ordered' );
226 331 $order->add_meta_data( '_is_pre_order', true );
227 332 $order->add_meta_data( '_merchant_order_pre_order_shipping_date', max( $shipping_dates ) );
228 333 $order->save();
334 + $this->trigger_emails( $order );
229 335 }
230 336 }
231 337
232 338 /**
@@ -288,8 +394,10 @@
288 394 $new_order->save();
289 395
290 396 $sub_order_ids[] = $new_order->get_id();
291 397
398 + $this->trigger_emails( $new_order );
399 +
292 400 // Save sub-order IDs to the original order
293 401 $original_order->update_meta_data( '_sub_order_ids', $sub_order_ids );
294 402
295 403
@@ -367,8 +475,9 @@
367 475 $new_order->add_meta_data( '_is_pre_order', true );
368 476 $new_order->add_meta_data( '_merchant_order_pre_order_shipping_date', $rule['shipping_timestamp'] );
369 477 $new_order->set_status( 'wc-pre-ordered' );
370 478 $new_order->save();
479 + $this->trigger_emails( $new_order );
371 480 $sub_order_ids[] = $new_order->get_id();
372 481 }
373 482
374 483 // Save sub-order IDs to the original order
@@ -379,8 +488,26 @@
379 488 $original_order->save();
380 489 }
381 490
382 491 /**
492 + * Trigger sending emails for specific order.
493 + *
494 + * @param $order WC_Order The order object.
495 + *
496 + * @return void
497 + */
498 + private function trigger_emails( $order ) {
499 + $mailer = WC()->mailer();
500 + // Send customer email
501 +// $customer_email_instance = $mailer->emails['WC_Email_Customer_Processing_Order'];
502 +// $customer_email_instance->trigger( $order->get_id(), $order );
503 +
504 + // Send admin email
505 + $admin_email_instance = $mailer->emails['WC_Email_New_Order'];
506 + $admin_email_instance->trigger( $order->get_id(), $order );
507 + }
508 +
509 + /**
383 510 * Hide order item meta that will be used for internal use only.
384 511 *
385 512 * @param $args array The hidden order item meta.
386 513 *
@@ -463,20 +590,25 @@
463 590 if ( apply_filters( 'merchant_pre_order_disable_sale_price_html', false, $html_price, $product ) ) {
464 591 return $html_price;
465 592 }
466 593
467 - 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 ) ) {
468 595 if ( '' === $product->get_price() ) {
469 596 return $html_price;
470 597 }
471 598
472 599 $offer = self::available_product_rule( $product->get_id() );
600 +
473 601 if ( empty( $offer ) && $product->is_type( 'variation' ) ) {
474 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 + }
475 607 }
476 608
477 609 if ( $product->is_type( 'variable' ) ) {
478 - $html_price = $this->variable_product_price_html( $product );
610 + $html_price = $this->variable_product_price_html( $product, $offer, $html_price );
479 611 } else {
480 612 $html_price = $this->simple_product_price_html( $product, $offer, $html_price );
481 613 }
482 614 }
@@ -490,43 +622,85 @@
490 622 * @param WC_Product $product The product object.
491 623 *
492 624 * @return string The price html.
493 625 */
494 - 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 +
495 632 $prices = array();
496 633 $variations = $product->get_children();
634 +
635 + $regular_prices = array();
636 + $sale_prices = array();
637 +
497 638 foreach ( $variations as $variation_id ) {
498 639 $variation = wc_get_product( $variation_id );
499 640 $variation_offer = self::available_product_rule( $variation_id );
500 - $regular_price = $variation->get_regular_price();
641 + $regular_price = $variation->get_regular_price();
642 +
643 + $regular_prices[] = $regular_price;
644 +
501 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 ) ) {
502 657 if ( $variation->is_on_sale() ) {
503 - $sale_price = $variation->get_sale_price();
658 + $sale_price = wc_get_price_to_display( $variation );
504 659 } else {
505 660 $sale_price = $regular_price;
506 661 }
507 - $prices[] = $sale_price;
662 + $prices[] = $sale_price;
663 + $sale_prices[] = $sale_price;
508 664 continue;
509 665 }
666 +
510 667 $sale_price = $this->calculate_discounted_price( $regular_price, $variation_offer, $variation );
511 668 if ( $sale_price <= 0 ) {
512 669 // If the price is less than 0, set it to the regular/sale price
513 670 if ( $variation->is_on_sale() ) {
514 - $sale_price = $variation->get_sale_price();
671 + $sale_price = wc_get_price_to_display( $variation );
515 672 } else {
516 673 $sale_price = $regular_price;
517 674 }
518 675 }
519 - $prices[] = $sale_price;
676 + $prices[] = $sale_price;
677 + $sale_prices[] = $sale_price;
520 678 }
521 679
522 - $min_price = min( $prices );
523 - $max_price = max( $prices );
524 - if ( $min_price !== $max_price ) {
525 - 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 );
526 687 }
527 688
528 - 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 '';
529 703 }
530 704
531 705 /**
532 706 * Simple product price html.
@@ -536,13 +710,13 @@
536 710 *
537 711 * @return string The price html.
538 712 */
539 713 private function simple_product_price_html( $product, $offer, $html_price ) {
540 - $sale = self::get_rule_sale( $offer );
714 + $sale = Merchant_Pre_Orders_Rules_Repository::get_rule_sale( $offer );
541 715 if ( ! $sale ) {
542 716 return $html_price;
543 717 }
544 - $regular_price = $product->get_regular_price();
718 + $regular_price = wc_get_price_to_display( $product, array( 'price' => $product->get_regular_price() ) );
545 719 $discounted_price = $this->calculate_discounted_price( $regular_price, $offer, $product );
546 720
547 721 return wc_format_sale_price( $regular_price, $discounted_price );
548 722 }
@@ -556,9 +730,9 @@
556 730 *
557 731 * @return float The discounted price.
558 732 */
559 733 public function calculate_discounted_price( $price, $offer, $product = null ) {
560 - $sale = self::get_rule_sale( $offer );
734 + $sale = Merchant_Pre_Orders_Rules_Repository::get_rule_sale( $offer );
561 735 $discount_type = $discount_value = '';
562 736 if ( $sale ) {
563 737 $discount_type = $offer['discount_type'];
564 738 $discount_value = $offer['discount_amount'];
@@ -612,10 +786,12 @@
612 786 continue;
613 787 }
614 788 $product_id = $cart_item['data']->get_id();
615 789 $product = wc_get_product( $product_id );
616 - 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 ) ) {
617 792 $offer = self::available_product_rule( $product->get_id() );
793 +
618 794 if ( $product->is_type( 'variable' ) ) {
619 795 $product_id = $cart_item['variation_id'];
620 796 $product = wc_get_product( $product_id );
621 797 // check if the variation has an offer
@@ -620,14 +796,41 @@
620 796 $product = wc_get_product( $product_id );
621 797 // check if the variation has an offer
622 798 $offer = self::available_product_rule( $product_id );
623 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 +
624 810 if ( empty( $offer ) ) {
625 811 continue;
626 812 }
627 -
813 + if ( ! isset( $offer['discount_toggle'] ) ) {
814 + continue;
815 + }
628 816 $regular_price = $product->get_regular_price();
629 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 + );
630 833 $cart_item['data']->set_price( $sale_price );
631 834 $cart_item['data']->set_meta_data( array( '_merchant_pre_order_sale' => $offer ) );
632 835 }
633 836 }
@@ -714,12 +917,12 @@
714 917 if ( 0 !== $parent_order_id ) {
715 918 $parent_order = wc_get_order( $parent_order_id );
716 919
717 920 if ( $parent_order->get_status() === 'completed' ) {
718 - $order->update_status( 'wc-completed', '[Merchant Pre Orders] ' );
921 + $order->update_status( 'processing', '[Merchant Pre Orders] ' );
719 922 }
720 - } elseif ( $order->get_status() === 'wc-pre-ordered' && $order->payment_complete() ) {
721 - $order->update_status( 'wc-completed', '[Merchant Pre Orders] ' );
923 + } elseif ( $order->payment_complete() ) {
924 + $order->update_status( 'processing', '[Merchant Pre Orders] ' );
722 925 }
723 926 }
724 927 }
725 928 }
@@ -743,18 +946,22 @@
743 946
744 947 /**
745 948 * Change pre-order button text for variable products.
746 949 *
747 - * @param array $data
748 - * @param object $product
749 - * @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.
750 953 *
751 954 * @return array
752 955 */
753 - public function change_button_text_for_variable_products( $data, $product, $variation ) {
754 - if ( $this->is_pre_order( $variation->get_id() ) ) {
755 - $pre_order_rule = self::available_product_rule( $variation->get_id() );
756 - $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';
757 964
758 965 $additional_text = $pre_order_rule['additional_text'] ? Merchant_Translator::translate( $pre_order_rule['additional_text'] )
759 966 : esc_html__( 'Ships on {date}.', 'merchant' );
760 967 $time_format = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
@@ -812,8 +1019,68 @@
812 1019 }
813 1020 }
814 1021
815 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 + /**
816 1083 * Display pre order additional information after add to cart form.
817 1084 *
818 1085 * @return void
819 1086 */
@@ -881,9 +1148,9 @@
881 1148 *
882 1149 * @return array
883 1150 */
884 1151 public function add_pre_orders_order_statuses( $order_statuses ) {
885 - $order_statuses['wc-pre-ordered'] = esc_html__( 'Pre Ordered', 'merchant' );
1152 + $order_statuses['wc-pre-ordered'] = esc_html__( 'Pre-Ordered', 'merchant' );
886 1153
887 1154 return $order_statuses;
888 1155 }
889 1156
@@ -911,10 +1178,18 @@
911 1178 */
912 1179 public function cart_message_handler( $item_data, $cart_item ) {
913 1180 $product_id = $cart_item['product_id'];
914 1181 if ( $cart_item['data']->is_type( 'variation' ) ) {
915 - $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 + }
916 1190 }
1191 +
917 1192 if ( $this->is_pre_order( $product_id ) ) {
918 1193 $pre_order_rule = self::available_product_rule( $product_id );
919 1194 if ( $pre_order_rule ) {
920 1195 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
@@ -942,9 +1217,11 @@
942 1217 *
943 1218 * @return string
944 1219 */
945 1220 public function order_item_meta_end( $item_id, $item, $order, $plain_text ) {
946 - 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 + }
947 1224 }
948 1225
949 1226 /**
950 1227 * Render pre order text.
@@ -953,8 +1230,72 @@
953 1230 echo $this->get_pre_order_text( get_the_ID(), 'span' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
954 1231 }
955 1232
956 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 + /**
957 1298 * Add the_title filter for block rendering.
958 1299 *
959 1300 * @param array $context Default context.
960 1301 *
@@ -960,9 +1301,9 @@
960 1301 *
961 1302 * @return array
962 1303 */
963 1304 public function add_block_title_filter( $context ) {
964 - 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' ) ) {
965 1306 $this->is_pre_order_filter_on = true;
966 1307 add_filter( 'the_title', array( $this, 'block_add_the_title_filter' ), 10, 2 );
967 1308 add_filter( 'render_block', array( $this, 'block_remove_the_title_filter' ), 10, 3 );
968 1309 }
@@ -1027,21 +1368,41 @@
1027 1368 * @return string
1028 1369 */
1029 1370 private function get_pre_order_text( $product_id, $render_type = '' ) {
1030 1371 if ( ! $this->is_pre_order( $product_id ) ) {
1031 - 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 + }
1032 1382 }
1033 1383
1034 1384 $pre_order_rule = self::available_product_rule( $product_id );
1035 1385 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
1036 - $pre_order_date = date_i18n( get_option( 'date_format' ), $pre_order_rule['cart_label_text'] );
1386 + $pre_order_date = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
1037 1387 if ( 'span' === $render_type ) {
1038 - return sprintf( '<span class="merchant-pre-orders-note"><span class="merchant-pre-orders-label">%s:</span><span>%s</span></span>', esc_html( $label_text ),
1039 - $pre_order_date );
1388 + return sprintf(
1389 + '<span class="merchant-pre-orders-note"><span class="merchant-pre-orders-label">%s:</span><span>%s</span></span>',
1390 + esc_html( $label_text ),
1391 + $pre_order_date
1392 + );
1040 1393 } elseif ( 'dl' === $render_type ) {
1041 - return sprintf( '<dl class="merchant-pre-orders-note"><dt>%s:</dt><dd>%s</dd></dl>', esc_html( $label_text ), $pre_order_date );
1394 + return sprintf(
1395 + '<dl class="merchant-pre-orders-note"><dt>%s:</dt><dd>%s</dd></dl>',
1396 + esc_html( $label_text ),
1397 + $pre_order_date
1398 + );
1042 1399 } else {
1043 - return sprintf( '%s: %s', esc_html( $label_text ), $pre_order_date );
1400 + return sprintf(
1401 + '%s: %s',
1402 + esc_html( $label_text ),
1403 + $pre_order_date
1404 + );
1044 1405 }
1045 1406 }
1046 1407
1047 1408 /**
@@ -1064,208 +1425,90 @@
1064 1425 ) );
1065 1426 }
1066 1427
1067 1428 /**
1068 - * Get the pre order rules.
1429 + * Get the pre-order rules.
1069 1430 *
1070 1431 * @param array $rule The rule to get.
1071 1432 *
1072 - * @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.
1073 1434 */
1074 - private static function get_rule_sale( $rule ) {
1075 - $sale = false;
1076 - if ( isset( $rule['discount_toggle'] ) && $rule['discount_toggle'] ) {
1077 - $discount_type = $rule['discount_type'];
1078 - $discount_amount = $rule['discount_amount'];
1079 -
1080 - $sale = array(
1081 - 'discount_type' => $discount_type,
1082 - 'discount_amount' => $discount_amount,
1083 - );
1084 - }
1085 -
1086 - /**
1087 - * Filter the pre order sale.
1088 - *
1089 - * @param array $sale The pre order sale.
1090 - * @param array $rule The pre order rule.
1091 - *
1092 - * @since 1.9.9
1093 - */
1094 - return apply_filters( 'merchant_pre_order_rule_sale', $sale, $rule );
1095 - }
1096 -
1097 1435 /**
1098 - * Get the pre order rules.
1436 + * Get the available product rule.
1099 1437 *
1100 - * @return array The pre order rules.
1101 - */
1102 - private static function pre_order_rules() {
1103 - return Merchant_Admin_Options::get( self::MODULE_ID, 'rules', array() );
1104 - }
1105 -
1106 - /**
1107 - * Check if the rule is valid.
1438 + * @param int $product_id The product ID.
1108 1439 *
1109 - * @param array $rule The rule to check.
1110 - *
1111 - * @return boolean True if the rule is valid, false otherwise.
1440 + * @return array The available product rule.
1112 1441 */
1113 - private static function is_valid_rule( $rule ) {
1114 - if ( ! isset( $rule['trigger_on'] ) ) {
1115 - return false;
1116 - }
1442 + public static function available_product_rule( $product_id ) {
1443 + static $cache = array();
1117 1444
1118 - if ( 'product' === $rule['trigger_on'] && empty( $rule['product_ids'] ) ) {
1119 - return false;
1445 + if ( isset( $cache[ $product_id ] ) ) {
1446 + return $cache[ $product_id ];
1120 1447 }
1121 1448
1122 - if ( 'category' === $rule['trigger_on'] && empty( $rule['category_slugs'] ) ) {
1123 - return false;
1124 - }
1449 + $cache[ $product_id ] = Merchant_Pre_Orders_Rules_Repository::get_rule_for_product( $product_id );
1125 1450
1126 - if ( isset( $rule['discount_toggle'] ) && $rule['discount_toggle'] === true ) {
1127 - if ( ! isset( $rule['discount_type'] ) ) {
1128 - return false;
1129 - }
1130 - if ( ! isset( $rule['discount_amount'] ) ) {
1131 - return false;
1132 - }
1133 - }
1134 -
1135 - if ( isset( $rule['partial_payment_toggle'] ) && $rule['partial_payment_toggle'] === true ) {
1136 - if ( ! isset( $rule['partial_payment_type'] ) ) {
1137 - return false;
1138 - }
1139 - if ( ! isset( $rule['partial_payment_amount'] ) ) {
1140 - return false;
1141 - }
1142 - }
1143 -
1144 - if ( ! isset( $rule['user_condition'] ) ) {
1145 - return false;
1146 - }
1147 -
1148 - if ( ( 'customers' === $rule['user_condition'] ) && empty( $rule['user_condition_users'] ) ) {
1149 - return false;
1150 - }
1151 -
1152 - if ( ( 'roles' === $rule['user_condition'] ) && empty( $rule['user_condition_roles'] ) ) {
1153 - return false;
1154 - }
1155 -
1156 - if ( empty( $rule['shipping_date'] ) ) {
1157 - return false;
1158 - }
1159 -
1160 - if ( empty( $rule['button_text'] ) ) {
1161 - return false;
1162 - }
1163 -
1164 - if ( empty( $rule['placement'] ) ) {
1165 - return false;
1166 - }
1167 -
1168 - return true;
1451 + return $cache[ $product_id ];
1169 1452 }
1170 1453
1171 1454 /**
1172 - * Prepare the rule fields.
1455 + * Migrate exclusion fields from old format to new individual toggles.
1173 1456 *
1174 - * @param array $rule The rule to prepare.
1175 - *
1176 - * @return array The prepared rule.
1457 + * @return void
1177 1458 */
1178 - private static function prepare_rule( $rule ) {
1179 - if ( 'product' === $rule['trigger_on'] ) {
1180 - $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;
1181 1465 }
1182 1466
1183 - if ( ! empty( $rule['pre_order_start'] ) ) {
1184 - $rule['pre_order_start'] = merchant_convert_date_to_timestamp( $rule['pre_order_start'], self::DATE_TIME_FORMAT );
1185 - }
1467 + $rules = Merchant_Pre_Orders_Rules_Repository::get_global_rules();
1468 + if ( empty( $rules ) ) {
1469 + update_option( $option_name, true );
1186 1470
1187 - if ( ! empty( $rule['pre_order_end'] ) ) {
1188 - $rule['pre_order_end'] = merchant_convert_date_to_timestamp( $rule['pre_order_end'], self::DATE_TIME_FORMAT );
1471 + return;
1189 1472 }
1190 1473
1191 - $rule['shipping_timestamp'] = merchant_convert_date_to_timestamp( $rule['shipping_date'], self::DATE_TIME_FORMAT );
1474 + $needs_migration = false;
1192 1475
1193 - return $rule;
1194 - }
1476 + foreach ( $rules as $rule_key => $rule ) {
1477 + $n = 0;
1195 1478
1196 - /**
1197 - * Get the available product rule.
1198 - *
1199 - * @param string $product_id The product ID.
1200 - *
1201 - * @return array The available product rule.
1202 - */
1203 - public static function available_product_rule( $product_id ) {
1204 - $available_rule = array();
1205 - $rules = self::pre_order_rules();
1206 - $current_time = merchant_get_current_timestamp();
1207 - $current_user = wp_get_current_user();
1208 - foreach ( $rules as $rule ) {
1209 - if ( self::is_valid_rule( $rule ) ) {
1210 - $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 + }
1211 1484
1212 - // check if pre-order start date is set and if it is not in the future
1213 - if ( ! empty( $rule['pre_order_start'] ) && $rule['pre_order_start'] > $current_time ) {
1214 - continue;
1215 - }
1485 + if ( ! isset( $rule['exclude_categories_toggle'] ) ) {
1486 + $rules[ $rule_key ]['exclude_categories_toggle'] = ! empty( $rule['excluded_categories'] );
1487 + ++ $n;
1488 + }
1216 1489
1217 - // check if pre-order end date is set and if it is in the past
1218 - if ( ! empty( $rule['pre_order_end'] ) && $rule['pre_order_end'] < $current_time ) {
1219 - continue;
1220 - }
1490 + if ( ! isset( $rule['exclude_tags_toggle'] ) ) {
1491 + $rules[ $rule_key ]['exclude_tags_toggle'] = ! empty( $rule['excluded_tags'] );
1492 + ++ $n;
1493 + }
1221 1494
1222 - if ( 'roles' === $rule['user_condition'] ) {
1223 - $allowed_roles = $rule['user_condition_roles'];
1224 - $user_roles = $current_user->roles;
1225 - $intersect = array_intersect( $allowed_roles, $user_roles );
1226 - if ( empty( $intersect ) ) {
1227 - continue;
1228 - }
1229 - }
1495 + if ( ! isset( $rule['exclude_brands_toggle'] ) ) {
1496 + $rules[ $rule_key ]['exclude_brands_toggle'] = ! empty( $rule['excluded_brands'] );
1497 + ++ $n;
1498 + }
1230 1499
1231 - if ( 'customers' === $rule['user_condition'] ) {
1232 - $allowed_users = $rule['user_condition_users'];
1233 - $current_user_id = $current_user->ID;
1234 - if ( ! in_array( $current_user_id, $allowed_users, true ) ) {
1235 - continue;
1236 - }
1237 - }
1500 + if ( $n > 0 ) {
1501 + $needs_migration = true; // Set to true if any iteration needs migration
1502 + }
1503 + }
1238 1504
1239 - if ( 'product' === $rule['trigger_on'] && in_array( $product_id, $rule['product_ids'], true ) ) {
1240 - $available_rule = $rule;
1241 - break;
1242 - }
1243 - if ( 'category' === $rule['trigger_on'] ) {
1244 - $terms = get_the_terms( $product_id, 'product_cat' );
1245 - if ( ! empty( $terms ) ) {
1246 - foreach ( $terms as $term ) {
1247 - if ( in_array( $term->slug, $rule['category_slugs'], true ) ) {
1248 - $available_rule = $rule;
1249 - break;
1250 - }
1251 - }
1252 - }
1253 - }
1254 - }
1505 + if ( $needs_migration ) {
1506 + Merchant_Admin_Options::set( self::MODULE_ID, 'rules', $rules );
1255 1507 }
1256 1508
1257 - /**
1258 - * Filter the available product rule.
1259 - *
1260 - * @param array $available_rule The available product rule.
1261 - * @param int $product_id The product ID.
1262 - *
1263 - * @return array The available product rule.
1264 - *
1265 - * @since 1.9.9
1266 - */
1267 - return apply_filters( 'merchant_pre_order_available_rule', $available_rule, $product_id );
1509 + // Mark migration as completed
1510 + update_option( $option_name, true );
1268 1511 }
1269 1512
1270 1513 /**
1271 1514 * Migrate old data to the new module storage.
@@ -1302,9 +1545,9 @@
1302 1545 $migrated = get_option( 'merchant_pre_orders_migrated' );
1303 1546 if ( ! $migrated ) {
1304 1547 $products = new WP_Query( $args );
1305 1548 if ( $products->have_posts() ) {
1306 - $rules = self::pre_order_rules();
1549 + $rules = Merchant_Pre_Orders_Rules_Repository::get_global_rules();
1307 1550 while ( $products->have_posts() ) {
1308 1551 $products->the_post();
1309 1552 $product_id = get_the_ID();
1310 1553 $pre_order_date = get_post_meta( $product_id, '_pre_order_date', true );
@@ -1333,5 +1576,6 @@
1333 1576 }
1334 1577 }
1335 1578 }
1336 1579
1337 -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' );