PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.3.49
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.3.49
1.4.15 1.4.14 1.4.13 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.54 1.3.53 1.3.52 1.3.51 1.3.50 1.3.49 1.3.48 1.3.47 1.3.46 All 178 releases
disco / app / Intents / IntentHelper.php

IntentHelper.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.3.49, at app/Intents/IntentHelper.php

976 lines 25.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php //phpcs:ignore
2
3 /**
4 * Intent Helper Trait.
5 *
6 * @package Disco
7 * @subpackage \App\Intents\IntentHelper.php
8 * @since 1.0.0
9 */
10
11 namespace Disco\App\Intents;
12
13 use Disco\App\Calc\CalcFactory;
14 use Disco\App\Campaign;
15 use Disco\App\Features\UserLimit;
16 use Disco\App\Utility\Config;
17 use Disco\App\Utility\Helper;
18 use Disco\App\Utility\Settings;
19
20 /**
21 * This Class contains all the common function for cart related intents,
22 * and the intents are Cart, Bulk, Bundle and BOGO
23 *
24 * @package Disco
25 * @subpackage Disco\App\Intents
26 * @author Ohidul Islam <wahid0003@gmail.com>
27 * @link https://webappick.com
28 * @license https://opensource.org/licenses/gpl-license.php GNU Public License
29 * @category IntentHelper
30 */
31 trait IntentHelper {//phpcs:ignore
32
33 /**
34 * Disco Prepare Intents.
35 *
36 * @param array $intents Skip Intents.
37 * Either a single intent or an array of intents.
38 * @return array
39 */
40 public function prepare_intents( $intents = [] ) {//phpcs:ignore
41 $campaigns = ( new Campaign )->get_campaigns( '1' );
42
43
44 $new_intents = array();
45
46 $settings = Settings::get();
47
48 if ( empty( $campaigns ) ) {
49 return $new_intents;
50 }
51
52 foreach ( $campaigns as $campaign ) {
53 // Check discount expiration date.
54 if ( ! Helper::is_in_valid_date( $campaign ) ) {
55 continue;
56 }
57
58 // Skip the intent if it is in the skip list.
59 if (
60 ! empty( $intents )
61 && is_array( $intents )
62 && ! in_array( $campaign->discount_intent, $intents, true )
63 ) {
64 continue;
65 }
66
67 if ( $campaign->discount_intent === 'BOGO' ) {
68 $campaign->discount_intent = 'BuyXGetX';
69
70 if ( in_array( $campaign->bogo_type, array( 'products', 'categories' ), true ) ) {
71 $campaign->discount_intent = 'BuyXGetY';
72 }
73 }
74
75 $new_intents[] = IntentFactory::get_intent( $campaign, $settings );
76 }//end foreach
77
78 return $new_intents;
79 }
80
81 /**
82 * Get discount applicable items from the cart.
83 *
84 * @param \WC_Cart $cart Cart Object.
85 * @param \Disco\App\Utility\Config $campaign Campaign Config.
86 * @return array
87 */
88 public function get_items_for_discount( $cart, $campaign ) { //phpcs:ignore
89 $items = array();
90 $discount_type = $campaign->get_discount_intent();
91 $cart_items = $cart->get_cart();
92 $bogo_type = $campaign->get_bogo_type();
93 $y_products = array();
94 $verified_yproducts = false;
95
96 /**
97 * Check if the discount type is BuyXGetY and the Y product is in the cart.
98 * If so, add the Y product to the items array.
99 */
100 if ( $discount_type === 'BuyXGetY' ) {
101 $rule_ids = $campaign->get_rule_product_ids();
102 $y_products = $this->get_y_product( $rule_ids, $cart_items, $bogo_type );
103 $verified_yproducts = $this->verify_yproduct_in_cart( $rule_ids, $cart_items, $bogo_type );
104 }
105
106 foreach ( $cart_items as $item ) {
107 // Skip free products - they should not count towards BOGO buy quantity
108 if ( ! empty( $item['is_free_product'] ) ) {
109 continue;
110 }
111
112 $id = $item['product_id'];
113
114 if ( $item['variation_id'] ) {
115 $id = $item['variation_id'];
116 }
117
118 // Continue if the product is not applicable for the campaign.
119 if ( ! $campaign->product_is_applicable( $id ) ) {
120 continue;
121 }
122
123 // Continue if the item is not passed the filter.
124 $product = wc_get_product( $id );
125
126 $info = array( 'product' => $product );
127
128 if ( ! Helper::is_filter_passed( $campaign, $info ) ) {
129 continue;
130 }
131
132 /**
133 * Check if the discount type is BuyXGetY and the Y product is in the cart.
134 * If so, add the Y product to the items array.
135 * These products not need to be passed the filter.
136 */
137 if ( $discount_type === 'BuyXGetY' && $verified_yproducts ) {
138 $items = array_merge( $items, $y_products );
139 }
140
141 $items[] = $item;
142 }//end foreach
143
144 return $items;
145 }
146
147 /**
148 * Verify the rules.
149 *
150 * @param \Disco\App\Utility\Config $campaign Campaign Config.
151 * @param array $item Cart Item.
152 * @param array $rule Cart Item.
153 * @return bool
154 */
155 public function verify_rules( $campaign, $item, $rule ) {
156 if ( is_object( $rule ) ) {
157 $rule = (array) $rule;
158 }
159
160 $basis = $this->get_basis_for_item( $item );
161
162 if ( empty( $rule ) ) {
163 return false;
164 }
165
166 switch ( $campaign->get_discount_intent() ) {
167 case 'Product':
168 case 'Cart':
169 return isset( $rule['discount_value'], $rule['discount_type'] );
170
171 case 'Bulk':
172 return $this->verify_bulk_rule( $basis, $rule );
173
174 case 'Bundle':
175 return $this->verify_bundle_rule( $basis, $rule );
176
177 case 'BuyXGetX':
178 return $this->verify_buyxgetx_rule( $basis, $item, $rule );
179
180 case 'BuyXGetY':
181 return $this->verify_buyxgety_rule( $campaign, $basis, $item, $rule );
182
183 default:
184 return false;
185 }
186 }
187
188 /**
189 * Get cart item id.
190 *
191 * @param array $item Cart Item.
192 * @return int
193 */
194 public function get_cart_item_id( $item ) {
195 $id = $item['product_id'];
196
197 if ( $item['variation_id'] > 0 ) {
198 $id = $item['variation_id'];
199 }
200
201 return $id;
202 }
203
204 /**
205 * Get the basis for cart.
206 * Returns the quantity or price of the cart.
207 * If the cart is not valid, then return 0.
208 *
209 * @param \Disco\App\Utility\Config $campaign Discount Rules.
210 * @param \WC_Cart $cart Cart .
211 * @return float|int
212 */
213 public function get_basis_for_cart( $campaign, $cart ) {
214 if ( ! $cart instanceof \WC_Cart || $cart->is_empty() ) {
215 return 0;
216 }
217
218 if ( 'cart_quantity' === $campaign->get_discount_based_on() ) {
219 return absint( $cart->get_cart_contents_count() );
220 }
221
222 return abs( $cart->get_subtotal() );
223 }
224
225 /**
226 * Get the basis for item.
227 * Returns the quantity or price of the item.
228 * If the item is not valid, then return 0.
229 *
230 * @param array $item Cart Item.
231 * @return int
232 */
233 public function get_basis_for_item( $item ) {
234 if ( empty( $item['quantity'] ) ) {
235 return 0;
236 }
237
238 return absint( $item['quantity'] );
239 }
240
241 /**
242 * Calculate discount.
243 * Returns the discount amount.
244 *
245 * @param float $cost Item Price or Cart Subtotal.
246 * @param string $discount_type Discount Type.
247 * @param float $discount_value Discount Value.
248 * @return float
249 */
250 public function calculate_discount( $cost, $discount_type, $discount_value ) {
251 $cost = (float) $cost;
252 $discount_value = (float) $discount_value;
253
254 if ( 'percent' === $discount_type ) {
255 $cost = $cost * $discount_value / 100;
256 }
257
258 if ( 'fixed' === $discount_type || 'fixed_per_product' === $discount_type ) {
259 $cost = $discount_value;
260 }
261
262 return $cost;
263 }
264
265 /**
266 * Get the discounted price.
267 *
268 * @param float $cost Item Price or Cart Subtotal.
269 * @param float $discounted_amount Discounted Amount.
270 * @return float Discounted Price.
271 */
272 public function discounted_price( $cost, $discounted_amount ) {
273 $discounted_price = $cost - $discounted_amount;
274
275 if ( $discounted_price < 0 ) {
276 $discounted_price = $cost;
277 }
278
279 return $discounted_price;
280 }
281
282 /**
283 * Get the discounted amount.
284 *
285 * @param float $cost Item Price or Cart Subtotal.
286 * @param float $discounted_amount Discounted Amount.
287 */
288 public function get_amount_after_discount( float $cost, float $discounted_amount ): float {
289 $discounted_amount = $cost - $discounted_amount;
290
291 if ( $discounted_amount < 0 ) {
292 $discounted_amount = $cost;
293 }
294
295 return $discounted_amount;
296 }
297
298 /**
299 * Check if a number is a multiple of another number for recursive discount.
300 *
301 * @param float|int $number Number to check.
302 * @param float|int $of Number to check against.
303 * @return bool Returns true if the number is a multiple of the given number, false otherwise.
304 */
305 public function is_multiple( $number, $of ) {
306 if ( $of >= $number ) {
307 return (float) $of % (float) $number === 0;
308 }
309
310 return false;
311 }
312
313 /**
314 * Check if a product is in a category.
315 *
316 * @param int $product_id Product ID.
317 * @param array $categories Categories.
318 * @return bool
319 */
320 public function is_in_category( $product_id, $categories ) {
321 // Ensure $categories is an array
322 if ( ! is_array( $categories ) ) {
323 $categories = array( $categories );
324 }
325
326 $get_product = wc_get_product( $product_id );
327
328 if ( ! $get_product ) {
329 return false;
330 }
331
332 if ( $get_product->get_type() === 'variation' ) {
333 $product_id = $get_product->get_parent_id();
334 }
335
336 $terms = get_the_terms( $product_id, 'product_cat' );
337
338 if ( ! $terms || is_wp_error( $terms ) ) {
339 return false;
340 }
341
342 $term_ids = array();
343
344 foreach ( $terms as $term ) {
345 $term_ids[] = $term->term_id;
346
347 // Include all parent category IDs
348 $parent_ids = get_ancestors( $term->term_id, 'product_cat' );
349
350 if ( empty( $parent_ids ) ) {
351 continue;
352 }
353
354 $term_ids = array_merge( $term_ids, $parent_ids );
355 }
356
357 // Remove duplicates just in case
358 $term_ids = array_unique( $term_ids );
359 $common_values = array_intersect( $term_ids, $categories );
360
361 return ! empty( $common_values );
362 }
363
364 /**
365 * Prepare a discount array for cart.
366 * Returns the discount array.
367 *
368 * @param \Disco\App\Utility\Config $campaign Campaign Config.
369 * @param array $items Cart Item Object.
370 * @param \WC_Cart $cart Cart Object.
371 */
372 public function get_item_discounts( Config $campaign, array $items, \WC_Cart $cart ): array {//phpcs:ignore
373 $discounts = array();
374 // $cart = WC()->cart;
375 // Get the discount rules.
376 $rules = $campaign->get_discount_rules();
377
378 // Check if the cart is not empty.
379 if ( empty( $items ) ) {
380 return $discounts;
381 }
382
383 /**
384 * Combined qualifying quantity for recursive BOGO, filtered by the
385 * conditions and few-products filters and excluding Disco free items.
386 */
387 $total_applicable_qty = $this->get_total_applicable_quantity( $cart, $campaign );
388
389 // Loop through the cart items.
390 foreach ( $items as $item ) {
391 // Get the item object.
392 $item_objet = $item['data'];
393
394 // Check if the campaign has rules.
395 if ( ! is_array( $rules ) || empty( $rules ) ) {
396 continue;
397 }
398
399 // Loop through the discount rules.
400 foreach ( $rules as $rule ) {
401 if ( is_object( $rule ) ) {
402 $rule = (array) $rule;
403 }
404
405 // Verify the rule by campaign settings.
406 if ( ! $this->verify_rules( $campaign, $item, $rule ) ) {
407 continue;
408 }
409
410 $r = md5( microtime() . wp_rand() );
411 $id = $this->get_cart_item_id( $item );
412
413 // Set the discount array.
414 $discounts[ $id ]['original_price'] = $item_objet->get_price();
415 $discounts[ $id ]['cart_item_key'] = $item['key'];
416
417 // Set discount applies to
418 $discounts[ $id ]['discount_applies_to'][ $r ] = CalcFactory::discount_applies_to( $rule, $campaign );
419
420 // Set Discounts.
421 if ( $rule['discount_type'] === 'free' ) {
422 $discounted_amount = array(
423 'price' => $item_objet->get_price(),
424 'discount' => 0,
425 'line_subtotal' => $item_objet->get_price() * $item['quantity'],
426 'discounted_quantities' => 0,
427 );
428 $discounts[ $id ]['free'] = true;
429 $discounts[ $id ]['get_ids'] = $rule['get_ids'];
430 $discounts[ $id ]['get_qty'] = CalcFactory::get_free_quantity( $rule, $item );
431
432 /**
433 * Recursive BOGO (any bogo type): base the free quantity on the
434 * combined qualifying quantity instead of this single line, so a
435 * cart whose qualifying quantity totals 4 grants 4 free items.
436 */
437 if ( 'yes' === $rule['recursive'] && (int) $rule['min'] > 0 ) {
438 $discounts[ $id ]['get_qty'] = (int) floor( $total_applicable_qty / (int) $rule['min'] ) * (int) $rule['get_quantity'];
439 }
440 } else {
441 $discounted_amount = CalcFactory::get_discount( $rule, $item, $cart, $campaign );
442 $discounts[ $id ]['free'] = false;
443 $discounts[ $id ]['get_ids'] = array();
444 $discounts[ $id ]['get_qty'] = 0;
445 }
446
447 // $discounts[ $id ]['get_ids'] = array_values( $rule['get_ids'] );
448
449 $discounts[ $id ]['discount_types'][ $r ] = $rule['discount_type'];
450 $discounts[ $id ]['intent'][ $r ] = $campaign->get_discount_intent();
451 $discounts[ $id ]['prices'][ $r ] = $discounted_amount['price'];
452 $discounts[ $id ]['discounts'][ $r ] = $discounted_amount['discount'];
453 $discounts[ $id ]['subtotals'][ $r ] = $discounted_amount['line_subtotal'];
454 $discounts[ $id ]['discounted_quantities'][ $r ] = $discounted_amount['discounted_quantities'];
455 $discounts[ $id ]['quantities'][ $r ] = $item['quantity'];
456 $offers = $this->get_item_offers( $rule );
457
458 $discounts[ $id ]['offers'][ $r ] = $offers;
459 }//end foreach
460 }//end foreach
461
462 return $discounts;
463 }
464
465 /**
466 * Prepare discounts for cart items.
467 *
468 * @param array $intents Intents.
469 * @param \WC_Cart $cart Cart.
470 * @return array|false
471 */
472 public function prepare_item_discounts( array $intents, \WC_Cart $cart ) { //phpcs:ignore
473 if ( empty( $intents ) ) {
474 return false;
475 }
476
477 $discounts = array();
478 $applied_campaign_id = 0;
479
480 // Loop through the intents.
481 foreach ( $intents as $intent ) {
482 /**
483 * Get a discount limit form campaign.
484 *
485 * Compare with total product meta and apply discount
486 */
487 $discount_limit = $intent->campaign->discount_max_user;
488 $applied_campaign_id = $intent->campaign->id;
489 $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
490
491 if ( ! empty( $discount_limit ) && ( $discount_limit >= 0 && $total_applied_campaign >= $discount_limit ) ) {
492 continue;
493 }
494
495 $items = $this->get_items_for_discount( $cart, $intent->campaign );
496 $get_discounts = $intent->get_discounts( $items, $cart );
497
498 if ( empty( $get_discounts ) ) {
499 continue;
500 }
501
502 foreach ( $get_discounts as $item_id => $discount ) {
503 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] );
504 }
505 }
506
507 // Check if the discounts are empty.
508 if ( empty( $discounts ) ) {
509 return false;
510 }
511
512 // Get the min or max discount amount for each item.
513 foreach ( $discounts as $item_id => $discount ) {
514 $discounts[ $item_id ] = $this->min_max_average( $discount['discounts'] );
515 }
516
517 // Set applied campaign on DiscountLimit class.
518 ( new UserLimit )->disco_start_session_on_checkout( $applied_campaign_id );
519
520 return $discounts;
521 }
522
523 /**
524 * Get valid BOGO category item ID.
525 *
526 * @param array $intents Intents.
527 * @param \WC_Cart $cart Cart.
528 * @param array $discounts Discounts.
529 * @return int|null
530 */
531 public function get_valid_bogo_category_item_id( array $intents, \WC_Cart $cart, array $discounts ) {
532 foreach ( $intents as $intent ) {
533 if (
534 $intent->campaign->get_discount_intent() !== 'BuyXGetY' ||
535 $intent->campaign->get_bogo_type() !== 'categories'
536 ) {
537 continue;
538 }
539
540 foreach ( $cart->get_cart() as $item ) {
541 $item_id = $this->get_cart_item_id( $item );
542
543 if ( isset( $discounts[ $item_id ] ) && $discounts[ $item_id ] > 0 ) {
544 return $item_id;
545 }
546 }
547 }
548
549 return null;
550 }
551
552 /**
553 * Prepare discounts for cart items.
554 *
555 * @param array $intents Intents.
556 * @param \WC_Cart $cart Cart.
557 * @return array|false
558 */
559 public function prepare_item_discounts_bogo_free( array $intents, \WC_Cart $cart ) { //phpcs:ignore
560 if ( empty( $intents ) ) {
561 return false;
562 }
563
564 $discounts = array();
565
566 $applied_campaign_id = 0;
567
568 // Loop through the intents.
569 foreach ( $intents as $intent ) {
570 /**
571 * Get a discount limit form campaign.
572 *
573 * Compare with total product meta and apply discount
574 */
575 $discount_limit = $intent->campaign->discount_max_user;
576 $applied_campaign_id = $intent->campaign->id;
577 $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
578
579 if ( ! empty( $discount_limit ) && ( $discount_limit >= 0 && $total_applied_campaign >= $discount_limit ) ) {
580 continue;
581 }
582
583 $items = $this->get_items_for_discount( $cart, $intent->campaign );
584 $get_discounts = $intent->get_discounts( $items, $cart );
585
586 if ( empty( $get_discounts ) ) {
587 continue;
588 }
589
590 foreach ( $get_discounts as $item_id => $discount ) {
591 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] );
592 $discounts[ $item_id ]['free'] = $discount['free'];
593 $discounts[ $item_id ]['get_ids'] = $discount['get_ids'];
594 $discounts[ $item_id ]['get_qty'] = $discount['get_qty'];
595 $discounts[ $item_id ]['bogo_type'] = $intent->campaign->get_bogo_type();
596 }
597 }
598
599 // Check if the discounts are empty.
600 if ( empty( $discounts ) ) {
601 return false;
602 }
603
604 // Get the min or max discount amount for each item.
605 foreach ( $discounts as $item_id => $discount ) {
606 $discounts['discount'] = $this->min_max_average( $discount['discounts'] );
607 $discounts['free'] = $discount['free'];
608 $discounts['get_ids'] = $discount['get_ids'] ? array_column( $discount['get_ids'], 'id' ) : array( $item_id ); //@phpcs:ignore
609 $discounts['get_qty'] = $discount['get_qty'];
610 $discounts['bogo_type'] = $discount['bogo_type'] ?? 'products';
611 }
612
613 // Set applied campaign on DiscountLimit class.
614 ( new UserLimit )->disco_start_session_on_checkout( $applied_campaign_id );
615
616 return $discounts;
617 }
618
619 /**
620 * Is it a bogo campaign?
621 *
622 * @param \Disco\App\Utility\Config $campaign Campaign Config.
623 */
624 public function is_bogo( Config $campaign ): bool {
625 return in_array( $campaign->get_discount_intent(), array( 'BuyXGetX', 'BuyXGetY' ), true );
626 }
627
628 /**
629 * Check cart is valid or not
630 */
631 public function cart_is_valid(): bool {
632 return WC()->cart instanceof \WC_Cart && ! WC()->cart->is_empty();
633 }
634
635 /**
636 * Get min or max price from an array discounted prices according to plugin settings.
637 *
638 * @param array $discounts Discounted Amounts.
639 */
640 public function min_max_average( array $discounts ): float {
641 $calculation_type = Settings::get( 'min_max_discount_amount' );
642
643 if ( empty( $discounts ) ) {
644 return 0.00;
645 }
646
647 if ( 'max' === $calculation_type ) {
648 return max( $discounts );
649 }
650
651 return min( $discounts );
652 }
653
654 /**
655 * Total qualifying quantity for recursive BOGO.
656 *
657 * Sums the quantity of every cart item that passes the campaign's
658 * few-products / "all" filter (product_is_applicable) and its conditions
659 * filter (is_filter_passed), excluding products Disco added as free. This is
660 * the combined "buy" quantity a recursive rule scales the free / discount
661 * quantity against, so products excluded by either filter are not counted.
662 *
663 * @param \WC_Cart $cart Cart object.
664 * @param \Disco\App\Utility\Config $campaign Campaign config.
665 */
666 private function get_total_applicable_quantity( $cart, $campaign ): int {
667 $total = 0;
668
669 $cart_items = $cart->get_cart();
670
671 if ( ! is_array( $cart_items ) ) {
672 return $total;
673 }
674
675 foreach ( $cart_items as $item ) {
676 if ( ! empty( $item['is_free_product'] ) ) {
677 continue;
678 }
679
680 $id = $item['product_id'];
681
682 if ( ! empty( $item['variation_id'] ) ) {
683 $id = $item['variation_id'];
684 }
685
686 if ( ! $campaign->product_is_applicable( $id ) ) {
687 continue;
688 }
689
690 $product = wc_get_product( $id );
691
692 if ( ! Helper::is_filter_passed( $campaign, array( 'product' => $product ) ) ) {
693 continue;
694 }
695
696 $total += (int) $item['quantity'];
697 }
698
699 return $total;
700 }
701
702 /**
703 * Get Offer Label.
704 *
705 * @param array $rule Discount Rule.
706 */
707 private function get_item_offers( array $rule ): array {
708 $label = '';
709 $quantity = $rule['min'];
710
711 if ( ! empty( $rule['max'] ) ) {
712 $quantity .= ' - ' . $rule['max'];
713 }
714
715 if ( 'free' === $rule['discount_type'] ) {
716 $label = 'Free';
717 }
718
719 if ( 'percent' === $rule['discount_type'] ) {
720 $label = $rule['discount_value'] . '% off';
721 }
722
723 if ( 'fixed' === $rule['discount_type'] ) {
724 $label = wc_price( $rule['discount_value'] ) . ' off';
725 }
726
727 if ( ! empty( $rule['get_quantity'] ) ) {
728 $quantity = $rule['get_quantity'];
729 }
730
731 return array(
732 'quantity' => $quantity,
733 'label' => $label,
734 );
735 }
736
737 /**
738 * Verify Bulk Intent Rules.
739 *
740 * @param int $quantity Item Quantity.
741 * @param array $rule Discount Rules.
742 */
743 private function verify_bulk_rule( int $quantity, array $rule ): bool {
744 $min = abs( $rule['min'] );
745
746 if ( empty( $rule['max'] ) ) {
747 $max = PHP_INT_MAX;
748 } else {
749 $max = abs( $rule['max'] );
750 }
751
752 if ( $quantity >= $min && $quantity <= $max ) {
753 return true;
754 }
755
756 return $quantity >= $max;
757 }
758
759 /**
760 * Verify Bundle Intent Rules.
761 *
762 * @param int $quantity Item Quantity.
763 * @param array $rule Discount Rules.
764 */
765 private function verify_bundle_rule( int $quantity, array $rule ): bool {
766 $rule_basis = abs( $rule['min'] );
767
768 if ( $rule['recursive'] === 'yes' ) {
769 if ( $quantity >= $rule_basis && ( $quantity % $rule_basis === 0 || $quantity > $rule_basis ) ) {
770 return true;
771 }
772 } elseif ( $quantity >= $rule_basis ) {
773 return true;
774 }
775
776 return false;
777 }
778
779 /**
780 * Verify BuyXGetX Intent Rules.
781 *
782 * @param float $quantity Item Quantity.
783 * @param array $item Cart Item.
784 * @param array $rule Discount Rules.
785 */
786 private function verify_buyxgetx_rule( float $quantity, array $item, array $rule ): bool {//phpcs:ignore
787
788 $verified = ( $quantity >= $rule['min'] && $quantity <= $rule['max'] );
789
790 if ( $verified && $rule['recursive'] === 'no' ) {
791 return true;
792 }
793
794 $base_quantity = abs( $rule['min'] );
795
796 if ( $rule['recursive'] === 'yes' && ! isset( $rule['max'] ) ) {
797 if ( $quantity >= $base_quantity && $quantity % $base_quantity === 0 ) {
798 return true;
799 }
800 } elseif ( $quantity >= $base_quantity ) {
801 return true;
802 }
803
804 return false;
805 }
806
807 /**
808 * Verify BuyXGetY Intent Rules.
809 *
810 * @param \Disco\App\Utility\Config $campaign Campaign Config.
811 * @param float $quantity Item Quantity.
812 * @param array $item Cart Item.
813 * @param array $rule Discount Rules.
814 */
815 private function verify_buyxgety_rule( Config $campaign, float $quantity, array $item, array $rule ): bool {
816 if ( ! is_array( $rule['get_ids'] ) ) {
817 return false;
818 }
819
820 $id = $this->get_cart_item_id( $item );
821 $rule_ids = array_column( $rule['get_ids'], 'id' );
822
823 /**
824 * This code use for automatically apply free items rule for all product BOGO campaigns.
825 */
826 if ( $rule['discount_type'] === 'free' && 'all' === $campaign->get_bogo_type() ) {
827 return $this->verify_buyxgetx_rule( $quantity, $item, $rule );
828 }
829
830 /**
831 * This code use for automatically apply free items rule product based BOGO campaigns.
832 */
833 if ( $rule['discount_type'] === 'free' && 'products' === $campaign->get_bogo_type() ) {
834 return $this->verify_xproduct_cart_rule( $campaign, $rule );
835 }
836
837 if ( 'products' === $campaign->get_bogo_type() && in_array( $id, $rule_ids, true ) ) {
838 return $this->verify_xproduct_cart_rule( $campaign, $rule );
839 }
840
841 if ( 'categories' === $campaign->get_bogo_type() && $this->is_in_category( $id, $rule_ids ) ) {
842 return $this->verify_xproduct_cart_rule( $campaign, $rule );
843 }
844
845 return false;
846 }
847
848 /**
849 * Verify XProduct Cart Rule.
850 *
851 * @param \Disco\App\Utility\Config $campaign Campaign Config.
852 * @param array $rule Discount Rules.
853 */
854 private function verify_xproduct_cart_rule( Config $campaign, array $rule ): bool { //phpcs:ignore
855 $cart = WC()->cart->get_cart();
856
857 foreach ( $cart as $item ) {
858 // Skip free products - they must not count towards the BOGO buy quantity,
859 // otherwise granted free items re-qualify as buy-X and escalate the tier.
860 if ( ! empty( $item['is_free_product'] ) ) {
861 continue;
862 }
863
864 $quantity = $item['quantity'];
865 $id = $item['product_id'];
866
867 if ( ! empty( $item['variation_id'] ) ) {
868 $id = $item['variation_id'];
869 }
870
871 if ( $campaign->product_is_applicable( $id ) && $this->verify_buyxgetx_rule( $quantity, $item, $rule ) ) {
872 $product = wc_get_product( $id );
873 $info = array( 'product' => $product );
874
875 if ( ! Helper::is_filter_passed( $campaign, $info ) ) {
876 continue;
877 }
878
879 return true;
880 }
881 }
882
883 return false;
884 }
885
886 /**
887 * Verify YProduct in Cart.
888 *
889 * @param array $rule_ids Rule Product IDs.
890 * @param array $cart Cart.
891 * @param string $bogo_type BOGO Type.
892 * @return bool Returns true if a valid Y product is found in the cart, false otherwise.
893 */
894 private function verify_yproduct_in_cart( array $rule_ids, array $cart, string $bogo_type ): bool { //phpcs:ignore
895 if ( 'categories' === $bogo_type && ! empty( $rule_ids ) ) {
896 foreach ( $cart as $item ) {
897 $id = $item['product_id'];
898
899 if ( ! empty( $item['variation_id'] ) ) {
900 $id = $item['variation_id'];
901 }
902
903 if ( ! $this->is_in_category( $id, $rule_ids ) ) {
904 continue;
905 }
906
907 return true; // Found a valid Y product
908 }
909 }
910
911 if ( 'products' === $bogo_type && ! empty( $rule_ids ) ) {
912 foreach ( $cart as $item ) {
913 $id = $item['product_id'];
914
915 if ( ! empty( $item['variation_id'] ) ) {
916 $id = $item['variation_id'];
917 }
918
919 if ( ! in_array( $id, $rule_ids, true ) ) {
920 continue;
921 }
922
923 return true; // Found a valid Y product
924 }
925 }
926
927 return false;
928 }
929
930 /**
931 * Get Y products from cart.
932 *
933 * @param array $rule_ids Rule Product IDs.
934 * @param array $cart Cart.
935 * @param string $bogo_type BOGO Type.
936 */
937 private function get_y_product( array $rule_ids, array $cart, string $bogo_type ): array { //phpcs:ignore
938 $y_products = array();
939
940 if ( 'categories' === $bogo_type && ! empty( $rule_ids ) ) {
941 foreach ( $cart as $item ) {
942 $id = $item['product_id'];
943
944 if ( ! empty( $item['variation_id'] ) ) {
945 $id = $item['variation_id'];
946 }
947
948 if ( ! $this->is_in_category( $id, $rule_ids ) ) {
949 continue;
950 }
951
952 $y_products[] = $item; // Push the full cart item
953 }
954 }
955
956 if ( 'products' === $bogo_type && ! empty( $rule_ids ) ) {
957 foreach ( $cart as $item ) {
958 $id = $item['product_id'];
959
960 if ( ! empty( $item['variation_id'] ) ) {
961 $id = $item['variation_id'];
962 }
963
964 if ( ! in_array( $id, $rule_ids, true ) ) {
965 continue;
966 }
967
968 $y_products[] = $item; // Push the full cart item
969 }
970 }
971
972 return $y_products;
973 }
974
975 }
976