PluginProbe
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant / 1.9.11
Product Labels, Quick View, Buy Now, Pre-Orders, Frequently Bought Together & More for WooCommerce – Merchant v1.9.11
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 1.9.11, at inc/modules/pre-orders/class-pre-orders-main-functionality.php

1,337 lines 41.5 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 * Init.
39 *
40 * @return void
41 */
42 public function init() {
43 add_filter( 'woocommerce_add_to_cart_validation', array( $this, 'allow_one_type_only' ), 99, 2 );
44 add_filter( 'woocommerce_add_cart_item_data', array( $this, 'add_cart_item_data' ), 10, 4 );
45 add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hidden_order_itemmeta' ) );
46 add_action( 'woocommerce_add_order_item_meta', array( $this, 'add_order_item_meta' ), 10, 2 );
47
48 // Cronjob.
49 if ( ! wp_next_scheduled( 'check_for_released_preorders' ) ) {
50 wp_schedule_event( time(), 'twicedaily', 'check_for_released_preorders' );
51 }
52
53 add_action( 'check_for_released_preorders', array( $this, 'check_for_pre_orders_and_maybe_update_status' ) );
54
55 add_filter( 'woocommerce_product_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
56 add_filter( 'woocommerce_product_single_add_to_cart_text', array( $this, 'change_button_text' ), 10, 2 );
57 add_filter( 'woocommerce_available_variation', array( $this, 'change_button_text_for_variable_products' ), 10, 3 );
58 add_action( 'woocommerce_before_add_to_cart_form', array( $this, 'additional_information_before_cart_form' ) );
59 add_action( 'woocommerce_after_add_to_cart_form', array( $this, 'additional_information_after_cart_form' ) );
60
61 // Cart
62 add_filter( 'woocommerce_get_item_data', array( $this, 'cart_message_handler' ), 10, 2 );
63
64 add_action( 'woocommerce_order_item_meta_end', array( $this, 'order_item_meta_end' ), 10, 4 );
65 add_action( 'woocommerce_shop_loop_item_title', array( $this, 'shop_loop_item_title' ) );
66
67 // Products block.
68 add_filter( 'render_block_context', array( $this, 'add_block_title_filter' ), 10, 1 );
69 add_filter( 'woocommerce_blocks_product_grid_item_html', array( $this, 'override_product_grid_block' ), PHP_INT_MAX, 3 );
70
71 $this->register_pre_orders_order_status();
72
73 add_filter( 'wc_order_statuses', array( $this, 'add_pre_orders_order_statuses' ) );
74 add_filter( 'woocommerce_post_class', array( $this, 'pre_orders_post_class' ), 10, 3 );
75
76 add_filter( 'woocommerce_get_price_html', array( $this, 'dynamic_discount_price_html' ), 10, 2 );
77 add_action( 'woocommerce_before_calculate_totals', array( $this, 'dynamic_discount_cart_price' ) );
78 add_action( 'woocommerce_checkout_order_created', array( $this, 'splitting_orders' ) );
79 add_filter( 'manage_woocommerce_page_wc-orders_columns', array( $this, 'shop_order_column' ), 11 );
80 add_filter( 'manage_edit-shop_order_columns', array( $this, 'shop_order_column' ), 11 );
81 add_action( 'manage_woocommerce_page_wc-orders_custom_column', array( $this, 'shop_order_column_content' ), 10, 2 );
82 add_action( 'manage_shop_order_posts_custom_column', array( $this, 'shop_order_column_content' ), 10, 2 );
83 }
84
85 /**
86 * Add shipping date column to the orders list.
87 *
88 * @param $columns array The columns.
89 *
90 * @return array The columns.
91 */
92 public function shop_order_column( $columns ) {
93 $columns['pre_order_shipping_date'] = esc_html__( 'Shipping Date', 'merchant' );
94
95 return $columns;
96 }
97
98 /**
99 * Display the shipping date in the orders list.
100 *
101 * @param $column string The column.
102 * @param $order WC_Order|int The order object in HPOS or order id.
103 *
104 * @return void
105 */
106 public function shop_order_column_content( $column, $order ) {
107 if ( $column === 'pre_order_shipping_date' ) {
108 if ( is_numeric( $order ) ) {
109 $order = wc_get_order( $order );
110 }
111 /**
112 * Filter the pre order shipping date.
113 *
114 * @param int $shipping_date The shipping date.
115 * @param WC_Order $order The order object.
116 *
117 * @since 1.9.9
118 */
119 $shipping_date = apply_filters(
120 'merchant_pre_order_shipping_date_column',
121 $order->get_meta( '_merchant_order_pre_order_shipping_date' ),
122 $order
123 );
124 if ( $shipping_date ) {
125 echo '<strong>' . esc_html( $this->convert_timestamp_to_human_readable( $shipping_date ) ) . '</strong>';
126 }
127 }
128 }
129
130 /**
131 * Convert timestamp to human readable date.
132 *
133 * @param $timestamp int The timestamp.
134 *
135 * @return string The human readable date.
136 */
137 private function convert_timestamp_to_human_readable( $timestamp ) {
138 $timezone = new DateTimeZone( merchant_timezone() );
139 $date = new \DateTime( 'now', $timezone );
140 $date->setTimestamp( $timestamp );
141
142 return $date->format( self::DATE_TIME_FORMAT );
143 }
144
145 /**
146 * Add offer details to the product.
147 *
148 * @param $cart_item_data array The cart item data.
149 * @param $product_id int The product ID.
150 * @param $variation_id int The variation ID.
151 * @param $quantity int The quantity.
152 *
153 * @return array The cart item data.
154 */
155 public function add_cart_item_data( $cart_item_data, $product_id, $variation_id, $quantity ) {
156 $product = wc_get_product( $product_id );
157 $offer = self::available_product_rule( $product_id );
158 if ( empty( $offer ) && $product->is_type( 'variable' ) ) {
159 $offer = self::available_product_rule( $variation_id );
160 }
161 if ( ! empty( $offer ) ) {
162 $cart_item_data['_merchant_pre_order'] = $offer;
163 $cart_item_data['_merchant_pre_order_shipping_date'] = $offer['shipping_timestamp'];
164 }
165
166 return $cart_item_data;
167 }
168
169 /**
170 * Add pre-order data to order item meta.
171 *
172 * @param $item_id int The item ID.
173 * @param $values array The values.
174 *
175 * @return void
176 */
177 public function add_order_item_meta( $item_id, $values ) {
178 if ( isset( $values['_merchant_pre_order'] ) ) {
179 wc_add_order_item_meta( $item_id, '_merchant_pre_order', $values['_merchant_pre_order'] );
180 }
181
182 if ( isset( $values['_merchant_pre_order_shipping_date'] ) ) {
183 wc_add_order_item_meta( $item_id, '_merchant_pre_order_shipping_date', $values['_merchant_pre_order_shipping_date'] );
184 }
185 }
186
187 /**
188 * Splitting orders.
189 *
190 * @param $order WC_Order The order object.
191 *
192 * @return void
193 */
194 public function splitting_orders( $order ) {
195 $mode = Merchant_Admin_Options::get( self::MODULE_ID, 'modes', 'unified_order' );
196 if ( 'unified_order' === $mode || 'only_pre_orders' === $mode ) {
197 $this->mark_whole_order_as_pre_order( $order );
198 } elseif ( 'group_pre_order_into_one_order' === $mode ) {
199 $this->group_pre_order_into_one_order( $order );
200 } elseif ( 'separate_order_for_pre_orders' === $mode ) {
201 $this->separate_order_for_pre_orders( $order );
202 }
203 }
204
205 /**
206 * Mark whole order as pre-order if it contains pre-order products.
207 *
208 * @param $order WC_Order The order object.
209 *
210 * @return void
211 */
212 public function mark_whole_order_as_pre_order( $order ) {
213 $has_pre_order = 0;
214 $shipping_dates = array();
215 foreach ( $order->get_items() as $item_id => $item ) {
216 $product_id = $item->get_product_id();
217 if ( $this->is_pre_order( $product_id ) ) {
218 $shipping_dates[] = $item->get_meta( '_merchant_pre_order_shipping_date' );
219 ++ $has_pre_order;
220 break;
221 }
222 }
223
224 if ( $has_pre_order ) {
225 $order->set_status( 'wc-pre-ordered' );
226 $order->add_meta_data( '_is_pre_order', true );
227 $order->add_meta_data( '_merchant_order_pre_order_shipping_date', max( $shipping_dates ) );
228 $order->save();
229 }
230 }
231
232 /**
233 * Group pre-order products into one order and keep the original order.
234 *
235 * @param WC_Order $order The order object.
236 *
237 * @return void
238 */
239 public function group_pre_order_into_one_order( $order ) {
240 // Get the original order
241 $original_order = $order;
242
243 // Store sub-orders IDs
244 $sub_order_ids = $pre_order_products = array();
245
246 // Loop through each product in the original order
247 foreach ( $original_order->get_items() as $item_id => $item ) {
248 if ( $this->is_pre_order( $item->get_product()->get_id() ) ) {
249 $pre_order_products[] = $item;
250 }
251 }
252
253 if ( ! empty( $pre_order_products ) ) {
254 $shipping_dates = array();
255 // Create a new order
256 $new_order = wc_create_order(
257 array(
258 'parent' => $original_order->get_id(),
259 )
260 );
261
262 // Copy order details from original order to new order.
263 $this->copy_order_details( $original_order, $new_order );
264
265 $new_order->add_meta_data( '_is_sub_order', true );
266 $new_order->add_meta_data( '_is_pre_order', true );
267 $new_order->set_status( 'wc-pre-ordered' );
268
269 // Copy order meta data from original order
270 foreach ( $original_order->get_meta_data() as $meta ) {
271 $new_order->add_meta_data( $meta->key, $meta->value );
272 }
273
274 // add pre-order products to the new order
275 foreach ( $pre_order_products as $item ) {
276 $rule = self::available_product_rule( $item->get_product()->get_id() );
277 $new_item = $this->clone_order_item( $item );
278 $new_item->add_meta_data( '_merchant_pre_order', $rule );
279 $new_item->add_meta_data( '_merchant_is_pre_order_product', true );
280 $new_item->add_meta_data( '_merchant_pre_order_shipping_date', $rule['shipping_timestamp'] );
281 $new_order->add_item( $new_item );
282 $shipping_dates[] = $rule['shipping_timestamp'];
283 }
284
285 // Calculate totals and set status
286 $new_order->calculate_totals();
287 $new_order->add_meta_data( '_merchant_order_pre_order_shipping_date', max( $shipping_dates ) );
288 $new_order->save();
289
290 $sub_order_ids[] = $new_order->get_id();
291
292 // Save sub-order IDs to the original order
293 $original_order->update_meta_data( '_sub_order_ids', $sub_order_ids );
294
295
296 // Update original order totals after removing items
297 $original_order->calculate_totals();
298 $original_order->save();
299 }
300 }
301
302 private function clone_order_item( $item ) {
303 $new_item = new WC_Order_Item_Product();
304 $new_item->set_product_id( $item->get_product_id() );
305 $new_item->set_variation_id( $item->get_variation_id() );
306 $new_item->set_name( $item->get_name() );
307 $new_item->set_quantity( $item->get_quantity() );
308 $new_item->set_subtotal( $item->get_subtotal() );
309 $new_item->set_total( $item->get_total() );
310 $new_item->set_taxes( $item->get_taxes() );
311 $new_item->set_meta_data( $item->get_meta_data() );
312
313 return $new_item;
314 }
315
316 /**
317 * Generate separate orders for each pre-order product.
318 *
319 * @param WC_Order $order The order object.
320 *
321 * @return void
322 */
323 public function separate_order_for_pre_orders( $order ) {
324 // Get the original order
325 $original_order = $order;
326
327 // Store sub-orders IDs
328 $sub_order_ids = array();
329
330 // Loop through each product in the original order
331 foreach ( $original_order->get_items() as $item_id => $item ) {
332 //check if the item is a product
333
334 // Get product details
335 $product_id = $item->get_product()->get_id();
336 if ( ! $product_id ) {
337 continue;
338 }
339
340 // check if the product is a pre-order
341 if ( ! $this->is_pre_order( $product_id ) ) {
342 continue;
343 }
344
345 // Create a new order
346 $new_order = wc_create_order(
347 array(
348 'parent' => $original_order->get_id(),
349 )
350 );
351
352 $new_item = $this->clone_order_item( $item );
353 $rule = self::available_product_rule( $product_id );
354 $new_item->add_meta_data( '_merchant_pre_order', $rule );
355 $new_item->add_meta_data( '_merchant_is_pre_order_product', true );
356 $new_item->add_meta_data( '_merchant_pre_order_shipping_date', $rule['shipping_timestamp'] );
357 $new_order->add_item( $new_item );
358
359 // Copy order details from original order to new order.
360 $this->copy_order_details( $original_order, $new_order );
361
362 // Calculate totals and set status
363 $new_order->calculate_totals();
364
365 // Save new order and store its ID
366 $new_order->add_meta_data( '_is_sub_order', true );
367 $new_order->add_meta_data( '_is_pre_order', true );
368 $new_order->add_meta_data( '_merchant_order_pre_order_shipping_date', $rule['shipping_timestamp'] );
369 $new_order->set_status( 'wc-pre-ordered' );
370 $new_order->save();
371 $sub_order_ids[] = $new_order->get_id();
372 }
373
374 // Save sub-order IDs to the original order
375 $original_order->update_meta_data( '_sub_order_ids', $sub_order_ids );
376
377 // Update original order totals after removing items
378 $original_order->calculate_totals();
379 $original_order->save();
380 }
381
382 /**
383 * Hide order item meta that will be used for internal use only.
384 *
385 * @param $args array The hidden order item meta.
386 *
387 * @return array The hidden order item meta.
388 */
389 function hidden_order_itemmeta( $args ) {
390 $args[] = '_merchant_pre_order';
391 $args[] = '_merchant_is_pre_order_product';
392 $args[] = '_merchant_pre_order_shipping_date';
393
394 return $args;
395 }
396
397 /**
398 * Copy order details from the original order to the new child order.
399 *
400 * @param $original_order WC_Order The original order.
401 * @param $new_order WC_Order The new child order.
402 *
403 * @return void
404 */
405 private function copy_order_details( $original_order, $new_order ) {
406 $new_order->set_customer_id( $original_order->get_customer_id() );
407
408 // set customer billing details
409 $new_order->set_billing_first_name( $original_order->get_billing_first_name() );
410 $new_order->set_billing_last_name( $original_order->get_billing_last_name() );
411 $new_order->set_billing_phone( $original_order->get_billing_phone() );
412 $new_order->set_billing_email( $original_order->get_billing_email() );
413 $new_order->set_billing_address_1( $original_order->get_billing_address_1() );
414 $new_order->set_billing_address_2( $original_order->get_billing_address_2() );
415 $new_order->set_billing_city( $original_order->get_billing_city() );
416 $new_order->set_billing_state( $original_order->get_billing_state() );
417 $new_order->set_billing_postcode( $original_order->get_billing_postcode() );
418 $new_order->set_billing_country( $original_order->get_billing_country() );
419
420 // set customer shipping details
421 $new_order->set_shipping_first_name( $original_order->get_shipping_first_name() );
422 $new_order->set_shipping_last_name( $original_order->get_shipping_last_name() );
423 $new_order->set_shipping_address_1( $original_order->get_shipping_address_1() );
424 $new_order->set_shipping_address_2( $original_order->get_shipping_address_2() );
425 $new_order->set_shipping_city( $original_order->get_shipping_city() );
426 $new_order->set_shipping_state( $original_order->get_shipping_state() );
427 $new_order->set_shipping_postcode( $original_order->get_shipping_postcode() );
428 $new_order->set_shipping_country( $original_order->get_shipping_country() );
429
430 $new_order->set_customer_note( $original_order->get_customer_note() );
431 /**
432 * This action is documented in woocommerce/includes/class-wc-checkout.php
433 *
434 * @since 3.0.0 or earlier
435 */
436 $new_order->set_customer_id( apply_filters( 'woocommerce_checkout_customer_id', get_current_user_id() ) );
437 $new_order->set_customer_ip_address( WC_Geolocation::get_ip_address() );
438 $new_order->set_customer_user_agent( wc_get_user_agent() );
439 $new_order->set_currency( get_woocommerce_currency() );
440 $new_order->set_created_via( 'checkout' );
441
442 // Copy order meta data from original order
443 foreach ( $original_order->get_meta_data() as $meta ) {
444 $new_order->add_meta_data( $meta->key, $meta->value );
445 }
446 }
447
448 /**
449 * Update the product price html based on the offer.
450 *
451 * @return string The price html.
452 */
453 public function dynamic_discount_price_html( $html_price, $product ) {
454 /**
455 * Disable the sale price change.
456 *
457 * @param bool $disable_sale_price The sale price change status.
458 * @param string $html_price The price.
459 * @param WC_Product $product The product object.
460 *
461 * @since 1.9.9
462 */
463 if ( apply_filters( 'merchant_pre_order_disable_sale_price_html', false, $html_price, $product ) ) {
464 return $html_price;
465 }
466
467 if ( ! is_admin() && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
468 if ( '' === $product->get_price() ) {
469 return $html_price;
470 }
471
472 $offer = self::available_product_rule( $product->get_id() );
473 if ( empty( $offer ) && $product->is_type( 'variation' ) ) {
474 $offer = self::available_product_rule( $product->get_parent_id() );
475 }
476
477 if ( $product->is_type( 'variable' ) ) {
478 $html_price = $this->variable_product_price_html( $product );
479 } else {
480 $html_price = $this->simple_product_price_html( $product, $offer, $html_price );
481 }
482 }
483
484 return $html_price;
485 }
486
487 /**
488 * Variable product price html.
489 *
490 * @param WC_Product $product The product object.
491 *
492 * @return string The price html.
493 */
494 private function variable_product_price_html( $product ) {
495 $prices = array();
496 $variations = $product->get_children();
497 foreach ( $variations as $variation_id ) {
498 $variation = wc_get_product( $variation_id );
499 $variation_offer = self::available_product_rule( $variation_id );
500 $regular_price = $variation->get_regular_price();
501 if ( empty( $variation_offer ) ) {
502 if ( $variation->is_on_sale() ) {
503 $sale_price = $variation->get_sale_price();
504 } else {
505 $sale_price = $regular_price;
506 }
507 $prices[] = $sale_price;
508 continue;
509 }
510 $sale_price = $this->calculate_discounted_price( $regular_price, $variation_offer, $variation );
511 if ( $sale_price <= 0 ) {
512 // If the price is less than 0, set it to the regular/sale price
513 if ( $variation->is_on_sale() ) {
514 $sale_price = $variation->get_sale_price();
515 } else {
516 $sale_price = $regular_price;
517 }
518 }
519 $prices[] = $sale_price;
520 }
521
522 $min_price = min( $prices );
523 $max_price = max( $prices );
524 if ( $min_price !== $max_price ) {
525 return wc_format_price_range( $min_price, $max_price );
526 }
527
528 return wc_price( $min_price );
529 }
530
531 /**
532 * Simple product price html.
533 *
534 * @param WC_Product $product The product object.
535 * @param array $offer The offer details.
536 *
537 * @return string The price html.
538 */
539 private function simple_product_price_html( $product, $offer, $html_price ) {
540 $sale = self::get_rule_sale( $offer );
541 if ( ! $sale ) {
542 return $html_price;
543 }
544 $regular_price = $product->get_regular_price();
545 $discounted_price = $this->calculate_discounted_price( $regular_price, $offer, $product );
546
547 return wc_format_sale_price( $regular_price, $discounted_price );
548 }
549
550 /**
551 * Calculate the offer discount for certain price.
552 *
553 * @param float $price The price.
554 * @param array $offer The offer.
555 * @param WC_Product $product The product object (only supplied to the filter).
556 *
557 * @return float The discounted price.
558 */
559 public function calculate_discounted_price( $price, $offer, $product = null ) {
560 $sale = self::get_rule_sale( $offer );
561 $discount_type = $discount_value = '';
562 if ( $sale ) {
563 $discount_type = $offer['discount_type'];
564 $discount_value = $offer['discount_amount'];
565 if ( 'percentage' === $discount_type ) {
566 $price = (float) $price - ( (float) $price * ( (float) $discount_value / 100 ) );
567 } else {
568 $price = (float) $price - (float) $discount_value;
569 }
570 }
571
572 /**
573 * Filter the discounted price.
574 *
575 * @param float $price The price.
576 * @param array $offer The offer.
577 * @param string $discount_type The discount type.
578 * @param float $discount_value The discount value.
579 * @param WC_Product $product The product object.
580 *
581 * @since 1.9.5
582 */
583 return apply_filters(
584 'merchant_pre_order_sale_calculate_discounted_price',
585 $price,
586 $offer,
587 $discount_type,
588 $discount_value,
589 $product
590 );
591 }
592
593 /**
594 * Set the discounted cart price for the product.
595 *
596 * @param $cart_object array The cart object.
597 *
598 * @return void
599 */
600 public function dynamic_discount_cart_price() {
601 foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
602 /**
603 * Disable the cart price change.
604 *
605 * @param bool $disable_cart_price The cart price change status.
606 * @param array $cart_item The cart item.
607 * @param string $cart_item_key The cart item key.
608 *
609 * @since 1.9.7
610 */
611 if ( apply_filters( 'merchant_pre_order_sale_disable_cart_price', false, $cart_item, $cart_item_key ) ) {
612 continue;
613 }
614 $product_id = $cart_item['data']->get_id();
615 $product = wc_get_product( $product_id );
616 if ( ! is_admin() && in_array( $product->get_type(), $this->allowed_product_types(), true ) ) {
617 $offer = self::available_product_rule( $product->get_id() );
618 if ( $product->is_type( 'variable' ) ) {
619 $product_id = $cart_item['variation_id'];
620 $product = wc_get_product( $product_id );
621 // check if the variation has an offer
622 $offer = self::available_product_rule( $product_id );
623 }
624 if ( empty( $offer ) ) {
625 continue;
626 }
627
628 $regular_price = $product->get_regular_price();
629 $sale_price = $this->calculate_discounted_price( $regular_price, $offer, $product );
630 $cart_item['data']->set_price( $sale_price );
631 $cart_item['data']->set_meta_data( array( '_merchant_pre_order_sale' => $offer ) );
632 }
633 }
634 }
635
636 /**
637 * Validation (one type only).
638 *
639 * @param boolean $passed
640 * @param integer $product_id
641 *
642 * @return boolean
643 */
644 public function allow_one_type_only( $passed, $product_id ) {
645 $products = array_filter( WC()->cart->get_cart_contents() );
646 $pre_orders = 0;
647 $has_pre_orders = false;
648 foreach ( $products as $product ) {
649 $is_pre_order = $this->is_pre_order( $product['data']->get_id() );
650 if ( $is_pre_order ) {
651 ++ $pre_orders;
652 }
653 }
654 if ( $pre_orders ) {
655 $has_pre_orders = true;
656 }
657 $input_post_data = array(
658 'variation_id' => filter_input( INPUT_POST, 'variation_id', FILTER_SANITIZE_NUMBER_INT ),
659 );
660
661 $variable_id = ( isset( $input_post_data['variation_id'] ) ) ? sanitize_text_field( wp_unslash( $input_post_data['variation_id'] ) ) : 0;
662 $is_variable_has_pre_order = $this->is_pre_order( $product_id ) || $this->is_pre_order( $variable_id );
663
664 if ( empty( $products ) || ( $is_variable_has_pre_order && $has_pre_orders ) || ( false === $is_variable_has_pre_order && false === $has_pre_orders ) ) {
665 $passed = true;
666 } else {
667 $mode = Merchant_Admin_Options::get( self::MODULE_ID, 'modes', 'unified_order' );
668 if ( 'only_pre_orders' === $mode ) {
669 $passed = false;
670 if ( $is_variable_has_pre_order ) {
671 $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.',
672 'merchant' );
673 } else {
674 $notice = esc_html__( 'We detected that your cart has pre-order products. Please remove them before being able to add this product.', 'merchant' );
675 }
676 wc_add_notice( $notice, 'error' );
677 }
678 }
679
680 return $passed;
681 }
682
683 /**
684 * Check if product is a pre order item by product ID.
685 *
686 * @param integer $product_id
687 *
688 * @return boolean
689 */
690 public function is_pre_order( $product_id ) {
691 $available_pre_order = self::available_product_rule( $product_id );
692
693 return ! empty( $available_pre_order );
694 }
695
696 /**
697 * Check for pre orders and maybe update the status.
698 *
699 * @return void
700 */
701 public function check_for_pre_orders_and_maybe_update_status() {
702 $args = array(
703 'status' => 'wc-pre-ordered',
704 );
705
706 $pre_ordered_orders = wc_get_orders( $args );
707
708 foreach ( $pre_ordered_orders as $order ) {
709 $pre_order_date = $order->get_meta( '_merchant_order_pre_order_shipping_date' );
710
711 if ( $pre_order_date < time() ) {
712 $parent_order_id = $order->get_parent_id();
713
714 if ( 0 !== $parent_order_id ) {
715 $parent_order = wc_get_order( $parent_order_id );
716
717 if ( $parent_order->get_status() === 'completed' ) {
718 $order->update_status( 'wc-completed', '[Merchant Pre Orders] ' );
719 }
720 } elseif ( $order->get_status() === 'wc-pre-ordered' && $order->payment_complete() ) {
721 $order->update_status( 'wc-completed', '[Merchant Pre Orders] ' );
722 }
723 }
724 }
725 }
726
727 /**
728 * Change pre-order button text.
729 *
730 * @param string $text
731 * @param WC_Product $product
732 *
733 * @return string
734 */
735 public function change_button_text( $text, $product ) {
736 if ( $product && $this->is_pre_order( $product->get_id() ) ) {
737 $pre_order_rule = self::available_product_rule( $product->get_id() );
738 $text = $pre_order_rule['button_text'] ? Merchant_Translator::translate( $pre_order_rule['button_text'] ) : esc_html__( 'Pre Order Now!', 'merchant' );
739 }
740
741 return $text;
742 }
743
744 /**
745 * Change pre-order button text for variable products.
746 *
747 * @param array $data
748 * @param object $product
749 * @param object $variation
750 *
751 * @return array
752 */
753 public function change_button_text_for_variable_products( $data, $product, $variation ) {
754 if ( $this->is_pre_order( $variation->get_id() ) ) {
755 $pre_order_rule = self::available_product_rule( $variation->get_id() );
756 $data['is_pre_order'] = true;
757
758 $additional_text = $pre_order_rule['additional_text'] ? Merchant_Translator::translate( $pre_order_rule['additional_text'] )
759 : esc_html__( 'Ships on {date}.', 'merchant' );
760 $time_format = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
761 $text = $this->replace_date_text( $additional_text, $time_format );
762
763 if ( ! empty( $text ) ) {
764 $data['is_pre_order_date'] = $this->replace_date_text( $additional_text, $time_format );
765 }
766 }
767
768 return $data;
769 }
770
771 /**
772 * Replace {date} markup with new text.
773 *
774 * @param string $string_contains_date_tag
775 * @param string $time_format
776 *
777 * @return string
778 */
779 public function replace_date_text( $string_contains_date_tag, $time_format ) {
780 $from = array( '{date}' );
781 $to = array( $time_format );
782
783 return str_replace( $from, $to, $string_contains_date_tag );
784 }
785
786 /**
787 * Display pre order additional information before add to cart form.
788 *
789 * @return void
790 */
791 public function additional_information_before_cart_form() {
792 $input_post_data = array(
793 'product_id' => filter_input( INPUT_POST, 'product_id', FILTER_SANITIZE_NUMBER_INT ),
794 );
795
796 global $post, $product;
797
798 // Do not override globals.
799 $_post = $post;
800 $_product = $product;
801
802 // In some cases the $post might be null. e.g inside quick view popup.
803 if ( ! $_post && isset( $input_post_data['product_id'] ) ) {
804 $_post = get_post( absint( $input_post_data['product_id'] ) );
805 $_product = wc_get_product( $_post->ID );
806 }
807 if ( $this->is_pre_order( $_post->ID ) ) {
808 $pre_order_rule = self::available_product_rule( $_product->get_id() );
809 if ( ! empty( $pre_order_rule ) && $pre_order_rule['placement'] === 'before' ) {
810 $this->maybe_render_additional_information( $pre_order_rule );
811 }
812 }
813 }
814
815 /**
816 * Display pre order additional information after add to cart form.
817 *
818 * @return void
819 */
820 public function additional_information_after_cart_form() {
821 $input_post_data = array(
822 'product_id' => filter_input( INPUT_POST, 'product_id', FILTER_SANITIZE_NUMBER_INT ),
823 );
824
825 global $post, $product;
826
827 // Do not override globals.
828 $_post = $post;
829 $_product = $product;
830
831 // In some cases the $post might be null. e.g inside quick view popup.
832 if ( ! $_post && isset( $input_post_data['product_id'] ) ) {
833 $_post = get_post( absint( $input_post_data['product_id'] ) );
834 $_product = wc_get_product( $_post->ID );
835 }
836 if ( $_product && $this->is_pre_order( $_post->ID ) ) {
837 $pre_order_rule = self::available_product_rule( $_product->get_id() );
838 if ( ! empty( $pre_order_rule ) && $pre_order_rule['placement'] === 'after' ) {
839 $this->maybe_render_additional_information( $pre_order_rule );
840 }
841 }
842 }
843
844 /**
845 * Maybe render additional information.
846 *
847 * @return void
848 */
849 public function maybe_render_additional_information( $rule ) {
850 if ( ! empty( $rule ) ) {
851 $additional_text = $rule['additional_text'] ? Merchant_Translator::translate( $rule['additional_text'] )
852 : esc_html__( 'Ships on {date}.', 'merchant' );
853 $time_format = date_i18n( get_option( 'date_format' ), $rule['shipping_timestamp'] );
854 $text = $this->replace_date_text( $additional_text, $time_format );
855
856 printf( '<div class="merchant-pre-orders-date">%s</div>', esc_html( $text ) );
857 }
858 }
859
860 /**
861 * Register pre orders status.
862 *
863 * @return void
864 */
865 public function register_pre_orders_order_status() {
866 register_post_status( 'wc-pre-ordered', array(
867 'label' => esc_html__( 'Pre Ordered', 'merchant' ),
868 'public' => true,
869 'show_in_admin_status_list' => true,
870 'show_in_admin_all_list' => true,
871 'exclude_from_search' => false,
872 /* translators: %s: pre ordered product count */
873 'label_count' => _n_noop( 'Pre Ordered <span class="count">(%s)</span>', 'Pre Ordered <span class="count">(%s)</span>', 'merchant' ),
874 ) );
875 }
876
877 /**
878 * Add pre orders status to order statuses.
879 *
880 * @param array $order_statuses
881 *
882 * @return array
883 */
884 public function add_pre_orders_order_statuses( $order_statuses ) {
885 $order_statuses['wc-pre-ordered'] = esc_html__( 'Pre Ordered', 'merchant' );
886
887 return $order_statuses;
888 }
889
890 /**
891 * Add pre orders class identifies to the body tag.
892 *
893 */
894 public function pre_orders_post_class( $classes, $product ) {
895 if ( $this->is_pre_order( $product->get_id() ) ) {
896 $classes[] = 'merchant-pre-ordered-product';
897 }
898
899 return $classes;
900 }
901
902 /**
903 * Filters cart item data to display cart.
904 *
905 * @param array $item_data Cart item data.
906 * @param array $cart_item Cart item array.
907 *
908 * @return array
909 *
910 * @see wc_get_formatted_cart_item_data for filter usage.
911 */
912 public function cart_message_handler( $item_data, $cart_item ) {
913 $product_id = $cart_item['product_id'];
914 if ( $cart_item['data']->is_type( 'variation' ) ) {
915 $product_id = $cart_item['variation_id'];
916 }
917 if ( $this->is_pre_order( $product_id ) ) {
918 $pre_order_rule = self::available_product_rule( $product_id );
919 if ( $pre_order_rule ) {
920 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
921 $pre_order_date = date_i18n( get_option( 'date_format' ), $pre_order_rule['shipping_timestamp'] );
922
923 $item_data[] = array(
924 'key' => $label_text,
925 'value' => $pre_order_date,
926 'display' => '',
927 );
928 }
929 }
930
931 return $item_data;
932 }
933
934 /**
935 * filter name: woocommerce_order_item_meta_end
936 * Adds pre order additional text to the order item name
937 *
938 * @param int $item_id Product ID.
939 * @param \WC_Order_Item_Product $item Item array data.
940 * @param \WC_Order $order Order data.
941 * @param bool $plain_text Is plain text or not.
942 *
943 * @return string
944 */
945 public function order_item_meta_end( $item_id, $item, $order, $plain_text ) {
946 echo $this->get_pre_order_text( $item->get_product()->get_id(), $plain_text ? '' : 'dl' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
947 }
948
949 /**
950 * Render pre order text.
951 */
952 public function shop_loop_item_title() {
953 echo $this->get_pre_order_text( get_the_ID(), 'span' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
954 }
955
956 /**
957 * Add the_title filter for block rendering.
958 *
959 * @param array $context Default context.
960 *
961 * @return array
962 */
963 public function add_block_title_filter( $context ) {
964 if ( ! empty( $context['postType'] ) && 'product' === $context['postType'] && ! $this->is_pre_order_filter_on ) {
965 $this->is_pre_order_filter_on = true;
966 add_filter( 'the_title', array( $this, 'block_add_the_title_filter' ), 10, 2 );
967 add_filter( 'render_block', array( $this, 'block_remove_the_title_filter' ), 10, 3 );
968 }
969
970 return $context;
971 }
972
973 /**
974 * Add pre order info to product grid block.
975 *
976 * @param string $html Product grid item HTML.
977 * @param object $data Product data passed to the template.
978 * @param \WC_Product $product Product object.
979 *
980 * @return string Updated product grid item HTML.
981 */
982 public function override_product_grid_block( $html, $data, $product ) {
983 $pre_order_text = $this->get_pre_order_text( $product->get_id(), 'span' );
984 if ( $pre_order_text ) {
985 $html = str_replace( $data->title, $data->title . $pre_order_text, $html );
986 }
987
988 return $html;
989 }
990
991 /**
992 * Adds pre order info to post_title.
993 *
994 * @param string $title Title.
995 * @param int $post_id Post ID.
996 *
997 * @return string
998 */
999 public function block_add_the_title_filter( $title, $post_id ) {
1000 return $title . $this->get_pre_order_text( $post_id, 'span' );
1001 }
1002
1003 /**
1004 * Remove the_title filter when block rendering finished.
1005 *
1006 * @param string $block_content The block content.
1007 * @param array $parsed_block The full block, including name and attributes.
1008 * @param WP_Block $block The block instance.
1009 *
1010 * @return string
1011 */
1012 public function block_remove_the_title_filter( $block_content, $parsed_block, $block ) {
1013 if ( $this->is_pre_order_filter_on ) {
1014 remove_filter( 'the_title', array( $this, 'block_add_the_title_filter' ), 10 );
1015 $this->is_pre_order_filter_on = false;
1016 }
1017
1018 return $block_content;
1019 }
1020
1021 /**
1022 * Get pre order text by product ID.
1023 *
1024 * @param int $product_id
1025 * @param string $render_type Render template type string. 'span', 'dl', ''. Default ''.
1026 *
1027 * @return string
1028 */
1029 private function get_pre_order_text( $product_id, $render_type = '' ) {
1030 if ( ! $this->is_pre_order( $product_id ) ) {
1031 return '';
1032 }
1033
1034 $pre_order_rule = self::available_product_rule( $product_id );
1035 $label_text = $pre_order_rule['cart_label_text'] ? Merchant_Translator::translate( $pre_order_rule['cart_label_text'] ) : esc_html__( 'Ships on', 'merchant' );
1036 $pre_order_date = date_i18n( get_option( 'date_format' ), $pre_order_rule['cart_label_text'] );
1037 if ( 'span' === $render_type ) {
1038 return sprintf( '<span class="merchant-pre-orders-note"><span class="merchant-pre-orders-label">%s:</span><span>%s</span></span>', esc_html( $label_text ),
1039 $pre_order_date );
1040 } elseif ( 'dl' === $render_type ) {
1041 return sprintf( '<dl class="merchant-pre-orders-note"><dt>%s:</dt><dd>%s</dd></dl>', esc_html( $label_text ), $pre_order_date );
1042 } else {
1043 return sprintf( '%s: %s', esc_html( $label_text ), $pre_order_date );
1044 }
1045 }
1046
1047 /**
1048 * Get the product types that are allowed to have storewide sale.
1049 *
1050 * @return array The allowed product types.
1051 */
1052 public function allowed_product_types() {
1053 /**
1054 * Filter the product types that are allowed to have storewide sale.
1055 *
1056 * @param array $product_types
1057 *
1058 * @since 1.9.5
1059 */
1060 return apply_filters( 'merchant_pre_order_allowed_product_types', array(
1061 'simple',
1062 'variation',
1063 'variable',
1064 ) );
1065 }
1066
1067 /**
1068 * Get the pre order rules.
1069 *
1070 * @param array $rule The rule to get.
1071 *
1072 * @return array|false The pre order rules or false if there are no rule sale.
1073 */
1074 private static function get_rule_sale( $rule ) {
1075 $sale = false;
1076 if ( isset( $rule['discount_toggle'] ) && $rule['discount_toggle'] ) {
1077 $discount_type = $rule['discount_type'];
1078 $discount_amount = $rule['discount_amount'];
1079
1080 $sale = array(
1081 'discount_type' => $discount_type,
1082 'discount_amount' => $discount_amount,
1083 );
1084 }
1085
1086 /**
1087 * Filter the pre order sale.
1088 *
1089 * @param array $sale The pre order sale.
1090 * @param array $rule The pre order rule.
1091 *
1092 * @since 1.9.9
1093 */
1094 return apply_filters( 'merchant_pre_order_rule_sale', $sale, $rule );
1095 }
1096
1097 /**
1098 * Get the pre order rules.
1099 *
1100 * @return array The pre order rules.
1101 */
1102 private static function pre_order_rules() {
1103 return Merchant_Admin_Options::get( self::MODULE_ID, 'rules', array() );
1104 }
1105
1106 /**
1107 * Check if the rule is valid.
1108 *
1109 * @param array $rule The rule to check.
1110 *
1111 * @return boolean True if the rule is valid, false otherwise.
1112 */
1113 private static function is_valid_rule( $rule ) {
1114 if ( ! isset( $rule['trigger_on'] ) ) {
1115 return false;
1116 }
1117
1118 if ( 'product' === $rule['trigger_on'] && empty( $rule['product_ids'] ) ) {
1119 return false;
1120 }
1121
1122 if ( 'category' === $rule['trigger_on'] && empty( $rule['category_slugs'] ) ) {
1123 return false;
1124 }
1125
1126 if ( isset( $rule['discount_toggle'] ) && $rule['discount_toggle'] === true ) {
1127 if ( ! isset( $rule['discount_type'] ) ) {
1128 return false;
1129 }
1130 if ( ! isset( $rule['discount_amount'] ) ) {
1131 return false;
1132 }
1133 }
1134
1135 if ( isset( $rule['partial_payment_toggle'] ) && $rule['partial_payment_toggle'] === true ) {
1136 if ( ! isset( $rule['partial_payment_type'] ) ) {
1137 return false;
1138 }
1139 if ( ! isset( $rule['partial_payment_amount'] ) ) {
1140 return false;
1141 }
1142 }
1143
1144 if ( ! isset( $rule['user_condition'] ) ) {
1145 return false;
1146 }
1147
1148 if ( ( 'customers' === $rule['user_condition'] ) && empty( $rule['user_condition_users'] ) ) {
1149 return false;
1150 }
1151
1152 if ( ( 'roles' === $rule['user_condition'] ) && empty( $rule['user_condition_roles'] ) ) {
1153 return false;
1154 }
1155
1156 if ( empty( $rule['shipping_date'] ) ) {
1157 return false;
1158 }
1159
1160 if ( empty( $rule['button_text'] ) ) {
1161 return false;
1162 }
1163
1164 if ( empty( $rule['placement'] ) ) {
1165 return false;
1166 }
1167
1168 return true;
1169 }
1170
1171 /**
1172 * Prepare the rule fields.
1173 *
1174 * @param array $rule The rule to prepare.
1175 *
1176 * @return array The prepared rule.
1177 */
1178 private static function prepare_rule( $rule ) {
1179 if ( 'product' === $rule['trigger_on'] ) {
1180 $rule['product_ids'] = array_map( 'intval', explode( ',', $rule['product_ids'] ) );
1181 }
1182
1183 if ( ! empty( $rule['pre_order_start'] ) ) {
1184 $rule['pre_order_start'] = merchant_convert_date_to_timestamp( $rule['pre_order_start'], self::DATE_TIME_FORMAT );
1185 }
1186
1187 if ( ! empty( $rule['pre_order_end'] ) ) {
1188 $rule['pre_order_end'] = merchant_convert_date_to_timestamp( $rule['pre_order_end'], self::DATE_TIME_FORMAT );
1189 }
1190
1191 $rule['shipping_timestamp'] = merchant_convert_date_to_timestamp( $rule['shipping_date'], self::DATE_TIME_FORMAT );
1192
1193 return $rule;
1194 }
1195
1196 /**
1197 * Get the available product rule.
1198 *
1199 * @param string $product_id The product ID.
1200 *
1201 * @return array The available product rule.
1202 */
1203 public static function available_product_rule( $product_id ) {
1204 $available_rule = array();
1205 $rules = self::pre_order_rules();
1206 $current_time = merchant_get_current_timestamp();
1207 $current_user = wp_get_current_user();
1208 foreach ( $rules as $rule ) {
1209 if ( self::is_valid_rule( $rule ) ) {
1210 $rule = self::prepare_rule( $rule );
1211
1212 // check if pre-order start date is set and if it is not in the future
1213 if ( ! empty( $rule['pre_order_start'] ) && $rule['pre_order_start'] > $current_time ) {
1214 continue;
1215 }
1216
1217 // check if pre-order end date is set and if it is in the past
1218 if ( ! empty( $rule['pre_order_end'] ) && $rule['pre_order_end'] < $current_time ) {
1219 continue;
1220 }
1221
1222 if ( 'roles' === $rule['user_condition'] ) {
1223 $allowed_roles = $rule['user_condition_roles'];
1224 $user_roles = $current_user->roles;
1225 $intersect = array_intersect( $allowed_roles, $user_roles );
1226 if ( empty( $intersect ) ) {
1227 continue;
1228 }
1229 }
1230
1231 if ( 'customers' === $rule['user_condition'] ) {
1232 $allowed_users = $rule['user_condition_users'];
1233 $current_user_id = $current_user->ID;
1234 if ( ! in_array( $current_user_id, $allowed_users, true ) ) {
1235 continue;
1236 }
1237 }
1238
1239 if ( 'product' === $rule['trigger_on'] && in_array( $product_id, $rule['product_ids'], true ) ) {
1240 $available_rule = $rule;
1241 break;
1242 }
1243 if ( 'category' === $rule['trigger_on'] ) {
1244 $terms = get_the_terms( $product_id, 'product_cat' );
1245 if ( ! empty( $terms ) ) {
1246 foreach ( $terms as $term ) {
1247 if ( in_array( $term->slug, $rule['category_slugs'], true ) ) {
1248 $available_rule = $rule;
1249 break;
1250 }
1251 }
1252 }
1253 }
1254 }
1255 }
1256
1257 /**
1258 * Filter the available product rule.
1259 *
1260 * @param array $available_rule The available product rule.
1261 * @param int $product_id The product ID.
1262 *
1263 * @return array The available product rule.
1264 *
1265 * @since 1.9.9
1266 */
1267 return apply_filters( 'merchant_pre_order_available_rule', $available_rule, $product_id );
1268 }
1269
1270 /**
1271 * Migrate old data to the new module storage.
1272 *
1273 * @return void
1274 */
1275 public static function data_migration() {
1276 $args = array(
1277 'post_type' => array( 'product', 'product_variation' ),
1278 'posts_per_page' => - 1,
1279 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1280 'meta_query' => array(
1281 'relation' => 'AND',
1282 array(
1283 'key' => '_pre_order_date',
1284 'compare' => 'EXISTS',
1285 ),
1286 array(
1287 'key' => '_pre_order_date',
1288 'compare' => '!=',
1289 'value' => '',
1290 ),
1291 array(
1292 'key' => '_is_pre_order',
1293 'compare' => 'EXISTS',
1294 ),
1295 array(
1296 'key' => '_is_pre_order',
1297 'compare' => 'in',
1298 'value' => array( 'yes', '1' ),
1299 ),
1300 ),
1301 );
1302 $migrated = get_option( 'merchant_pre_orders_migrated' );
1303 if ( ! $migrated ) {
1304 $products = new WP_Query( $args );
1305 if ( $products->have_posts() ) {
1306 $rules = self::pre_order_rules();
1307 while ( $products->have_posts() ) {
1308 $products->the_post();
1309 $product_id = get_the_ID();
1310 $pre_order_date = get_post_meta( $product_id, '_pre_order_date', true );
1311 $is_pre_order = get_post_meta( $product_id, '_is_pre_order', true );
1312 $pre_order_date = merchant_convert_date_to_timestamp( $pre_order_date, 'yy-m-d' );
1313 $pre_order_date = date_i18n( self::DATE_TIME_FORMAT, $pre_order_date );
1314 if ( $is_pre_order ) {
1315 $rules[] = array(
1316 'offer-title' => esc_html__( 'Custom Pre-order', 'merchant' ),
1317 'trigger_on' => 'product',
1318 'product_ids' => $product_id,
1319 'discount_toggle' => false,
1320 'user_condition' => 'all',
1321 'shipping_date' => $pre_order_date,
1322 'button_text' => esc_html__( 'Pre Order Now!', 'merchant' ),
1323 'additional_text' => esc_html__( 'Ships on {date}.', 'merchant' ),
1324 'placement' => 'before',
1325 'cart_label_text' => esc_html__( 'Ships on', 'merchant' ),
1326 'layout' => 'rule-details',
1327 );
1328 }
1329 }
1330 Merchant_Admin_Options::set( self::MODULE_ID, 'rules', $rules );
1331 }
1332 update_option( 'merchant_pre_orders_migrated', true );
1333 }
1334 }
1335 }
1336
1337 add_action( 'init', 'Merchant_Pre_Orders_Main_Functionality::data_migration' );