PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.1.9
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.1.9
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.1.9, at app/Intents/IntentHelper.php

661 lines 19.7 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\Campaign;
14 use Disco\App\Utility\Helper;
15 use Disco\App\Utility\Settings;
16
17 /**
18 * This Class contains all the common function for cart related intents,
19 * and the intents are Cart, Bulk, Bundle and BOGO
20 *
21 * @package Disco
22 * @subpackage Disco\App\Intents
23 * @author Ohidul Islam <wahid0003@gmail.com>
24 * @link https://webappick.com
25 * @license https://opensource.org/licenses/gpl-license.php GNU Public License
26 * @category IntentHelper
27 */
28 trait IntentHelper {//phpcs:ignore
29
30 /**
31 * Disco Prepare Intents.
32 *
33 * @param array $intents Skip Intents.
34 * Either a single intent or an array of intents.
35 * @return array
36 */
37 public function prepare_intents( $intents = [] ) {//phpcs:ignore
38 $campaigns = ( new Campaign )->get_campaigns( '1' );
39
40
41 $new_intents = array();
42
43 $settings = Settings::get();
44
45 if ( empty( $campaigns ) ) {
46 return $new_intents;
47 }
48
49 foreach ( $campaigns as $campaign ) {
50 // Check discount expiration date.
51 if ( ! Helper::is_in_valid_date( $campaign ) ) {
52 continue;
53 }
54
55 // Skip the intent if it is in the skip list.
56 if (
57 ! empty( $intents )
58 && is_array( $intents )
59 && ! in_array( $campaign->discount_intent, $intents, true )
60 ) {
61 continue;
62 }
63
64 if ( $campaign->discount_intent === 'BOGO' ) {
65 $campaign->discount_intent = 'BuyXGetX';
66
67 if ( in_array( $campaign->bogo_type, array( 'products', 'categories' ), true ) ) {
68 $campaign->discount_intent = 'BuyXGetY';
69 }
70 }
71
72 $new_intents[] = IntentFactory::get_intent( $campaign, $settings );
73 }//end foreach
74
75 return $new_intents;
76 }
77
78 /**
79 * Get discount applicable items from the cart.
80 *
81 * @param \WC_Cart $cart Cart Object.
82 * @param \Disco\App\Utility\Config $campaign Campaign Config.
83 * @return array
84 */
85 public function get_items_for_discount( $cart, $campaign ) {
86 $items = array();
87
88 foreach ( $cart->get_cart() as $item ) {
89 $id = $item['product_id'];
90
91 if ( $item['variation_id'] ) {
92 $id = $item['variation_id'];
93 }
94
95 // Continue if the product is not applicable for the campaign.
96 if ( ! $campaign->product_is_applicable( $id ) ) {
97 continue;
98 }
99
100 // Continue if the item is not passed the filter.
101 $product = new \WC_Product( $id );
102
103 $info = array( 'product' => $product );
104
105 if ( ! Helper::is_filter_passed( $campaign, $info ) ) {
106 continue;
107 }
108
109 $items[] = $item;
110 }//end foreach
111
112 return $items;
113 }
114
115 /**
116 * Verify the rules.
117 *
118 * @param \Disco\App\Utility\Config $campaign Campaign Config.
119 * @param array $item Cart Item.
120 * @param array $rule Cart Item.
121 * @return bool
122 */
123 public function verify_rules( $campaign, $item, $rule ) {
124 if ( is_object( $rule ) ) {
125 $rule = (array) $rule;
126 }
127
128 $basis = $this->get_basis_for_item( $item );
129
130 if ( empty( $rule ) ) {
131 return false;
132 }
133
134 switch ( $campaign->get_discount_intent() ) {
135 case 'Product':
136 case 'Cart':
137 return isset( $rule['discount_value'], $rule['discount_type'] );
138
139 case 'Bulk':
140 return $this->verify_bulk_rule( $basis, $rule );
141
142 case 'Bundle':
143 return $this->verify_bundle_rule( $basis, $rule );
144
145 case 'BuyXGetX':
146 return $this->verify_buyxgetx_rule( $basis, $item, $rule );
147
148 case 'BuyXGetY':
149 return $this->verify_buyxgety_rule( $campaign, $basis, $item, $rule );
150
151 default:
152 return false;
153 }
154 }
155
156 /**
157 * Get cart item id.
158 *
159 * @param array $item Cart Item.
160 * @return int
161 */
162 public function get_cart_item_id( $item ) {
163 $id = $item['product_id'];
164
165 if ( $item['variation_id'] > 0 ) {
166 $id = $item['variation_id'];
167 }
168
169 return $id;
170 }
171
172 /**
173 * Get the basis for cart.
174 * Returns the quantity or price of the cart.
175 * If the cart is not valid, then return 0.
176 *
177 * @param \Disco\App\Utility\Config $campaign Discount Rules.
178 * @param \WC_Cart $cart Cart .
179 * @return float|int
180 */
181 public function get_basis_for_cart( $campaign, $cart ) {
182 if ( ! $cart instanceof \WC_Cart || $cart->is_empty() ) {
183 return 0;
184 }
185
186 if ( 'cart_quantity' === $campaign->get_discount_based_on() ) {
187 return absint( $cart->get_cart_contents_count() );
188 }
189
190 return abs( $cart->get_subtotal() );
191 }
192
193 /**
194 * Get the basis for item.
195 * Returns the quantity or price of the item.
196 * If the item is not valid, then return 0.
197 *
198 * @param array $item Cart Item.
199 * @return int
200 */
201 public function get_basis_for_item( $item ) {
202 if ( empty( $item['quantity'] ) ) {
203 return 0;
204 }
205
206 return absint( $item['quantity'] );
207 }
208
209 /**
210 * Calculate discount.
211 * Returns the discount amount.
212 *
213 * @param float $cost Item Price or Cart Subtotal.
214 * @param string $discount_type Discount Type.
215 * @param float $discount_value Discount Value.
216 * @return float
217 */
218 public function calculate_discount( $cost, $discount_type, $discount_value ) {
219 $cost = (float) $cost;
220 $discount_value = (float) $discount_value;
221
222 if ( 'percent' === $discount_type ) {
223 $cost = $cost * $discount_value / 100;
224 }
225
226 if ( 'fixed' === $discount_type || 'fixed_per_product' === $discount_type ) {
227 $cost = $discount_value;
228 }
229
230 return $cost;
231 }
232
233 /**
234 * Get the discounted price.
235 *
236 * @param float $cost Item Price or Cart Subtotal.
237 * @param float $discounted_amount Discounted Amount.
238 * @return float
239 */
240 public function discounted_price( $cost, $discounted_amount ) {
241 $discounted_price = $cost - $discounted_amount;
242
243 if ( $discounted_price < 0 ) {
244 $discounted_price = $cost;
245 }
246
247 return $discounted_price;
248 }
249
250 /**
251 * Check if a number is a multiple of another number for recursive discount.
252 *
253 * @param float|int $number Number to check.
254 * @param float|int $of Number to check against.
255 * @return bool Returns true if the number is a multiple of the given number, false otherwise.
256 */
257 public function is_multiple( $number, $of ) {
258 if ( $of >= $number ) {
259 return (float) $of % (float) $number === 0;
260 }
261
262 return false;
263 }
264
265 /**
266 * Check if a product is in a category.
267 *
268 * @param int $product_id Product ID.
269 * @param array $categories Categories.
270 * @return bool
271 */
272 public function is_in_category( $product_id, $categories ) {
273 $terms = get_the_terms( $product_id, 'product_cat' );
274 codecept_debug( $terms );
275
276 if ( ! $terms || is_wp_error( $terms ) ) {
277 return false;
278 }
279
280 $term_ids = wp_list_pluck( $terms, 'term_id' );
281 $common_values = array_intersect( $term_ids, $categories );
282
283 return ! empty( $common_values );
284 }
285
286 /**
287 * Prepare a discount array for cart.
288 * Returns the discount array.
289 *
290 * @param \Disco\App\Utility\Config $campaign Campaign Config .
291 * @param array $items Cart Item Object.
292 * @return array
293 */
294 public function get_item_discounts( $campaign, $items ) {//phpcs:ignore
295 $discounts = array();
296 // Get the discount rules.
297 $rules = $campaign->get_discount_rules();
298
299 // Check if the cart has valid items.
300 if ( ! is_array( $items ) && empty( $items ) ) {
301 return $discounts;
302 }
303
304 // Loop through the cart items.
305 foreach ( $items as $item ) {
306 // Get the item object.
307 $item_objet = $item['data'];
308
309 // Check if the campaign has rules.
310 if ( ! is_array( $rules ) || empty( $rules ) ) {
311 continue;
312 }
313
314 // Loop through the discount rules.
315 foreach ( $rules as $rule ) {
316 if ( is_object( $rule ) ) {
317 $rule = (array) $rule;
318 }
319
320 // Verify the rule by campaign settings.
321 if ( ! $this->verify_rules( $campaign, $item, $rule ) ) {
322 continue;
323 }
324
325 $r = md5( microtime() . wp_rand() );
326
327 $id = $this->get_cart_item_id( $item );
328 $discounted_amount = $this->calculate_discount( $item_objet->get_price(), $rule['discount_type'], $rule['discount_value'] );
329 $discounted_price = $this->discounted_price( $item_objet->get_price(), $discounted_amount );
330
331 // Set the discount array.
332 $discounts[ $id ]['original_price'] = $item_objet->get_price();
333 $discounts[ $id ]['item_key'] = $item['key'];
334
335 // Set Discount Price Array.
336 if ( $rule['discount_type'] === 'free' ) {
337 $discounts[ $id ]['prices'][ $r ] = 0;
338 } else {
339 $discounts[ $id ]['prices'][ $r ] = $discounted_price;
340 }
341
342 $discounts[ $id ]['discount_types'][ $r ] = $rule['discount_type'];
343 $discounts[ $id ]['intent'][ $r ] = $campaign->get_discount_intent();
344 $discounts[ $id ]['get_ids'] = array_values( $rule['get_ids'] );
345 $discounts[ $id ]['discounts'][ $r ] = $discounted_amount;
346 $discounts[ $id ]['labels'][ $r ] = $rule['discount_label'];
347 $discounts[ $id ]['quantities'][ $r ] = $rule['get_quantity'];
348 $discounts[ $id ]['bogo_qty'][ $r ] = '';
349 $offers = $this->get_item_offers( $rule );
350
351 // Calculate the discounted amount and price for BOGO.
352 if ( 'free' !== $rule['discount_type'] && $this->is_bogo( $campaign ) ) {
353 $bogo_discounts = $this->get_items_discount_for_bogo( $id, $discounts, $r, $rule, $item );
354 $discounts[ $id ] = array_merge( $discounts[ $id ], $bogo_discounts );
355 }
356
357 $discounts[ $id ]['offers'][ $r ] = $offers;
358 }//end foreach
359 }//end foreach
360
361 return $discounts;
362 }
363
364 /**
365 * Prepare bogo discounts for cart items.
366 *
367 * @param int $id Cart Item ID.
368 * @param array $discounts Cart Item Discounts.
369 * @param string $r Discount Rule Key.
370 * @param array $rule Discount Rule.
371 * @param array $item Cart Item.
372 * @return array
373 */
374 public function get_items_discount_for_bogo( $id, $discounts, $r, $rule, $item ) {
375 if ( empty( $rule['get_quantity'] ) || $rule['get_quantity'] <= 1 ) {
376 return array();
377 }
378
379 $discounted_amount = $discounts[ $id ]['discounts'][ $r ];
380
381 $discounts[ $id ]['discounts'][ $r ] = $discounted_amount * abs( $rule['get_quantity'] );
382 $subtotal = abs( $item['line_subtotal'] ) - $discounted_amount;
383 $new_price = $subtotal / abs( $item['quantity'] );
384 $discounts[ $id ]['prices'][ $r ] = $new_price;
385
386 $discount_str = '<del>' . wc_price( $discounts[ $id ]['original_price'] ) . '</del> <ins>' . wc_price( $new_price ) . '</ins>';
387 $offer_label = $rule['get_quantity'] . ' x ' . $discount_str . '<br/>';
388 $offer_label .= ( abs( $item['quantity'] ) - abs( $rule['get_quantity'] ) ) . ' x <ins>' . wc_price( $discounts[ $id ]['original_price'] ) . '</ins>';
389 $discounts[ $id ]['bogo_qty'][ $r ] = $offer_label;
390
391 return $discounts[ $id ];
392 }
393
394 /**
395 * Prepare discounts for cart items.
396 *
397 * @param array $intents Intents.
398 * @return array|false
399 */
400 public function prepare_item_discounts( $intents ) {
401 $this->prepare_intents( array( 'Cart' ) );// ,'Bundle', 'BOGO'
402 // @phpstan-ignore-line
403 $cart = WC()->cart;
404
405 if ( empty( $intents ) ) {
406 return false;
407 }
408
409 $discounts = array();
410
411 foreach ( $intents as $intent ) {
412 $items = $this->get_items_for_discount( $cart, $intent->campaign );
413
414 $get_discounts = $intent->get_discounts( $items, $cart );
415
416 if ( empty( $get_discounts ) ) {
417 continue;
418 }
419
420 $discounts = $this->merge_discounts( $discounts, $get_discounts );
421 }
422
423 return $discounts;
424 }
425
426 /**
427 * Is it a bogo campaign.
428 *
429 * @param \Disco\App\Utility\Config $campaign Campaign Config.
430 * @return bool
431 */
432 public function is_bogo( $campaign ) {
433 return in_array( $campaign->get_discount_intent(), array( 'BuyXGetX', 'BuyXGetY' ), true );
434 }
435
436 /**
437 * Check cart is valid or not
438 *
439 * @return bool
440 */
441 public function cart_is_valid() {
442 return WC()->cart instanceof \WC_Cart && ! WC()->cart->is_empty();
443 }
444
445 /**
446 * Get min or max price from an array discounted prices according to plugin settings.
447 *
448 * @param array $discounted_prices Discounted Prices.
449 * @return float
450 */
451 public function min_max_average( $discounted_prices ) {
452 $calculation_type = Settings::get( 'min_max_discount_amount' );
453
454 if ( empty( $discounted_prices ) ) {
455 return 0.00;
456 }
457
458 if ( 'average' === $calculation_type ) {
459 return (float) array_sum( $discounted_prices ) / count( $discounted_prices );
460 }
461
462 if ( 'max' === $calculation_type ) {
463 return (float) min( $discounted_prices );
464 }
465
466 return (float) max( $discounted_prices );
467 }
468
469 /**
470 * Check if the rule is recursive and the basis is not multiple.
471 *
472 * @param float $basis Number to compare.
473 * @param array $rule Discount Rules.
474 * @return bool
475 */
476 public function check_recursive_and_basis( $basis, $rule ) {
477 $rule_basis = abs( $rule['min'] );
478
479 if ( $rule['recursive'] === 'yes' && ! $this->is_multiple( $rule_basis, $basis ) ) {
480 return true;
481 }
482
483 return $rule['recursive'] === 'no' && (float) $basis !== (float) $rule_basis;
484 }
485
486 /**
487 * Merges the discounts for cart items.
488 *
489 * @param array $current_discounts The current discounts.
490 * @param array $new_discounts The new discounts.
491 * @return array The merged discounts.
492 */
493 private function merge_discounts( $current_discounts, $new_discounts ) {
494 foreach ( $new_discounts as $item_id => $discount ) {
495 if ( ! isset( $current_discounts[ $item_id ] ) ) {
496 $current_discounts[ $item_id ] = $discount;
497 }
498
499 $current_discounts[ $item_id ] = $this->merge_discount_data( $current_discounts[ $item_id ], $discount );
500 }
501
502 return $current_discounts;
503 }
504
505 /**
506 * Merges the discount data for a single cart item.
507 *
508 * @param array $current_data The current discount data.
509 * @param array $new_data The new discount data.
510 * @return array The merged discount data.
511 */
512 private function merge_discount_data( array $current_data, array $new_data ) {
513 if ( empty( $current_data ) ) {
514 return $new_data;
515 }
516
517 $data = array(
518 'prices' => array_unique( array_merge( $current_data['prices'], $new_data['prices'] ) ),
519 'discounts' => array_merge( $current_data['discounts'], $new_data['discounts'] ),
520 'quantities' => array_merge( $current_data['quantities'], $new_data['quantities'] ),
521 'get_ids' => array_merge( $current_data['get_ids'], $new_data['get_ids'] ),
522 'intent' => array_merge( $current_data['intent'], $new_data['intent'] ),
523 'labels' => array_merge( $current_data['labels'], $new_data['labels'] ),
524 'offers' => array_merge( $current_data['offers'], $new_data['offers'] ),
525 'bogo_qty' => array_merge( $current_data['bogo_qty'], $new_data['bogo_qty'] ),
526 'discount_types' => array_merge( $current_data['discount_types'], $new_data['discount_types'] ),
527 'original_price' => $new_data['original_price'],
528 'item_key' => $new_data['item_key'],
529 );
530 // Note: Consider if you need to recalculate this if 'prices' hasn't changed
531 $data['final_price'] = $this->min_max_average( $data['prices'] );
532 $data['key'] = array_search( $data['final_price'], $data['prices'], false );//phpcs:ignore
533
534 return $data;
535 }
536
537 /**
538 * Get Offer Label.
539 *
540 * @param array $rule Discount Rule.
541 * @return array
542 */
543 private function get_item_offers( $rule ) {
544 $label = '';
545 $quantity = $rule['min'];
546
547 if ( ! empty( $rule['max'] ) ) {
548 $quantity .= ' - ' . $rule['max'];
549 }
550
551 if ( 'free' === $rule['discount_type'] ) {
552 $label = 'Free';
553 }
554
555 if ( 'percent' === $rule['discount_type'] ) {
556 $label = $rule['discount_value'] . '% off';
557 }
558
559 if ( 'fixed' === $rule['discount_type'] ) {
560 $label = wc_price( $rule['discount_value'] ) . ' off';
561 }
562
563 if ( ! empty( $rule['get_quantity'] ) ) {
564 $quantity = $rule['get_quantity'];
565 }
566
567 return array(
568 'quantity' => $quantity,
569 'label' => $label,
570 );
571 }
572
573 /**
574 * Verify Bulk Intent Rules.
575 *
576 * @param float $basis Number to compare.
577 * @param array $rule Discount Rules.
578 * @return bool
579 */
580 private function verify_bulk_rule( $basis, $rule ) {
581 if ( empty( $rule['max'] ) ) {
582 $rule['max'] = PHP_INT_MAX;
583 }
584
585 return $basis >= $rule['min'] && $basis <= $rule['max'];
586 }
587
588 /**
589 * Verify Bundle Intent Rules.
590 *
591 * @param float $basis Number to compare.
592 * @param array $rule Discount Rules.
593 * @return bool
594 */
595 private function verify_bundle_rule( $basis, $rule ) {
596 $rule_basis = abs( $rule['min'] );
597
598 if ( $rule['recursive'] === 'yes' && ! $this->is_multiple( $rule_basis, $basis ) ) {
599 return false;
600 }
601
602 return $rule['recursive'] !== 'no' || (float) $basis === (float) $rule_basis;
603 }
604
605 /**
606 * Verify BuyXGetX Intent Rules.
607 *
608 * @param float $basis Number to compare.
609 * @param array $item Cart Item.
610 * @param array $rule Discount Rules.
611 * @return bool
612 */
613 private function verify_buyxgetx_rule( $basis, $item, $rule ) {//phpcs:ignore
614 $verified = ( $basis >= $rule['min'] && $basis <= $rule['max'] );
615
616 $rule_basis = abs( $rule['min'] );
617
618 if ( ! ( $verified & $rule['recursive'] ) && $this->is_multiple( $rule_basis, $basis ) ) {
619 $verified = true;
620 }
621
622 return $verified;
623 }
624
625 /**
626 * Verify BuyXGetY Intent Rules.
627 *
628 * @param \Disco\App\Utility\Config $campaign Campaign Config.
629 * @param float $basis Number to compare.
630 * @param array $item Cart Item.
631 * @param array $rule Discount Rules.
632 * @return bool
633 */
634 private function verify_buyxgety_rule( $campaign, $basis, $item, $rule ) {
635 if ( ! is_array( $rule['get_ids'] ) ) {
636 return false;
637 }
638
639 $id = $this->get_cart_item_id( $item );
640
641 if ( 'products' === $campaign->get_bogo_type() && ! in_array( $id, $rule['get_ids'], true ) ) {
642 return false;
643 }
644
645 if ( 'categories' === $campaign->get_bogo_type() && ! $this->is_in_category( $id, $rule['get_ids'] ) ) {
646 return false;
647 }
648
649 $verified = ( $basis >= $rule['min'] && $basis <= $rule['max'] );
650
651 $rule_basis = abs( $rule['min'] );
652
653 if ( ! ( $verified & $rule['recursive'] ) && $this->is_multiple( $rule_basis, $basis ) ) {
654 $verified = true;
655 }
656
657 return $verified;
658 }
659
660 }
661