PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.3.53
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.3.53
1.4.17 1.4.16 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 All 180 releases
disco / app / Intents / IntentHelper.php

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

971 lines 25.8 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
479 // Loop through the intents.
480 foreach ( $intents as $intent ) {
481 /**
482 * Get a discount limit form campaign.
483 *
484 * Compare with total product meta and apply discount
485 */
486 $discount_limit = $intent->campaign->discount_max_user;
487 $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
488
489 if ( ! empty( $discount_limit ) && ( $discount_limit >= 0 && $total_applied_campaign >= $discount_limit ) ) {
490 continue;
491 }
492
493 $items = $this->get_items_for_discount( $cart, $intent->campaign );
494 $get_discounts = $intent->get_discounts( $items, $cart );
495
496 if ( empty( $get_discounts ) ) {
497 continue;
498 }
499
500 // Only stage campaigns whose discount actually applied to the cart.
501 ( new UserLimit )->disco_start_session_on_checkout( $intent->campaign->id );
502
503 foreach ( $get_discounts as $item_id => $discount ) {
504 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] );
505 }
506 }
507
508 // Check if the discounts are empty.
509 if ( empty( $discounts ) ) {
510 return false;
511 }
512
513 // Get the min or max discount amount for each item.
514 foreach ( $discounts as $item_id => $discount ) {
515 $discounts[ $item_id ] = $this->min_max_average( $discount['discounts'] );
516 }
517
518 return $discounts;
519 }
520
521 /**
522 * Get valid BOGO category item ID.
523 *
524 * @param array $intents Intents.
525 * @param \WC_Cart $cart Cart.
526 * @param array $discounts Discounts.
527 * @return int|null
528 */
529 public function get_valid_bogo_category_item_id( array $intents, \WC_Cart $cart, array $discounts ) {
530 foreach ( $intents as $intent ) {
531 if (
532 $intent->campaign->get_discount_intent() !== 'BuyXGetY' ||
533 $intent->campaign->get_bogo_type() !== 'categories'
534 ) {
535 continue;
536 }
537
538 foreach ( $cart->get_cart() as $item ) {
539 $item_id = $this->get_cart_item_id( $item );
540
541 if ( isset( $discounts[ $item_id ] ) && $discounts[ $item_id ] > 0 ) {
542 return $item_id;
543 }
544 }
545 }
546
547 return null;
548 }
549
550 /**
551 * Prepare discounts for cart items.
552 *
553 * @param array $intents Intents.
554 * @param \WC_Cart $cart Cart.
555 * @return array|false
556 */
557 public function prepare_item_discounts_bogo_free( array $intents, \WC_Cart $cart ) { //phpcs:ignore
558 if ( empty( $intents ) ) {
559 return false;
560 }
561
562 $discounts = array();
563
564 // Loop through the intents.
565 foreach ( $intents as $intent ) {
566 /**
567 * Get a discount limit form campaign.
568 *
569 * Compare with total product meta and apply discount
570 */
571 $discount_limit = $intent->campaign->discount_max_user;
572 $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
573
574 if ( ! empty( $discount_limit ) && ( $discount_limit >= 0 && $total_applied_campaign >= $discount_limit ) ) {
575 continue;
576 }
577
578 $items = $this->get_items_for_discount( $cart, $intent->campaign );
579 $get_discounts = $intent->get_discounts( $items, $cart );
580
581 if ( empty( $get_discounts ) ) {
582 continue;
583 }
584
585 // Only stage campaigns whose discount actually applied to the cart.
586 ( new UserLimit )->disco_start_session_on_checkout( $intent->campaign->id );
587
588 foreach ( $get_discounts as $item_id => $discount ) {
589 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] );
590 $discounts[ $item_id ]['free'] = $discount['free'];
591 $discounts[ $item_id ]['get_ids'] = $discount['get_ids'];
592 $discounts[ $item_id ]['get_qty'] = $discount['get_qty'];
593 $discounts[ $item_id ]['bogo_type'] = $intent->campaign->get_bogo_type();
594 }
595 }
596
597 // Check if the discounts are empty.
598 if ( empty( $discounts ) ) {
599 return false;
600 }
601
602 // Get the min or max discount amount for each item.
603 foreach ( $discounts as $item_id => $discount ) {
604 $discounts['discount'] = $this->min_max_average( $discount['discounts'] );
605 $discounts['free'] = $discount['free'];
606 $discounts['get_ids'] = $discount['get_ids'] ? array_column( $discount['get_ids'], 'id' ) : array( $item_id ); //@phpcs:ignore
607 $discounts['get_qty'] = $discount['get_qty'];
608 $discounts['bogo_type'] = $discount['bogo_type'] ?? 'products';
609 }
610
611 return $discounts;
612 }
613
614 /**
615 * Is it a bogo campaign?
616 *
617 * @param \Disco\App\Utility\Config $campaign Campaign Config.
618 */
619 public function is_bogo( Config $campaign ): bool {
620 return in_array( $campaign->get_discount_intent(), array( 'BuyXGetX', 'BuyXGetY' ), true );
621 }
622
623 /**
624 * Check cart is valid or not
625 */
626 public function cart_is_valid(): bool {
627 return WC()->cart instanceof \WC_Cart && ! WC()->cart->is_empty();
628 }
629
630 /**
631 * Get min or max price from an array discounted prices according to plugin settings.
632 *
633 * @param array $discounts Discounted Amounts.
634 */
635 public function min_max_average( array $discounts ): float {
636 $calculation_type = Settings::get( 'min_max_discount_amount' );
637
638 if ( empty( $discounts ) ) {
639 return 0.00;
640 }
641
642 if ( 'max' === $calculation_type ) {
643 return max( $discounts );
644 }
645
646 return min( $discounts );
647 }
648
649 /**
650 * Total qualifying quantity for recursive BOGO.
651 *
652 * Sums the quantity of every cart item that passes the campaign's
653 * few-products / "all" filter (product_is_applicable) and its conditions
654 * filter (is_filter_passed), excluding products Disco added as free. This is
655 * the combined "buy" quantity a recursive rule scales the free / discount
656 * quantity against, so products excluded by either filter are not counted.
657 *
658 * @param \WC_Cart $cart Cart object.
659 * @param \Disco\App\Utility\Config $campaign Campaign config.
660 */
661 private function get_total_applicable_quantity( $cart, $campaign ): int {
662 $total = 0;
663
664 $cart_items = $cart->get_cart();
665
666 if ( ! is_array( $cart_items ) ) {
667 return $total;
668 }
669
670 foreach ( $cart_items as $item ) {
671 if ( ! empty( $item['is_free_product'] ) ) {
672 continue;
673 }
674
675 $id = $item['product_id'];
676
677 if ( ! empty( $item['variation_id'] ) ) {
678 $id = $item['variation_id'];
679 }
680
681 if ( ! $campaign->product_is_applicable( $id ) ) {
682 continue;
683 }
684
685 $product = wc_get_product( $id );
686
687 if ( ! Helper::is_filter_passed( $campaign, array( 'product' => $product ) ) ) {
688 continue;
689 }
690
691 $total += (int) $item['quantity'];
692 }
693
694 return $total;
695 }
696
697 /**
698 * Get Offer Label.
699 *
700 * @param array $rule Discount Rule.
701 */
702 private function get_item_offers( array $rule ): array {
703 $label = '';
704 $quantity = $rule['min'];
705
706 if ( ! empty( $rule['max'] ) ) {
707 $quantity .= ' - ' . $rule['max'];
708 }
709
710 if ( 'free' === $rule['discount_type'] ) {
711 $label = 'Free';
712 }
713
714 if ( 'percent' === $rule['discount_type'] ) {
715 $label = $rule['discount_value'] . '% off';
716 }
717
718 if ( 'fixed' === $rule['discount_type'] ) {
719 $label = wc_price( $rule['discount_value'] ) . ' off';
720 }
721
722 if ( ! empty( $rule['get_quantity'] ) ) {
723 $quantity = $rule['get_quantity'];
724 }
725
726 return array(
727 'quantity' => $quantity,
728 'label' => $label,
729 );
730 }
731
732 /**
733 * Verify Bulk Intent Rules.
734 *
735 * @param int $quantity Item Quantity.
736 * @param array $rule Discount Rules.
737 */
738 private function verify_bulk_rule( int $quantity, array $rule ): bool {
739 $min = abs( $rule['min'] );
740
741 if ( empty( $rule['max'] ) ) {
742 $max = PHP_INT_MAX;
743 } else {
744 $max = abs( $rule['max'] );
745 }
746
747 if ( $quantity >= $min && $quantity <= $max ) {
748 return true;
749 }
750
751 return $quantity >= $max;
752 }
753
754 /**
755 * Verify Bundle Intent Rules.
756 *
757 * @param int $quantity Item Quantity.
758 * @param array $rule Discount Rules.
759 */
760 private function verify_bundle_rule( int $quantity, array $rule ): bool {
761 $rule_basis = abs( $rule['min'] );
762
763 if ( $rule['recursive'] === 'yes' ) {
764 if ( $quantity >= $rule_basis && ( $quantity % $rule_basis === 0 || $quantity > $rule_basis ) ) {
765 return true;
766 }
767 } elseif ( $quantity >= $rule_basis ) {
768 return true;
769 }
770
771 return false;
772 }
773
774 /**
775 * Verify BuyXGetX Intent Rules.
776 *
777 * @param float $quantity Item Quantity.
778 * @param array $item Cart Item.
779 * @param array $rule Discount Rules.
780 */
781 private function verify_buyxgetx_rule( float $quantity, array $item, array $rule ): bool {//phpcs:ignore
782
783 $verified = ( $quantity >= $rule['min'] && $quantity <= $rule['max'] );
784
785 if ( $verified && $rule['recursive'] === 'no' ) {
786 return true;
787 }
788
789 $base_quantity = abs( $rule['min'] );
790
791 if ( $rule['recursive'] === 'yes' && ! isset( $rule['max'] ) ) {
792 if ( $quantity >= $base_quantity && $quantity % $base_quantity === 0 ) {
793 return true;
794 }
795 } elseif ( $quantity >= $base_quantity ) {
796 return true;
797 }
798
799 return false;
800 }
801
802 /**
803 * Verify BuyXGetY Intent Rules.
804 *
805 * @param \Disco\App\Utility\Config $campaign Campaign Config.
806 * @param float $quantity Item Quantity.
807 * @param array $item Cart Item.
808 * @param array $rule Discount Rules.
809 */
810 private function verify_buyxgety_rule( Config $campaign, float $quantity, array $item, array $rule ): bool {
811 if ( ! is_array( $rule['get_ids'] ) ) {
812 return false;
813 }
814
815 $id = $this->get_cart_item_id( $item );
816 $rule_ids = array_column( $rule['get_ids'], 'id' );
817
818 /**
819 * This code use for automatically apply free items rule for all product BOGO campaigns.
820 */
821 if ( $rule['discount_type'] === 'free' && 'all' === $campaign->get_bogo_type() ) {
822 return $this->verify_buyxgetx_rule( $quantity, $item, $rule );
823 }
824
825 /**
826 * This code use for automatically apply free items rule product based BOGO campaigns.
827 */
828 if ( $rule['discount_type'] === 'free' && 'products' === $campaign->get_bogo_type() ) {
829 return $this->verify_xproduct_cart_rule( $campaign, $rule );
830 }
831
832 if ( 'products' === $campaign->get_bogo_type() && in_array( $id, $rule_ids, true ) ) {
833 return $this->verify_xproduct_cart_rule( $campaign, $rule );
834 }
835
836 if ( 'categories' === $campaign->get_bogo_type() && $this->is_in_category( $id, $rule_ids ) ) {
837 return $this->verify_xproduct_cart_rule( $campaign, $rule );
838 }
839
840 return false;
841 }
842
843 /**
844 * Verify XProduct Cart Rule.
845 *
846 * @param \Disco\App\Utility\Config $campaign Campaign Config.
847 * @param array $rule Discount Rules.
848 */
849 private function verify_xproduct_cart_rule( Config $campaign, array $rule ): bool { //phpcs:ignore
850 $cart = WC()->cart->get_cart();
851
852 foreach ( $cart as $item ) {
853 // Skip free products - they must not count towards the BOGO buy quantity,
854 // otherwise granted free items re-qualify as buy-X and escalate the tier.
855 if ( ! empty( $item['is_free_product'] ) ) {
856 continue;
857 }
858
859 $quantity = $item['quantity'];
860 $id = $item['product_id'];
861
862 if ( ! empty( $item['variation_id'] ) ) {
863 $id = $item['variation_id'];
864 }
865
866 if ( $campaign->product_is_applicable( $id ) && $this->verify_buyxgetx_rule( $quantity, $item, $rule ) ) {
867 $product = wc_get_product( $id );
868 $info = array( 'product' => $product );
869
870 if ( ! Helper::is_filter_passed( $campaign, $info ) ) {
871 continue;
872 }
873
874 return true;
875 }
876 }
877
878 return false;
879 }
880
881 /**
882 * Verify YProduct in Cart.
883 *
884 * @param array $rule_ids Rule Product IDs.
885 * @param array $cart Cart.
886 * @param string $bogo_type BOGO Type.
887 * @return bool Returns true if a valid Y product is found in the cart, false otherwise.
888 */
889 private function verify_yproduct_in_cart( array $rule_ids, array $cart, string $bogo_type ): bool { //phpcs:ignore
890 if ( 'categories' === $bogo_type && ! empty( $rule_ids ) ) {
891 foreach ( $cart as $item ) {
892 $id = $item['product_id'];
893
894 if ( ! empty( $item['variation_id'] ) ) {
895 $id = $item['variation_id'];
896 }
897
898 if ( ! $this->is_in_category( $id, $rule_ids ) ) {
899 continue;
900 }
901
902 return true; // Found a valid Y product
903 }
904 }
905
906 if ( 'products' === $bogo_type && ! empty( $rule_ids ) ) {
907 foreach ( $cart as $item ) {
908 $id = $item['product_id'];
909
910 if ( ! empty( $item['variation_id'] ) ) {
911 $id = $item['variation_id'];
912 }
913
914 if ( ! in_array( $id, $rule_ids, true ) ) {
915 continue;
916 }
917
918 return true; // Found a valid Y product
919 }
920 }
921
922 return false;
923 }
924
925 /**
926 * Get Y products from cart.
927 *
928 * @param array $rule_ids Rule Product IDs.
929 * @param array $cart Cart.
930 * @param string $bogo_type BOGO Type.
931 */
932 private function get_y_product( array $rule_ids, array $cart, string $bogo_type ): array { //phpcs:ignore
933 $y_products = array();
934
935 if ( 'categories' === $bogo_type && ! empty( $rule_ids ) ) {
936 foreach ( $cart as $item ) {
937 $id = $item['product_id'];
938
939 if ( ! empty( $item['variation_id'] ) ) {
940 $id = $item['variation_id'];
941 }
942
943 if ( ! $this->is_in_category( $id, $rule_ids ) ) {
944 continue;
945 }
946
947 $y_products[] = $item; // Push the full cart item
948 }
949 }
950
951 if ( 'products' === $bogo_type && ! empty( $rule_ids ) ) {
952 foreach ( $cart as $item ) {
953 $id = $item['product_id'];
954
955 if ( ! empty( $item['variation_id'] ) ) {
956 $id = $item['variation_id'];
957 }
958
959 if ( ! in_array( $id, $rule_ids, true ) ) {
960 continue;
961 }
962
963 $y_products[] = $item; // Push the full cart item
964 }
965 }
966
967 return $y_products;
968 }
969
970 }
971