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 +1581 -536 1.82.3.2 View file →
@@ -1,536 +1,1581 @@
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 - * Pre order products.
21 - *
22 - */
23 - private $pre_order_products = [];
24 -
25 - /**
26 - * Init.
27 - *
28 - * @return void
29 - */
30 - public function init() {
31 - add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'allow_one_type_only' ), 99, 2 );
32 -
33 - add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'manage_pre_orders' ), 10, 2 );
34 - add_filter( 'woocommerce_thankyou', array( $this, 'set_pre_order_status'), 10 );
35 - add_filter( 'woocommerce_billing_fields', array( $this, 'add_shipping_date_field' ) );
36 -
37 - // Cronjob.
38 - if ( ! wp_next_scheduled( 'check_for_released_preorders' ) ) {
39 - wp_schedule_event( time(), 'twicedaily', 'check_for_released_preorders' );
40 - }
41 -
42 - add_action( 'check_for_released_preorders', array( $this, 'check_for_pre_orders_and_maybe_update_status' ) );
43 -
44 - // Variations tab
45 - add_action( 'woocommerce_product_after_variable_attributes', array( $this, 'custom_fields_for_variable_products' ), 10, 3 );
46 -
47 - // Inventory tab
48 - add_action( 'woocommerce_product_options_stock_status', array( $this, 'custom_fields_for_simple_products' ) );
49 -
50 - add_action( 'woocommerce_save_product_variation', array( $this, 'custom_fields_for_variable_products_save' ), 10, 2 );
51 - add_action( 'woocommerce_process_product_meta', array( $this, 'custom_fields_for_simple_products_save' ), 10, 2 );
52 -
53 - add_filter( 'woocommerce_product_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
54 - add_filter( 'woocommerce_product_single_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
55 - add_filter( 'woocommerce_available_variation', array( $this, 'change_button_text_for_variable_products' ), 10, 3 );
56 - add_action( 'woocommerce_before_add_to_cart_form', array( $this, 'maybe_render_additional_information' ), 10 );
57 -
58 - $this->register_pre_orders_order_status();
59 - add_filter( 'wc_order_statuses', array( $this, 'add_pre_orders_order_statuses' ) );
60 - add_filter( 'woocommerce_post_class', array( $this, 'pre_orders_post_class' ), 10, 3 );
61 - }
62 -
63 - /**
64 - * Validation (one type only).
65 - *
66 - * @param boolean $passed
67 - * @param integer $product_id
68 - * @return boolean
69 - */
70 - public function allow_one_type_only( $passed, $product_id ) {
71 - $products = array_filter( WC()->cart->get_cart_contents() );
72 - $has_pre_orders = false;
73 -
74 - foreach ( $products as $product ) {
75 - $isPreOrder = $this->isPreOrder( $product['data']->get_id() );
76 - if ( $isPreOrder ) {
77 - $has_pre_orders = true;
78 - }
79 - }
80 -
81 - $input_post_data = array(
82 - 'variation_id' => filter_input(INPUT_POST, 'variation_id', FILTER_SANITIZE_NUMBER_INT)
83 - );
84 -
85 - $variableId = ( isset( $input_post_data['variation_id'] ) ) ? sanitize_text_field( wp_unslash( $input_post_data['variation_id'] ) ) : 0;
86 - $is_variable_has_pre_order = $this->isPreOrder( $product_id, $variableId );
87 -
88 - if ( empty( $products ) || ( $is_variable_has_pre_order && $has_pre_orders ) || ( false === $is_variable_has_pre_order && false === $has_pre_orders ) ) {
89 - $passed = true;
90 - } else {
91 - $passed = false;
92 - if ( $is_variable_has_pre_order ) {
93 - $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.', 'merchant' );
94 - } else {
95 - $notice = esc_html__( 'We detected that your cart has pre-order products. Please remove them before being able to add this product.', 'merchant' );
96 - }
97 - wc_add_notice( $notice, 'error' );
98 - }
99 -
100 - return $passed;
101 - }
102 -
103 - /**
104 - * Check if is pre order.
105 - *
106 - * @param integer $product_id
107 - * @param integer $variableId
108 - * @return boolean
109 - */
110 - public function isPreOrder( $product_id, $variableId = 0 ) {
111 - if ( 'yes' === get_post_meta( $product_id, '_is_pre_order', true ) && new DateTime( get_post_meta( $product_id, '_pre_order_date', true ) ) > new DateTime() ) {
112 - return true;
113 - } elseif ( 'yes' === get_post_meta( $variableId, '_is_pre_order', true ) && new DateTime( get_post_meta( $variableId, '_pre_order_date', true ) ) > new DateTime() ) {
114 - return true;
115 - }
116 -
117 - return false;
118 - }
119 -
120 - /**
121 - * Update pre order meta data (date).
122 - *
123 - * @param integer $orderId
124 - * @param array $data
125 - * @return void
126 - */
127 - public function manage_pre_orders( $orderId, $data ) {
128 - $order = wc_get_order( $orderId );
129 -
130 - if ( isset( $data['preorder_date'] ) ) {
131 - $order->update_meta_data( '_preorder_date', esc_attr( $data['preorder_date'] ) );
132 - $order->save();
133 - }
134 - }
135 -
136 - /**
137 - * Set pre order status.
138 - *
139 - * @param string $status
140 - * @param integer $order_id
141 - * @param object $order
142 - * @return string
143 - */
144 - public function set_pre_order_status( $order_id ) {
145 - if ( ! $order_id ) {
146 - return;
147 - }
148 -
149 - $order = wc_get_order($order_id);
150 -
151 - // Change the order status.
152 - if ( $order->get_meta( '_preorder_date' ) ) {
153 - $order->update_status( 'wc-pre-ordered' );
154 - }
155 -
156 - // Save.
157 - $order->save();
158 - }
159 -
160 - /**
161 - * Add shipping date field.
162 - *
163 - * @param array $fields
164 - * @return array
165 - */
166 - public function add_shipping_date_field( $fields ) {
167 - if ( ! is_checkout() && ! is_cart() ) {
168 - return $fields;
169 - }
170 -
171 - global $woocommerce;
172 -
173 - $this->check_pre_order_products( $woocommerce->cart->get_cart() );
174 -
175 - if ( count( $this->get_pre_order_products() ) > 0 ) {
176 - $fields['preorder_date'] = [
177 - 'type' => 'text',
178 - 'class' => array( 'merchant-hidden' ),
179 - 'required' => true,
180 - 'default' => $this->get_oldest_date(),
181 - ];
182 - }
183 -
184 - return $fields;
185 - }
186 -
187 - /**
188 - * Check pre order products in cart.
189 - *
190 - * @param array $items
191 - * @return void
192 - */
193 - public function check_pre_order_products( $items ) {
194 - if ( isset( $items['line_items'] ) ) {
195 - $items = $items['line_items'];
196 - }
197 -
198 - $pre_order_products = array_filter( $items, function ( $v ) {
199 - return 'yes' === get_post_meta( $v['product_id'], '_is_pre_order', true ) && new DateTime( get_post_meta( $v['product_id'], '_pre_order_date', true ) ) > new DateTime() || 'yes' === get_post_meta( $v['variation_id'], '_is_pre_order', true ) && new DateTime( get_post_meta( $v['variation_id'], '_pre_order_date', true ) ) > new DateTime();
200 - } );
201 -
202 - $this->set_pre_order_products( $pre_order_products );
203 - }
204 -
205 - /**
206 - * Get the oldest date.
207 - *
208 - * @return string
209 - */
210 - public function get_oldest_date() {
211 - $product_with_oldest_date = array_reduce( $this->get_pre_order_products(), function ( $a, $b ) {
212 - if ( null === $a ) {
213 - return $b;
214 - }
215 - $aId = isset( $a['variation_id'] ) && 0 !== $a['variation_id'] ? $a['variation_id'] : $a['product_id'];
216 - $bId = isset( $b['variation_id'] ) && 0 !== $b['variation_id'] ? $b['variation_id'] : $b['product_id'];
217 - return $a ? ( strtotime( get_post_meta( $aId, '_pre_order_date', true ) ) > strtotime( get_post_meta( $bId, '_pre_order_date', true ) ) ? $a : $b ) : $b;
218 - } );
219 -
220 - $oldestId = isset( $product_with_oldest_date['variation_id'] ) && 0 !== $product_with_oldest_date['variation_id'] ? $product_with_oldest_date['variation_id'] : $product_with_oldest_date['product_id'];
221 -
222 - return get_post_meta( $oldestId, '_pre_order_date', true );
223 - }
224 -
225 - /**
226 - * Get pre order products.
227 - *
228 - * @return array
229 - */
230 - public function get_pre_order_products() {
231 - return $this->pre_order_products;
232 - }
233 -
234 - /**
235 - * Set pre order products.
236 - *
237 - * @param array $pre_order_products
238 - * @return $this
239 - */
240 - public function set_pre_order_products( $pre_order_products ) {
241 - $this->pre_order_products = $pre_order_products;
242 - return $this;
243 - }
244 -
245 - /**
246 - * Check for pre orders and maybe update the status.
247 - *
248 - * @return void
249 - */
250 - public function check_for_pre_orders_and_maybe_update_status() {
251 - $args = [
252 - 'status' => 'wc-pre-ordered',
253 - ];
254 -
255 - $pre_ordered_orders = wc_get_orders( $args );
256 -
257 - foreach ( $pre_ordered_orders as $order ) {
258 - $pre_order_date = strtotime( $order->get_meta('_preorder_date') );
259 -
260 - if ( $pre_order_date < time() ) {
261 - $parent_order_id = $order->get_parent_id();
262 -
263 - if ( 0 !== $parent_order_id ) {
264 - $parent_order = wc_get_order( $parent_order_id );
265 -
266 - if ( $parent_order->get_status() === 'completed' ) {
267 - $order->update_status( 'wc-completed', '[WooCommerce Pre Orders] ' );
268 - }
269 - } else {
270 - if ( $order->get_status() === 'wc-pre-ordered' && $order->payment_complete() ) {
271 - $order->update_status( 'wc-completed', '[WooCommerce Pre Orders] ' );
272 - }
273 - }
274 - }
275 - }
276 - }
277 -
278 - /**
279 - * Custom pre-order fields for variations.
280 - *
281 - * @param integer $loop
282 - * @param array $variation_data
283 - * @param object $variation
284 - * @return void
285 - */
286 - public function custom_fields_for_variable_products( $loop, $variation_data, $variation ) {
287 - echo '<div class="is_pre_order_meta_field form-row form-row-full" style="display: flex; align-items: center;">';
288 - woocommerce_wp_checkbox(
289 - [
290 - 'id' => '_is_pre_order_' . $variation->ID,
291 - 'label' => '&nbsp;' . esc_html__( 'Pre-Order Product - Set this product as pre-order', 'merchant' ),
292 - 'value' => get_post_meta( $variation->ID, '_is_pre_order', true ),
293 - ]
294 - );
295 -
296 - echo wc_help_tip( __( 'Important: To pre-order out of stock products you must enable the \'Backorder\' stock option.', 'merchant' ) );
297 - echo '</div>';
298 - echo '<div class="form-row form-row-full">';
299 - woocommerce_wp_text_input(
300 - [
301 - 'type' => 'date',
302 - 'id' => '_pre_order_date_' . $variation->ID,
303 - 'label' => esc_html__( 'Pre-Order Shipping Date', 'merchant' ),
304 - 'value' => get_post_meta( $variation->ID, '_pre_order_date', true ),
305 - ]
306 - );
307 - echo '</div>';
308 - }
309 -
310 - /**
311 - * Custom pre-order fields for simple products.
312 - *
313 - * @return void
314 - */
315 - public function custom_fields_for_simple_products() {
316 - echo '<div class="is_pre_order_meta_field form-row form-row-full hide_if_variable" style="display: flex; align-items: center;">';
317 - woocommerce_wp_checkbox(
318 - [
319 - 'id' => '_is_pre_order',
320 - 'label' => esc_html__( 'Pre-Order Product', 'merchant' ),
321 - 'description' => esc_html__( 'Set this product as pre-order', 'merchant' ),
322 - 'value' => get_post_meta( get_the_ID(), '_is_pre_order', true ),
323 - ]
324 - );
325 -
326 - echo '<div style="margin-left: -20px">';
327 - echo wc_help_tip( __( 'Important: To pre-order out of stock products you must enable the \'Backorder\' stock option.', 'merchant' ) );
328 - echo '</div>';
329 - echo '</div>';
330 - echo '<div class="form-row form-row-full hide_if_variable">';
331 - woocommerce_wp_text_input(
332 - array(
333 - 'type' => 'date',
334 - 'id' => '_pre_order_date',
335 - 'label' => esc_html__( 'Pre-Order Shipping Date', 'merchant' ),
336 - 'value' => get_post_meta( get_the_ID(), '_pre_order_date', true ),
337 - )
338 - );
339 - echo '</div>';
340 -
341 - }
342 -
343 - /**
344 - * Save custom fields for variable products.
345 - *
346 - * @param integer $post_id
347 - * @return void
348 - */
349 - public function custom_fields_for_variable_products_save( $post_id ) {
350 - $input_post_data = array(
351 - '_is_pre_order_by_post_id' => filter_input(INPUT_POST, '_is_pre_order_' . $post_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS),
352 - '_pre_order_data_by_post_id' => filter_input(INPUT_POST, '_pre_order_date_' . $post_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS),
353 - );
354 -
355 - $product = wc_get_product( $post_id );
356 -
357 - $is_pre_order_variation = isset( $input_post_data[ '_is_pre_order_by_post_id' ] ) ? 'yes' : 'no';
358 - $product->update_meta_data( '_is_pre_order', $is_pre_order_variation );
359 -
360 - if ( 'yes' === $is_pre_order_variation && isset( $input_post_data[ '_pre_order_data_by_post_id' ] ) ) {
361 - $pre_order_date_value = sanitize_text_field( wp_unslash( $input_post_data[ '_pre_order_data_by_post_id' ] ) );
362 - $product->update_meta_data( '_pre_order_date', $pre_order_date_value );
363 - }
364 -
365 - $product->save();
366 - }
367 -
368 - /**
369 - * Save custom fields for simple products.
370 - *
371 - * @param integer $post_id
372 - * @return void
373 - */
374 - public function custom_fields_for_simple_products_save( $post_id ) {
375 - $input_post_data = array(
376 - '_is_pre_order' => filter_input(INPUT_POST, '_is_pre_order', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
377 - '_pre_order_date' => filter_input(INPUT_POST, '_pre_order_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
378 - );
379 -
380 - $product = wc_get_product( $post_id );
381 - $is_pre_order = isset( $input_post_data['_is_pre_order'] ) ? 'yes' : 'no';
382 - $product->update_meta_data( '_is_pre_order', $is_pre_order );
383 -
384 - if ( 'yes' === $is_pre_order && isset( $input_post_data['_pre_order_date'] ) ) {
385 - $pre_order_date_value = sanitize_text_field( wp_unslash( $input_post_data['_pre_order_date'] ) );
386 - $product->update_meta_data( '_pre_order_date', esc_attr( $pre_order_date_value ) );
387 - } else {
388 - $product->update_meta_data( '_pre_order_date', '' );
389 - }
390 -
391 - $product->save();
392 - }
393 -
394 - /**
395 - * Change pre-order button text.
396 - *
397 - * @param string $text
398 - * @return string
399 - */
400 - public function change_button_text( $text ) {
401 - $input_post_data = array(
402 - 'product_id' => filter_input(INPUT_POST, 'product_id', FILTER_SANITIZE_NUMBER_INT),
403 - );
404 -
405 - global $post;
406 - $_post = $post;
407 -
408 - // In some cases the $post might be null. e.g inside quick view popup.
409 - if ( ! $_post && isset( $input_post_data[ 'product_id' ] ) ) {
410 - $_post = get_post( absint( $input_post_data[ 'product_id' ] ) );
411 - }
412 -
413 - if ( 'yes' === get_post_meta( $_post->ID, '_is_pre_order', true ) && strtotime( get_post_meta( $_post->ID, '_pre_order_date', true ) ) > time() ) {
414 - $text = Merchant_Admin_Options::get( 'pre-orders', 'button_text', esc_html__( 'Pre Order Now!', 'merchant' ) );
415 - }
416 -
417 - return $text;
418 - }
419 -
420 - /**
421 - * Change pre-order button text for variable products.
422 - *
423 - * @param array $data
424 - * @param object $product
425 - * @param object $variation
426 - * @return array
427 - */
428 - public function change_button_text_for_variable_products( $data, $product, $variation ) {
429 - global $product;
430 -
431 - if ( get_post_meta( $variation->get_id(), '_is_pre_order', true ) === 'yes' && strtotime( get_post_meta( $variation->get_id(), '_pre_order_date', true ) ) > time() ) {
432 -
433 - $data['is_pre_order'] = true;
434 -
435 - $additional_text = Merchant_Admin_Options::get( 'pre-orders', 'additional_text', esc_html__( 'Ships on {date}.', 'merchant' ) );
436 - $time_format = date_i18n( get_option( 'date_format' ), strtotime( get_post_meta( $variation->get_id(), '_pre_order_date', true ) ) );
437 - $text = $this->replaceDateTxt( $additional_text, $time_format );
438 -
439 - if ( ! empty( $text ) ) {
440 - $data['is_pre_order_date'] = $this->replaceDateTxt( $additional_text, $time_format );
441 - }
442 -
443 - }
444 -
445 - return $data;
446 - }
447 -
448 - /**
449 - * Replace {date} markup with new text.
450 - *
451 - * @param string $string
452 - * @param string $time_format
453 - * @return string
454 - */
455 - public function replaceDateTxt( $string, $time_format ) {
456 - $from = array( '{date}' );
457 - $to = array( $time_format );
458 -
459 - return str_replace( $from, $to, $string );
460 - }
461 -
462 - /**
463 - * Maybe render additional information.
464 - *
465 - * @return void
466 - */
467 - public function maybe_render_additional_information() {
468 - $input_post_data = array(
469 - 'product_id' => filter_input(INPUT_POST, 'product_id', FILTER_SANITIZE_NUMBER_INT),
470 - );
471 -
472 - global $post, $product;
473 -
474 - // Do not override globals.
475 - $_post = $post;
476 - $_product = $product;
477 -
478 - // In some cases the $post might be null. e.g inside quick view popup.
479 - if ( ! $_post && isset( $input_post_data[ 'product_id' ] ) ) {
480 - $_post = get_post( absint( $input_post_data[ 'product_id' ] ) );
481 - $_product = wc_get_product( $_post->ID );
482 - }
483 -
484 - if ( null !== $_product ) {
485 - if ( 'yes' === get_post_meta( $_post->ID, '_is_pre_order', true ) && strtotime( get_post_meta( $_post->ID, '_pre_order_date', true ) ) > time() ) {
486 - $additional_text = Merchant_Admin_Options::get( 'pre-orders', 'additional_text', esc_html__( 'Ships on {date}.', 'merchant' ) );
487 - $time_format = date_i18n( get_option( 'date_format' ), strtotime( get_post_meta( $_post->ID, '_pre_order_date', true ) ) );
488 - $text = $this->replaceDateTxt( $additional_text, $time_format );
489 -
490 - echo sprintf( '<div class="merchant-pre-orders-date">%s</div>', esc_html( $text ) );
491 - }
492 - }
493 - }
494 -
495 - /**
496 - * Register pre orders status.
497 - *
498 - * @return void
499 - */
500 - public function register_pre_orders_order_status() {
501 - register_post_status( 'wc-pre-ordered', array(
502 - 'label' => esc_html__( 'Pre Ordered', 'merchant' ),
503 - 'public' => true,
504 - 'show_in_admin_status_list' => true,
505 - 'show_in_admin_all_list' => true,
506 - 'exclude_from_search' => false,
507 - /* translators: %s: pre ordered product count */
508 - 'label_count' => _n_noop( 'Pre Ordered <span class="count">(%s)</span>', 'Pre Ordered <span class="count">(%s)</span>', 'merchant' )
509 - ) );
510 - }
511 -
512 - /**
513 - * Add pre orders status to order statuses.
514 - *
515 - * @param array $order_statuses
516 - * @return array
517 - */
518 - public function add_pre_orders_order_statuses( $order_statuses ) {
519 - $order_statuses['wc-pre-ordered'] = 'Pre Ordered';
520 -
521 - return $order_statuses;
522 - }
523 -
524 - /**
525 - * Add pre orders class identifies to the body tag.
526 - *
527 - */
528 - public function pre_orders_post_class( $classes, $product ) {
529 - if ( 'yes' === get_post_meta( $product->get_id(), '_is_pre_order', true ) && strtotime( get_post_meta( $product->get_id(), '_pre_order_date', true ) ) > time() ) {
530 - $classes[] = 'merchant-pre-ordered-product';
531 - }
532 -
533 - return $classes;
534 - }
535 -
536 -}
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' );