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
merchant / inc / modules / pre-orders / class-pre-orders-main-functionality.php

class-pre-orders-main-functionality.php in Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant 2.3.2, at inc/modules/pre-orders/class-pre-orders-main-functionality.php

1,582 lines 50.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Pre Orders.
5 *
6 * @package Merchant
7 */
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit; // Exit if accessed directly
11 }
12
13 /**
14 * Pre Orders Class.
15 *
16 */
17 class Merchant_Pre_Orders_Main_Functionality {
18
19 /**
20 * Module ID.
21 *
22 */
23 const MODULE_ID = 'pre-orders';
24
25 /**
26 * @var string Date time format
27 */
28 public const DATE_TIME_FORMAT = 'm-d-Y h:i A';
29
30 /**
31 * Flag if the_title filter is added or not.
32 *
33 * @var bool
34 */
35 private $is_pre_order_filter_on = false;
36
37 /**
38 * @var string The module ID.
39 */
40 private $module_id = 'pre-orders';
41
42 /**
43 * Init.
44 *
45 * @return void
46 */
47 public function init() {
48 add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'allow_one_type_only' ), 99, 2 );
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' ) );
52 add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) );
53 add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_order_item_meta' ), 10, 4 );
54
55 // Cronjob.
56 if ( ! wp_next_scheduled( 'check_for_released_preorders' ) ) {
57 wp_schedule_event( time(), 'twicedaily', 'check_for_released_preorders' );
58 }
59
60 add_action( 'check_for_released_preorders', array( $this, 'check_for_pre_orders_and_maybe_update_status' ) );
61
62 add_filter( 'woocommerce_product_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
63 add_filter( 'woocommerce_product_single_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
64 add_filter( 'woocommerce_available_variation', array( $this, 'attach_pre_order_data_to_variations' ), 10, 3 );
65 add_action( 'woocommerce_before_add_to_cart_form', array( $this, 'additional_information_before_cart_form' ) );
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' ) );
68
69 // Cart
70 add_filter( 'woocommerce_get_item_data', array( $this, 'cart_message_handler' ), 10, 2 );
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
81 add_action( 'woocommerce_order_item_meta_end', array( $this, 'order_item_meta_end' ), 10, 4 );
82 add_action( 'woocommerce_shop_loop_item_title', array( $this, 'shop_loop_item_title' ) );
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
87 // Products block.
88 add_filter( 'render_block_context', array( $this, 'add_block_title_filter' ), 10, 1 );
89 add_filter( 'woocommerce_blocks_product_grid_item_html', array( $this, 'override_product_grid_block' ), PHP_INT_MAX, 3 );
90
91 $this->register_pre_orders_order_status();
92
93 add_filter( 'wc_order_statuses', array( $this, 'add_pre_orders_order_statuses' ) );
94 add_filter( 'woocommerce_post_class', array( $this, 'pre_orders_post_class' ), 10, 3 );
95
96 add_filter( 'woocommerce_get_price_html', array( $this, 'dynamic_discount_price_html' ), 10, 2 );
97 add_action( 'woocommerce_before_calculate_totals', array( $this, 'dynamic_discount_cart_price' ) );
98 add_action( 'woocommerce_thankyou', array( $this, 'splitting_orders' ) );
99 add_filter( 'manage_woocommerce_page_wc-orders_columns', array( $this, 'shop_order_column' ), 11 );
100 add_filter( 'manage_edit-shop_order_columns', array( $this, 'shop_order_column' ), 11 );
101 add_action( 'manage_woocommerce_page_wc-orders_custom_column', array( $this, 'shop_order_column_content' ), 10, 2 );
102 add_action( 'manage_shop_order_posts_custom_column', array( $this, 'shop_order_column_content' ), 10, 2 );
103 }
104
105 /**
106 * Add shipping date column to the orders list.
107 *
108 * @param $columns array The columns.
109 *
110 * @return array The columns.
111 */
112 public function shop_order_column( $columns ) {
113 $columns['pre_order_shipping_date'] = esc_html__( 'Shipping Date', 'merchant' );
114
115 return $columns;
116 }
117
118 /**
119 * Display the shipping date in the orders list.
120 *
121 * @param $column string The column.
122 * @param $order WC_Order|int The order object in HPOS or order id.
123 *
124 * @return void
125 */
126 public function shop_order_column_content( $column, $order ) {
127 if ( $column === 'pre_order_shipping_date' ) {
128 if ( is_numeric( $order ) ) {
129 $order = wc_get_order( $order );
130 }
131 /**
132 * Filter the pre order shipping date.
133 *
134 * @param int $shipping_date The shipping date.
135 * @param WC_Order $order The order object.
136 *
137 * @since 1.9.9
138 */
139 $shipping_date = apply_filters(
140 'merchant_pre_order_shipping_date_column',
141 $order->get_meta( '_merchant_order_pre_order_shipping_date' ),
142 $order
143 );
144 if ( $shipping_date ) {
145 echo '<strong>' . esc_html( $this->convert_timestamp_to_human_readable( $shipping_date ) ) . '</strong>';
146 }
147 }
148 }
149
150 /**
151 * Convert timestamp to human readable date.
152 *
153 * @param $timestamp int The timestamp.
154 *
155 * @return string The human readable date.
156 */
157 private function convert_timestamp_to_human_readable( $timestamp ) {
158 $timezone = new DateTimeZone( merchant_timezone() );
159 $date = new \DateTime( '@' . $timestamp ); // Create from UTC timestamp
160 $date->setTimezone( $timezone ); // Convert to site timezone
161
162 return $date->format( self::DATE_TIME_FORMAT );
163 }
164
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 /**
219 * Add offer details to the product.
220 *
221 * @param $cart_item_data array The cart item data.
222 * @param $product_id int The product ID.
223 * @param $variation_id int The variation ID.
224 * @param $quantity int The quantity.
225 *
226 * @return array The cart item data.
227 */
228 public function add_cart_item_data( $cart_item_data, $product_id, $variation_id, $quantity ) {
229 $product = wc_get_product( $product_id );
230 $offer = self::available_product_rule( $product_id );
231 if ( empty( $offer ) && $product->is_type( 'variable' ) ) {
232 $offer = self::available_product_rule( $variation_id );
233 }
234 if ( ! empty( $offer ) ) {
235 $cart_item_data['_merchant_pre_order'] = $offer;
236 $cart_item_data['_merchant_pre_order_shipping_date'] = $offer['shipping_timestamp'];
237 }
238
239 return $cart_item_data;
240 }
241
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 /**
269 * Add pre-order data to order item meta.
270 *
271 * @param $item WC_Order_Item The order item object.
272 * @param $cart_item_key string The cart item key.
273 * @param $values array The values.
274 * @param $order WC_Order The order object.
275 *
276 * @return void
277 * @throws Exception
278 */
279 public function add_order_item_meta( $item, $cart_item_key, $values, $order ) {
280 if ( isset( $values['_merchant_pre_order'] ) ) {
281 $item->update_meta_data( '_merchant_pre_order', $values['_merchant_pre_order'] );
282 }
283
284 if ( isset( $values['_merchant_pre_order_shipping_date'] ) ) {
285 $item->update_meta_data( '_merchant_pre_order_shipping_date', $values['_merchant_pre_order_shipping_date'] );
286 }
287 }
288
289 /**
290 * Splitting orders.
291 *
292 * @param $order_id int The order ID.
293 *
294 * @return void
295 */
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 // }
309 }
310
311 /**
312 * Mark whole order as pre-order if it contains pre-order products.
313 *
314 * @param $order WC_Order The order object.
315 *
316 * @return void
317 */
318 public function mark_whole_order_as_pre_order( $order ) {
319 $has_pre_order = 0;
320 $shipping_dates = array();
321 foreach ( $order->get_items() as $item_id => $item ) {
322 $product_id = $item->get_product_id();
323 if ( $this->is_pre_order( $product_id ) ) {
324 $shipping_dates[] = $item->get_meta( '_merchant_pre_order_shipping_date' );
325 ++ $has_pre_order;
326 }
327 }
328
329 if ( $has_pre_order ) {
330 $order->set_status( 'wc-pre-ordered' );
331 $order->add_meta_data( '_is_pre_order', true );
332 $order->add_meta_data( '_merchant_order_pre_order_shipping_date', max( $shipping_dates ) );
333 $order->save();
334 $this->trigger_emails( $order );
335 }
336 }
337
338 /**
339 * Group pre-order products into one order and keep the original order.
340 *
341 * @param WC_Order $order The order object.
342 *
343 * @return void
344 */
345 public function group_pre_order_into_one_order( $order ) {
346 // Get the original order
347 $original_order = $order;
348
349 // Store sub-orders IDs
350 $sub_order_ids = $pre_order_products = array();
351
352 // Loop through each product in the original order
353 foreach ( $original_order->get_items() as $item_id => $item ) {
354 if ( $this->is_pre_order( $item->get_product()->get_id() ) ) {
355 $pre_order_products[] = $item;
356 }
357 }
358
359 if ( ! empty( $pre_order_products ) ) {
360 $shipping_dates = array();
361 // Create a new order
362 $new_order = wc_create_order(
363 array(
364 'parent' => $original_order->get_id(),
365 )
366 );
367
368 // Copy order details from original order to new order.
369 $this->copy_order_details( $original_order, $new_order );
370
371 $new_order->add_meta_data( '_is_sub_order', true );
372 $new_order->add_meta_data( '_is_pre_order', true );
373 $new_order->set_status( 'wc-pre-ordered' );
374
375 // Copy order meta data from original order
376 foreach ( $original_order->get_meta_data() as $meta ) {
377 $new_order->add_meta_data( $meta->key, $meta->value );
378 }
379
380 // add pre-order products to the new order
381 foreach ( $pre_order_products as $item ) {
382 $rule = self::available_product_rule( $item->get_product()->get_id() );
383 $new_item = $this->clone_order_item( $item );
384 $new_item->add_meta_data( '_merchant_pre_order', $rule );
385 $new_item->add_meta_data( '_merchant_is_pre_order_product', true );
386 $new_item->add_meta_data( '_merchant_pre_order_shipping_date', $rule['shipping_timestamp'] );
387 $new_order->add_item( $new_item );
388 $shipping_dates[] = $rule['shipping_timestamp'];
389 }
390
391 // Calculate totals and set status
392 $new_order->calculate_totals();
393 $new_order->add_meta_data( '_merchant_order_pre_order_shipping_date', max( $shipping_dates ) );
394 $new_order->save();
395
396 $sub_order_ids[] = $new_order->get_id();
397
398 $this->trigger_emails( $new_order );
399
400 // Save sub-order IDs to the original order
401 $original_order->update_meta_data( '_sub_order_ids', $sub_order_ids );
402
403
404 // Update original order totals after removing items
405 $original_order->calculate_totals();
406 $original_order->save();
407 }
408 }
409
410 private function clone_order_item( $item ) {
411 $new_item = new WC_Order_Item_Product();
412 $new_item->set_product_id( $item->get_product_id() );
413 $new_item->set_variation_id( $item->get_variation_id() );
414 $new_item->set_name( $item->get_name() );
415 $new_item->set_quantity( $item->get_quantity() );
416 $new_item->set_subtotal( $item->get_subtotal() );
417 $new_item->set_total( $item->get_total() );
418 $new_item->set_taxes( $item->get_taxes() );
419 $new_item->set_meta_data( $item->get_meta_data() );
420
421 return $new_item;
422 }
423
424 /**
425 * Generate separate orders for each pre-order product.
426 *
427 * @param WC_Order $order The order object.
428 *
429 * @return void
430 */
431 public function separate_order_for_pre_orders( $order ) {
432 // Get the original order
433 $original_order = $order;
434
435 // Store sub-orders IDs
436 $sub_order_ids = array();
437
438 // Loop through each product in the original order
439 foreach ( $original_order->get_items() as $item_id => $item ) {
440 //check if the item is a product
441
442 // Get product details
443 $product_id = $item->get_product()->get_id();
444 if ( ! $product_id ) {
445 continue;
446 }
447
448 // check if the product is a pre-order
449 if ( ! $this->is_pre_order( $product_id ) ) {
450 continue;
451 }
452
453 // Create a new order
454 $new_order = wc_create_order(
455 array(
456 'parent' => $original_order->get_id(),
457 )
458 );
459
460 $new_item = $this->clone_order_item( $item );
461 $rule = self::available_product_rule( $product_id );
462 $new_item->add_meta_data( '_merchant_pre_order', $rule );
463 $new_item->add_meta_data( '_merchant_is_pre_order_product', true );
464 $new_item->add_meta_data( '_merchant_pre_order_shipping_date', $rule['shipping_timestamp'] );
465 $new_order->add_item( $new_item );
466
467 // Copy order details from original order to new order.
468 $this->copy_order_details( $original_order, $new_order );
469
470 // Calculate totals and set status
471 $new_order->calculate_totals();
472
473 // Save new order and store its ID
474 $new_order->add_meta_data( '_is_sub_order', true );
475 $new_order->add_meta_data( '_is_pre_order', true );
476 $new_order->add_meta_data( '_merchant_order_pre_order_shipping_date', $rule['shipping_timestamp'] );
477 $new_order->set_status( 'wc-pre-ordered' );
478 $new_order->save();
479 $this->trigger_emails( $new_order );
480 $sub_order_ids[] = $new_order->get_id();
481 }
482
483 // Save sub-order IDs to the original order
484 $original_order->update_meta_data( '_sub_order_ids', $sub_order_ids );
485
486 // Update original order totals after removing items
487 $original_order->calculate_totals();
488 $original_order->save();
489 }
490
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 /**
510 * Hide order item meta that will be used for internal use only.
511 *
512 * @param $args array The hidden order item meta.
513 *
514 * @return array The hidden order item meta.
515 */
516 function hidden_order_itemmeta( $args ) {
517 $args[] = '_merchant_pre_order';
518 $args[] = '_merchant_is_pre_order_product';
519 $args[] = '_merchant_pre_order_shipping_date';
520
521 return $args;
522 }
523
524 /**
525 * Copy order details from the original order to the new child order.
526 *
527 * @param $original_order WC_Order The original order.
528 * @param $new_order WC_Order The new child order.
529 *
530 * @return void
531 */
532 private function copy_order_details( $original_order, $new_order ) {
533 $new_order->set_customer_id( $original_order->get_customer_id() );
534
535 // set customer billing details
536 $new_order->set_billing_first_name( $original_order->get_billing_first_name() );
537 $new_order->set_billing_last_name( $original_order->get_billing_last_name() );
538 $new_order->set_billing_phone( $original_order->get_billing_phone() );
539 $new_order->set_billing_email( $original_order->get_billing_email() );
540 $new_order->set_billing_address_1( $original_order->get_billing_address_1() );
541 $new_order->set_billing_address_2( $original_order->get_billing_address_2() );
542 $new_order->set_billing_city( $original_order->get_billing_city() );
543 $new_order->set_billing_state( $original_order->get_billing_state() );
544 $new_order->set_billing_postcode( $original_order->get_billing_postcode() );
545 $new_order->set_billing_country( $original_order->get_billing_country() );
546
547 // set customer shipping details
548 $new_order->set_shipping_first_name( $original_order->get_shipping_first_name() );
549 $new_order->set_shipping_last_name( $original_order->get_shipping_last_name() );
550 $new_order->set_shipping_address_1( $original_order->get_shipping_address_1() );
551 $new_order->set_shipping_address_2( $original_order->get_shipping_address_2() );
552 $new_order->set_shipping_city( $original_order->get_shipping_city() );
553 $new_order->set_shipping_state( $original_order->get_shipping_state() );
554 $new_order->set_shipping_postcode( $original_order->get_shipping_postcode() );
555 $new_order->set_shipping_country( $original_order->get_shipping_country() );
556
557 $new_order->set_customer_note( $original_order->get_customer_note() );
558 /**
559 * This action is documented in woocommerce/includes/class-wc-checkout.php
560 *
561 * @since 3.0.0 or earlier
562 */
563 $new_order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) );
564 $new_order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
565 $new_order->set_customer_user_agent( wc_get_user_agent() );
566 $new_order->set_currency( get_woocommerce_currency() );
567 $new_order->set_created_via( 'checkout' );
568
569 // Copy order meta data from original order
570 foreach ( $original_order->get_meta_data() as $meta ) {
571 $new_order->add_meta_data( $meta->key, $meta->value );
572 }
573 }
574
575 /**
576 * Update the product price html based on the offer.
577 *
578 * @return string The price html.
579 */
580 public function dynamic_discount_price_html( $html_price, $product ) {
581 /**
582 * Disable the sale price change.
583 *
584 * @param bool $disable_sale_price The sale price change status.
585 * @param string $html_price The price.
586 * @param WC_Product $product The product object.
587 *
588 * @since 1.9.9
589 */
590 if ( apply_filters( 'merchant_pre_order_disable_sale_price_html', false, $html_price, $product ) ) {
591 return $html_price;
592 }
593
594 if ( ( ! is_admin() || wp_doing_ajax() ) && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
595 if ( '' === $product->get_price() ) {
596 return $html_price;
597 }
598
599 $offer = self::available_product_rule( $product->get_id() );
600
601 if ( empty( $offer ) && $product->is_type( 'variation' ) ) {
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 }
607 }
608
609 if ( $product->is_type( 'variable' ) ) {
610 $html_price = $this->variable_product_price_html( $product, $offer, $html_price );
611 } else {
612 $html_price = $this->simple_product_price_html( $product, $offer, $html_price );
613 }
614 }
615
616 return $html_price;
617 }
618
619 /**
620 * Variable product price html.
621 *
622 * @param WC_Product $product The product object.
623 *
624 * @return string The price html.
625 */
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
632 $prices = array();
633 $variations = $product->get_children();
634
635 $regular_prices = array();
636 $sale_prices = array();
637
638 foreach ( $variations as $variation_id ) {
639 $variation = wc_get_product( $variation_id );
640 $variation_offer = self::available_product_rule( $variation_id );
641 $regular_price = $variation->get_regular_price();
642
643 $regular_prices[] = $regular_price;
644
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 ) ) {
657 if ( $variation->is_on_sale() ) {
658 $sale_price = wc_get_price_to_display( $variation );
659 } else {
660 $sale_price = $regular_price;
661 }
662 $prices[] = $sale_price;
663 $sale_prices[] = $sale_price;
664 continue;
665 }
666
667 $sale_price = $this->calculate_discounted_price( $regular_price, $variation_offer, $variation );
668 if ( $sale_price <= 0 ) {
669 // If the price is less than 0, set it to the regular/sale price
670 if ( $variation->is_on_sale() ) {
671 $sale_price = wc_get_price_to_display( $variation );
672 } else {
673 $sale_price = $regular_price;
674 }
675 }
676 $prices[] = $sale_price;
677 $sale_prices[] = $sale_price;
678 }
679
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 );
687 }
688
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 '';
703 }
704
705 /**
706 * Simple product price html.
707 *
708 * @param WC_Product $product The product object.
709 * @param array $offer The offer details.
710 *
711 * @return string The price html.
712 */
713 private function simple_product_price_html( $product, $offer, $html_price ) {
714 $sale = Merchant_Pre_Orders_Rules_Repository::get_rule_sale( $offer );
715 if ( ! $sale ) {
716 return $html_price;
717 }
718 $regular_price = wc_get_price_to_display( $product, array( 'price' => $product->get_regular_price() ) );
719 $discounted_price = $this->calculate_discounted_price( $regular_price, $offer, $product );
720
721 return wc_format_sale_price( $regular_price, $discounted_price );
722 }
723
724 /**
725 * Calculate the offer discount for certain price.
726 *
727 * @param float $price The price.
728 * @param array $offer The offer.
729 * @param WC_Product $product The product object (only supplied to the filter).
730 *
731 * @return float The discounted price.
732 */
733 public function calculate_discounted_price( $price, $offer, $product = null ) {
734 $sale = Merchant_Pre_Orders_Rules_Repository::get_rule_sale( $offer );
735 $discount_type = $discount_value = '';
736 if ( $sale ) {
737 $discount_type = $offer['discount_type'];
738 $discount_value = $offer['discount_amount'];
739 if ( 'percentage' === $discount_type ) {
740 $price = (float) $price - ( (float) $price * ( (float) $discount_value / 100 ) );
741 } else {
742 $price = (float) $price - (float) $discount_value;
743 }
744 }
745
746 /**
747 * Filter the discounted price.
748 *
749 * @param float $price The price.
750 * @param array $offer The offer.
751 * @param string $discount_type The discount type.
752 * @param float $discount_value The discount value.
753 * @param WC_Product $product The product object.
754 *
755 * @since 1.9.5
756 */
757 return apply_filters(
758 'merchant_pre_order_sale_calculate_discounted_price',
759 $price,
760 $offer,
761 $discount_type,
762 $discount_value,
763 $product
764 );
765 }
766
767 /**
768 * Set the discounted cart price for the product.
769 *
770 * @param $cart_object array The cart object.
771 *
772 * @return void
773 */
774 public function dynamic_discount_cart_price() {
775 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
776 /**
777 * Disable the cart price change.
778 *
779 * @param bool $disable_cart_price The cart price change status.
780 * @param array $cart_item The cart item.
781 * @param string $cart_item_key The cart item key.
782 *
783 * @since 1.9.7
784 */
785 if ( apply_filters( 'merchant_pre_order_sale_disable_cart_price', false, $cart_item, $cart_item_key ) ) {
786 continue;
787 }
788 $product_id = $cart_item['data']->get_id();
789 $product = wc_get_product( $product_id );
790
791 if ( ( ! is_admin() || wp_doing_ajax() ) && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
792 $offer = self::available_product_rule( $product->get_id() );
793
794 if ( $product->is_type( 'variable' ) ) {
795 $product_id = $cart_item['variation_id'];
796 $product = wc_get_product( $product_id );
797 // check if the variation has an offer
798 $offer = self::available_product_rule( $product_id );
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
810 if ( empty( $offer ) ) {
811 continue;
812 }
813 if ( ! isset( $offer['discount_toggle'] ) ) {
814 continue;
815 }
816 $regular_price = $product->get_regular_price();
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 );
833 $cart_item['data']->set_price( $sale_price );
834 $cart_item['data']->set_meta_data( array( '_merchant_pre_order_sale' => $offer ) );
835 }
836 }
837 }
838
839 /**
840 * Validation (one type only).
841 *
842 * @param boolean $passed
843 * @param integer $product_id
844 *
845 * @return boolean
846 */
847 public function allow_one_type_only( $passed, $product_id ) {
848 $products = array_filter( WC()->cart->get_cart_contents() );
849 $pre_orders = 0;
850 $has_pre_orders = false;
851 foreach ( $products as $product ) {
852 $is_pre_order = $this->is_pre_order( $product['data']->get_id() );
853 if ( $is_pre_order ) {
854 ++ $pre_orders;
855 }
856 }
857 if ( $pre_orders ) {
858 $has_pre_orders = true;
859 }
860 $input_post_data = array(
861 'variation_id' => filter_input( INPUT_POST, 'variation_id', FILTER_SANITIZE_NUMBER_INT ),
862 );
863
864 $variable_id = ( isset( $input_post_data['variation_id'] ) ) ? sanitize_text_field( wp_unslash( $input_post_data['variation_id'] ) ) : 0;
865 $is_variable_has_pre_order = $this->is_pre_order( $product_id ) || $this->is_pre_order( $variable_id );
866
867 if ( empty( $products ) || ( $is_variable_has_pre_order && $has_pre_orders ) || ( false === $is_variable_has_pre_order && false === $has_pre_orders ) ) {
868 $passed = true;
869 } else {
870 $mode = Merchant_Admin_Options::get( self::MODULE_ID, 'modes', 'unified_order' );
871 if ( 'only_pre_orders' === $mode ) {
872 $passed = false;
873 if ( $is_variable_has_pre_order ) {
874 $notice = esc_html__( 'We detected that you are trying to add a pre-order product in your cart. Please remove the rest of the products before proceeding.',
875 'merchant' );
876 } else {
877 $notice = esc_html__( 'We detected that your cart has pre-order products. Please remove them before being able to add this product.', 'merchant' );
878 }
879 wc_add_notice( $notice, 'error' );
880 }
881 }
882
883 return $passed;
884 }
885
886 /**
887 * Check if product is a pre order item by product ID.
888 *
889 * @param integer $product_id
890 *
891 * @return boolean
892 */
893 public function is_pre_order( $product_id ) {
894 $available_pre_order = self::available_product_rule( $product_id );
895
896 return ! empty( $available_pre_order );
897 }
898
899 /**
900 * Check for pre orders and maybe update the status.
901 *
902 * @return void
903 */
904 public function check_for_pre_orders_and_maybe_update_status() {
905 $args = array(
906 'status' => 'wc-pre-ordered',
907 );
908
909 $pre_ordered_orders = wc_get_orders( $args );
910
911 foreach ( $pre_ordered_orders as $order ) {
912 $pre_order_date = $order->get_meta( '_merchant_order_pre_order_shipping_date' );
913
914 if ( $pre_order_date < time() ) {
915 $parent_order_id = $order->get_parent_id();
916
917 if ( 0 !== $parent_order_id ) {
918 $parent_order = wc_get_order( $parent_order_id );
919
920 if ( $parent_order->get_status() === 'completed' ) {
921 $order->update_status( 'processing', '[Merchant Pre Orders] ' );
922 }
923 } elseif ( $order->payment_complete() ) {
924 $order->update_status( 'processing', '[Merchant Pre Orders] ' );
925 }
926 }
927 }
928 }
929
930 /**
931 * Change pre-order button text.
932 *
933 * @param string $text
934 * @param WC_Product $product
935 *
936 * @return string
937 */
938 public function change_button_text( $text, $product ) {
939 if ( $product && $this->is_pre_order( $product->get_id() ) ) {
940 $pre_order_rule = self::available_product_rule( $product->get_id() );
941 $text = $pre_order_rule['button_text'] ? Merchant_Translator::translate( $pre_order_rule['button_text'] ) : esc_html__( 'Pre Order Now!', 'merchant' );
942 }
943
944 return $text;
945 }
946
947 /**
948 * Change pre-order button text for variable products.
949 *
950 * @param array $data variation data.
951 * @param WC_Product $product The product object.
952 * @param WC_Product_Variation $variation The variation object.
953 *
954 * @return array
955 */
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';
964
965 $additional_text = $pre_order_rule['additional_text'] ? Merchant_Translator::translate( $pre_order_rule['additional_text'] )
966 : esc_html__( 'Ships on {date}.', 'merchant' );
967 $time_format = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
968 $text = $this->replace_date_text( $additional_text, $time_format );
969
970 if ( ! empty( $text ) ) {
971 $data['is_pre_order_date'] = $this->replace_date_text( $additional_text, $time_format );
972 }
973 }
974
975 return $data;
976 }
977
978 /**
979 * Replace {date} markup with new text.
980 *
981 * @param string $string_contains_date_tag
982 * @param string $time_format
983 *
984 * @return string
985 */
986 public function replace_date_text( $string_contains_date_tag, $time_format ) {
987 $from = array( '{date}' );
988 $to = array( $time_format );
989
990 return str_replace( $from, $to, $string_contains_date_tag );
991 }
992
993 /**
994 * Display pre order additional information before add to cart form.
995 *
996 * @return void
997 */
998 public function additional_information_before_cart_form() {
999 $input_post_data = array(
1000 'product_id' => filter_input( INPUT_POST, 'product_id', FILTER_SANITIZE_NUMBER_INT ),
1001 );
1002
1003 global $post, $product;
1004
1005 // Do not override globals.
1006 $_post = $post;
1007 $_product = $product;
1008
1009 // In some cases the $post might be null. e.g inside quick view popup.
1010 if ( ! $_post && isset( $input_post_data['product_id'] ) ) {
1011 $_post = get_post( absint( $input_post_data['product_id'] ) );
1012 $_product = wc_get_product( $_post->ID );
1013 }
1014 if ( $this->is_pre_order( $_post->ID ) ) {
1015 $pre_order_rule = self::available_product_rule( $_product->get_id() );
1016 if ( ! empty( $pre_order_rule ) && $pre_order_rule['placement'] === 'before' ) {
1017 $this->maybe_render_additional_information( $pre_order_rule );
1018 }
1019 }
1020 }
1021
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 /**
1083 * Display pre order additional information after add to cart form.
1084 *
1085 * @return void
1086 */
1087 public function additional_information_after_cart_form() {
1088 $input_post_data = array(
1089 'product_id' => filter_input( INPUT_POST, 'product_id', FILTER_SANITIZE_NUMBER_INT ),
1090 );
1091
1092 global $post, $product;
1093
1094 // Do not override globals.
1095 $_post = $post;
1096 $_product = $product;
1097
1098 // In some cases the $post might be null. e.g inside quick view popup.
1099 if ( ! $_post && isset( $input_post_data['product_id'] ) ) {
1100 $_post = get_post( absint( $input_post_data['product_id'] ) );
1101 $_product = wc_get_product( $_post->ID );
1102 }
1103 if ( $_product && $this->is_pre_order( $_post->ID ) ) {
1104 $pre_order_rule = self::available_product_rule( $_product->get_id() );
1105 if ( ! empty( $pre_order_rule ) && $pre_order_rule['placement'] === 'after' ) {
1106 $this->maybe_render_additional_information( $pre_order_rule );
1107 }
1108 }
1109 }
1110
1111 /**
1112 * Maybe render additional information.
1113 *
1114 * @return void
1115 */
1116 public function maybe_render_additional_information( $rule ) {
1117 if ( ! empty( $rule ) ) {
1118 $additional_text = $rule['additional_text'] ? Merchant_Translator::translate( $rule['additional_text'] )
1119 : esc_html__( 'Ships on {date}.', 'merchant' );
1120 $time_format = date_i18n( get_option( 'date_format' ), $rule['shipping_timestamp'] );
1121 $text = $this->replace_date_text( $additional_text, $time_format );
1122
1123 printf( '<div class="merchant-pre-orders-date">%s</div>', esc_html( $text ) );
1124 }
1125 }
1126
1127 /**
1128 * Register pre orders status.
1129 *
1130 * @return void
1131 */
1132 public function register_pre_orders_order_status() {
1133 register_post_status( 'wc-pre-ordered', array(
1134 'label' => esc_html__( 'Pre Ordered', 'merchant' ),
1135 'public' => true,
1136 'show_in_admin_status_list' => true,
1137 'show_in_admin_all_list' => true,
1138 'exclude_from_search' => false,
1139 /* translators: %s: pre ordered product count */
1140 'label_count' => _n_noop( 'Pre Ordered <span class="count">(%s)</span>', 'Pre Ordered <span class="count">(%s)</span>', 'merchant' ),
1141 ) );
1142 }
1143
1144 /**
1145 * Add pre orders status to order statuses.
1146 *
1147 * @param array $order_statuses
1148 *
1149 * @return array
1150 */
1151 public function add_pre_orders_order_statuses( $order_statuses ) {
1152 $order_statuses['wc-pre-ordered'] = esc_html__( 'Pre-Ordered', 'merchant' );
1153
1154 return $order_statuses;
1155 }
1156
1157 /**
1158 * Add pre orders class identifies to the body tag.
1159 *
1160 */
1161 public function pre_orders_post_class( $classes, $product ) {
1162 if ( $this->is_pre_order( $product->get_id() ) ) {
1163 $classes[] = 'merchant-pre-ordered-product';
1164 }
1165
1166 return $classes;
1167 }
1168
1169 /**
1170 * Filters cart item data to display cart.
1171 *
1172 * @param array $item_data Cart item data.
1173 * @param array $cart_item Cart item array.
1174 *
1175 * @return array
1176 *
1177 * @see wc_get_formatted_cart_item_data for filter usage.
1178 */
1179 public function cart_message_handler( $item_data, $cart_item ) {
1180 $product_id = $cart_item['product_id'];
1181 if ( $cart_item['data']->is_type( 'variation' ) ) {
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 }
1190 }
1191
1192 if ( $this->is_pre_order( $product_id ) ) {
1193 $pre_order_rule = self::available_product_rule( $product_id );
1194 if ( $pre_order_rule ) {
1195 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
1196 $pre_order_date = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
1197
1198 $item_data[] = array(
1199 'key' => $label_text,
1200 'value' => $pre_order_date,
1201 'display' => '',
1202 );
1203 }
1204 }
1205
1206 return $item_data;
1207 }
1208
1209 /**
1210 * filter name: woocommerce_order_item_meta_end
1211 * Adds pre order additional text to the order item name
1212 *
1213 * @param int $item_id Product ID.
1214 * @param \WC_Order_Item_Product $item Item array data.
1215 * @param \WC_Order $order Order data.
1216 * @param bool $plain_text Is plain text or not.
1217 *
1218 * @return string
1219 */
1220 public function order_item_meta_end( $item_id, $item, $order, $plain_text ) {
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 }
1224 }
1225
1226 /**
1227 * Render pre order text.
1228 */
1229 public function shop_loop_item_title() {
1230 echo $this->get_pre_order_text( get_the_ID(), 'span' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1231 }
1232
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 /**
1298 * Add the_title filter for block rendering.
1299 *
1300 * @param array $context Default context.
1301 *
1302 * @return array
1303 */
1304 public function add_block_title_filter( $context ) {
1305 if ( ! empty( $context['postType'] ) && 'product' === $context['postType'] && ! $this->is_pre_order_filter_on && ! is_singular( 'product' ) ) {
1306 $this->is_pre_order_filter_on = true;
1307 add_filter( 'the_title', array( $this, 'block_add_the_title_filter' ), 10, 2 );
1308 add_filter( 'render_block', array( $this, 'block_remove_the_title_filter' ), 10, 3 );
1309 }
1310
1311 return $context;
1312 }
1313
1314 /**
1315 * Add pre order info to product grid block.
1316 *
1317 * @param string $html Product grid item HTML.
1318 * @param object $data Product data passed to the template.
1319 * @param \WC_Product $product Product object.
1320 *
1321 * @return string Updated product grid item HTML.
1322 */
1323 public function override_product_grid_block( $html, $data, $product ) {
1324 $pre_order_text = $this->get_pre_order_text( $product->get_id(), 'span' );
1325 if ( $pre_order_text ) {
1326 $html = str_replace( $data->title, $data->title . $pre_order_text, $html );
1327 }
1328
1329 return $html;
1330 }
1331
1332 /**
1333 * Adds pre order info to post_title.
1334 *
1335 * @param string $title Title.
1336 * @param int $post_id Post ID.
1337 *
1338 * @return string
1339 */
1340 public function block_add_the_title_filter( $title, $post_id ) {
1341 return $title . $this->get_pre_order_text( $post_id, 'span' );
1342 }
1343
1344 /**
1345 * Remove the_title filter when block rendering finished.
1346 *
1347 * @param string $block_content The block content.
1348 * @param array $parsed_block The full block, including name and attributes.
1349 * @param WP_Block $block The block instance.
1350 *
1351 * @return string
1352 */
1353 public function block_remove_the_title_filter( $block_content, $parsed_block, $block ) {
1354 if ( $this->is_pre_order_filter_on ) {
1355 remove_filter( 'the_title', array( $this, 'block_add_the_title_filter' ), 10 );
1356 $this->is_pre_order_filter_on = false;
1357 }
1358
1359 return $block_content;
1360 }
1361
1362 /**
1363 * Get pre order text by product ID.
1364 *
1365 * @param int $product_id
1366 * @param string $render_type Render template type string. 'span', 'dl', ''. Default ''.
1367 *
1368 * @return string
1369 */
1370 private function get_pre_order_text( $product_id, $render_type = '' ) {
1371 if ( ! $this->is_pre_order( $product_id ) ) {
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 }
1382 }
1383
1384 $pre_order_rule = self::available_product_rule( $product_id );
1385 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
1386 $pre_order_date = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
1387 if ( 'span' === $render_type ) {
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 );
1393 } elseif ( 'dl' === $render_type ) {
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 );
1399 } else {
1400 return sprintf(
1401 '%s: %s',
1402 esc_html( $label_text ),
1403 $pre_order_date
1404 );
1405 }
1406 }
1407
1408 /**
1409 * Get the product types that are allowed to have storewide sale.
1410 *
1411 * @return array The allowed product types.
1412 */
1413 public function allowed_product_types() {
1414 /**
1415 * Filter the product types that are allowed to have storewide sale.
1416 *
1417 * @param array $product_types
1418 *
1419 * @since 1.9.5
1420 */
1421 return apply_filters( 'merchant_pre_order_allowed_product_types', array(
1422 'simple',
1423 'variation',
1424 'variable',
1425 ) );
1426 }
1427
1428 /**
1429 * Get the pre-order rules.
1430 *
1431 * @param array $rule The rule to get.
1432 *
1433 * @return array|false The pre-order rules or false if there are no rule sale.
1434 */
1435 /**
1436 * Get the available product rule.
1437 *
1438 * @param int $product_id The product ID.
1439 *
1440 * @return array The available product rule.
1441 */
1442 public static function available_product_rule( $product_id ) {
1443 static $cache = array();
1444
1445 if ( isset( $cache[ $product_id ] ) ) {
1446 return $cache[ $product_id ];
1447 }
1448
1449 $cache[ $product_id ] = Merchant_Pre_Orders_Rules_Repository::get_rule_for_product( $product_id );
1450
1451 return $cache[ $product_id ];
1452 }
1453
1454 /**
1455 * Migrate exclusion fields from old format to new individual toggles.
1456 *
1457 * @return void
1458 */
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;
1465 }
1466
1467 $rules = Merchant_Pre_Orders_Rules_Repository::get_global_rules();
1468 if ( empty( $rules ) ) {
1469 update_option( $option_name, true );
1470
1471 return;
1472 }
1473
1474 $needs_migration = false;
1475
1476 foreach ( $rules as $rule_key => $rule ) {
1477 $n = 0;
1478
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 }
1484
1485 if ( ! isset( $rule['exclude_categories_toggle'] ) ) {
1486 $rules[ $rule_key ]['exclude_categories_toggle'] = ! empty( $rule['excluded_categories'] );
1487 ++ $n;
1488 }
1489
1490 if ( ! isset( $rule['exclude_tags_toggle'] ) ) {
1491 $rules[ $rule_key ]['exclude_tags_toggle'] = ! empty( $rule['excluded_tags'] );
1492 ++ $n;
1493 }
1494
1495 if ( ! isset( $rule['exclude_brands_toggle'] ) ) {
1496 $rules[ $rule_key ]['exclude_brands_toggle'] = ! empty( $rule['excluded_brands'] );
1497 ++ $n;
1498 }
1499
1500 if ( $n > 0 ) {
1501 $needs_migration = true; // Set to true if any iteration needs migration
1502 }
1503 }
1504
1505 if ( $needs_migration ) {
1506 Merchant_Admin_Options::set( self::MODULE_ID, 'rules', $rules );
1507 }
1508
1509 // Mark migration as completed
1510 update_option( $option_name, true );
1511 }
1512
1513 /**
1514 * Migrate old data to the new module storage.
1515 *
1516 * @return void
1517 */
1518 public static function data_migration() {
1519 $args = array(
1520 'post_type' => array( 'product', 'product_variation' ),
1521 'posts_per_page' => - 1,
1522 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1523 'meta_query' => array(
1524 'relation' => 'AND',
1525 array(
1526 'key' => '_pre_order_date',
1527 'compare' => 'EXISTS',
1528 ),
1529 array(
1530 'key' => '_pre_order_date',
1531 'compare' => '!=',
1532 'value' => '',
1533 ),
1534 array(
1535 'key' => '_is_pre_order',
1536 'compare' => 'EXISTS',
1537 ),
1538 array(
1539 'key' => '_is_pre_order',
1540 'compare' => 'in',
1541 'value' => array( 'yes', '1' ),
1542 ),
1543 ),
1544 );
1545 $migrated = get_option( 'merchant_pre_orders_migrated' );
1546 if ( ! $migrated ) {
1547 $products = new WP_Query( $args );
1548 if ( $products->have_posts() ) {
1549 $rules = Merchant_Pre_Orders_Rules_Repository::get_global_rules();
1550 while ( $products->have_posts() ) {
1551 $products->the_post();
1552 $product_id = get_the_ID();
1553 $pre_order_date = get_post_meta( $product_id, '_pre_order_date', true );
1554 $is_pre_order = get_post_meta( $product_id, '_is_pre_order', true );
1555 $pre_order_date = merchant_convert_date_to_timestamp( $pre_order_date, 'yy-m-d' );
1556 $pre_order_date = date_i18n( self::DATE_TIME_FORMAT, $pre_order_date );
1557 if ( $is_pre_order ) {
1558 $rules[] = array(
1559 'offer-title' => esc_html__( 'Custom Pre-order', 'merchant' ),
1560 'trigger_on' => 'product',
1561 'product_ids' => $product_id,
1562 'discount_toggle' => false,
1563 'user_condition' => 'all',
1564 'shipping_date' => $pre_order_date,
1565 'button_text' => esc_html__( 'Pre Order Now!', 'merchant' ),
1566 'additional_text' => esc_html__( 'Ships on {date}.', 'merchant' ),
1567 'placement' => 'before',
1568 'cart_label_text' => esc_html__( 'Ships on', 'merchant' ),
1569 'layout' => 'rule-details',
1570 );
1571 }
1572 }
1573 Merchant_Admin_Options::set( self::MODULE_ID, 'rules', $rules );
1574 }
1575 update_option( 'merchant_pre_orders_migrated', true );
1576 }
1577 }
1578 }
1579
1580 add_action( 'init', 'Merchant_Pre_Orders_Main_Functionality::data_migration' );
1581 add_action( 'init', 'Merchant_Pre_Orders_Main_Functionality::migrate_exclusion_fields' );
1582