PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.22
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.22
1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 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 All 185 releases
← All changes | app/Intents/IntentHelper.php +126 -44 1.4.6 → 1.4.22 View file →
@@ -64,9 +64,9 @@
64 64 ) {
65 65 continue;
66 66 }
67 67
68 - if ( $campaign->discount_intent === 'BOGO' ) {
68 + if ( 'BOGO' === $campaign->discount_intent ) {
69 69 $campaign->discount_intent = 'BuyXGetX';
70 70
71 71 if ( in_array( $campaign->bogo_type, array( 'products', 'categories' ), true ) ) {
72 72 $campaign->discount_intent = 'BuyXGetY';
@@ -97,9 +97,9 @@
97 97 /**
98 98 * Check if the discount type is BuyXGetY and the Y product is in the cart.
99 99 * If so, add the Y product to the items array.
100 100 */
101 - if ( $discount_type === 'BuyXGetY' ) {
101 + if ( 'BuyXGetY' === $discount_type ) {
102 102 $rule_ids = $campaign->get_rule_product_ids();
103 103 $y_products = $this->get_y_product( $rule_ids, $cart_items, $bogo_type );
104 104 $verified_yproducts = $this->verify_yproduct_in_cart( $rule_ids, $cart_items, $bogo_type );
105 105 }
@@ -134,9 +134,9 @@
134 134 * Check if the discount type is BuyXGetY and the Y product is in the cart.
135 135 * If so, add the Y product to the items array.
136 136 * These products not need to be passed the filter.
137 137 */
138 - if ( $discount_type === 'BuyXGetY' && $verified_yproducts ) {
138 + if ( 'BuyXGetY' === $discount_type && $verified_yproducts ) {
139 139 $items = array_merge( $items, $y_products );
140 140 }
141 141
142 142 $items[] = $item;
@@ -251,17 +251,39 @@
251 251 public function calculate_discount( $cost, $discount_type, $discount_value ) {
252 252 $cost = (float) $cost;
253 253 $discount_value = (float) $discount_value;
254 254
255 - if ( 'percent' === $discount_type ) {
256 - $cost = $cost * $discount_value / 100;
257 - }
255 + switch ( $discount_type ) {
256 + case 'percent':
257 + return $cost * $discount_value / 100;
258 258
259 - if ( 'fixed' === $discount_type || 'fixed_per_product' === $discount_type ) {
260 - $cost = $discount_value;
261 - }
259 + case 'fixed':
260 + case 'fixed_per_product':
261 + return $discount_value;
262 262
263 - return $cost;
263 + case 'free':
264 + // The whole cost comes off, which is what makes the item free.
265 + return $cost;
266 +
267 + default:
268 + /**
269 + * Anything else discounts nothing.
270 + *
271 + * This used to fall through and return $cost untouched, which the
272 + * callers then treat as the discount: ProductIntent hands it the
273 + * product price and CartIntent the cart subtotal, so an
274 + * unrecognised type gave away the product or the entire cart.
275 + *
276 + * Only Bulk, Bundle and BOGO route through the Calc classes,
277 + * where every type has an implementation; Product and Cart come
278 + * straight here, so the gap was reachable by any rule carrying a
279 + * type this method does not know: a campaign saved while
280 + * fixed_price and percent_per_product were still offered in the
281 + * builder, a REST write (discount_type inside discount_rules is
282 + * not validated), or a hand edited row.
283 + */
284 + return 0.0;
285 + }//end switch
264 286 }
265 287
266 288 /**
267 289 * Get the discounted price.
@@ -304,9 +326,9 @@
304 326 * @return bool Returns true if the number is a multiple of the given number, false otherwise.
305 327 */
306 328 public function is_multiple( $number, $of ) {
307 329 if ( $of >= $number ) {
308 - return (float) $of % (float) $number === 0;
330 + return 0 === (float) $of % (float) $number;
309 331 }
310 332
311 333 return false;
312 334 }
@@ -493,8 +515,37 @@
493 515 }
494 516
495 517 $discountable_units = $this->grouped_discount_budget( $rule, $total_qualifying_units );
496 518
519 + /**
520 + * A flat amount is charged for the pooled group as a whole.
521 + *
522 + * Pooling exists so the cart together can reach the tier minimum, so
523 + * the rule qualifies for the group rather than for each line that
524 + * contributed quantity. The Calc layer returns the rule value whole
525 + * for every line it is handed, because a flat amount does not scale
526 + * with units, so spreading it across lines multiplied the discount by
527 + * the line count: a flat 10 came out as 20 over two lines.
528 + *
529 + * It is applied to the first eligible line carrying the entire pooled
530 + * budget, which is what lets a recursive rule still charge once per
531 + * bundle: the Calc layer derives the bundle count from that quantity.
532 + * Per unit types keep every line, since their amount is meant to
533 + * scale with the units.
534 + */
535 + if ( in_array( $rule['discount_type'], array( 'fixed', 'fixed_price' ), true ) ) {
536 + $first_line = reset( $eligible_lines );
537 +
538 + if ( ! empty( $first_line['item'] ) && $discountable_units > 0 ) {
539 + $item = $first_line['item'];
540 + $item['disco_forced_qty'] = $discountable_units;
541 +
542 + $this->apply_rule_to_discount( $discounts, $campaign, $rule, $item, $cart, $total_applicable_qty );
543 + }
544 +
545 + continue;
546 + }
547 +
497 548 // Hand out the discountable units across the eligible lines in cart order.
498 549 foreach ( $eligible_lines as $eligible_line ) {
499 550 if ( $discountable_units <= 0 ) {
500 551 break;
@@ -572,9 +623,9 @@
572 623 // Set discount applies to
573 624 $discounts[ $effective_product_id ]['discount_applies_to'][ $rule_key ] = CalcFactory::discount_applies_to( $rule, $campaign );
574 625
575 626 // Set Discounts.
576 - if ( $rule['discount_type'] === 'free' ) {
627 + if ( 'free' === $rule['discount_type'] ) {
577 628 $discounted_amount = array(
578 629 'price' => $product->get_price(),
579 630 'discount' => 0,
580 631 'line_subtotal' => $product->get_price() * $item['quantity'],
@@ -652,17 +703,10 @@
652 703 * Get a discount limit form campaign.
653 704 *
654 705 * Compare with total product meta and apply discount
655 706 */
656 - $discount_limit = $intent->campaign->discount_max_user;
657 -
658 - // Only count applied orders when a limit is actually configured.
659 - if ( ! empty( $discount_limit ) && $discount_limit >= 0 ) {
660 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
661 -
662 - if ( $total_applied_campaign >= $discount_limit ) {
663 - continue;
664 - }
707 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
708 + continue;
665 709 }
666 710
667 711 $items = $this->get_items_for_discount( $cart, $intent->campaign );
668 712 $get_discounts = $intent->get_discounts( $items, $cart );
@@ -670,13 +714,19 @@
670 714 if ( empty( $get_discounts ) ) {
671 715 continue;
672 716 }
673 717
674 - // Only stage campaigns whose discount actually applied to the cart.
675 - ( new UserLimit )->disco_start_session_on_checkout( $intent->campaign->id );
676 -
677 718 foreach ( $get_discounts as $item_id => $discount ) {
719 + /**
720 + * Keep the campaign alongside its amount at the same index.
721 + *
722 + * Staging cannot happen yet: several campaigns can offer an amount
723 + * for the same item and only one of them survives the min/max
724 + * reduction below. Crediting every campaign here would consume the
725 + * usage limit of campaigns whose discount never reached the cart.
726 + */
678 727 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] );
728 + $discounts[ $item_id ]['campaigns'][] = (int) $intent->campaign->id;
679 729 }
680 730 }
681 731
682 732 // Check if the discounts are empty.
@@ -683,13 +733,27 @@
683 733 if ( empty( $discounts ) ) {
684 734 return false;
685 735 }
686 736
737 + $applied_campaign_ids = array();
738 +
687 739 // Get the min or max discount amount for each item.
688 740 foreach ( $discounts as $item_id => $discount ) {
689 - $discounts[ $item_id ] = $this->min_max_average( $discount['discounts'] );
741 + $winning_amount = $this->min_max_average( $discount['discounts'] );
742 + $winning_index = array_search( $winning_amount, $discount['discounts'], true );
743 +
744 + if ( false !== $winning_index && isset( $discount['campaigns'][ $winning_index ] ) ) {
745 + $applied_campaign_ids[ $discount['campaigns'][ $winning_index ] ] = true;
746 + }
747 +
748 + $discounts[ $item_id ] = $winning_amount;
690 749 }
691 750
751 + // Stage only the campaigns that actually won an item.
752 + foreach ( array_keys( $applied_campaign_ids ) as $applied_campaign_id ) {
753 + ( new UserLimit )->disco_start_session_on_checkout( $applied_campaign_id );
754 + }
755 +
692 756 return $discounts;
693 757 }
694 758
695 759 /**
@@ -768,9 +832,10 @@
768 832 if ( empty( $intents ) ) {
769 833 return false;
770 834 }
771 835
772 - $discounts = array();
836 + $discounts = array();
837 + $candidate_campaign_ids = array();
773 838
774 839 // Loop through the intents.
775 840 foreach ( $intents as $intent ) {
776 841 /**
@@ -777,17 +842,10 @@
777 842 * Get a discount limit form campaign.
778 843 *
779 844 * Compare with total product meta and apply discount
780 845 */
781 - $discount_limit = $intent->campaign->discount_max_user;
782 -
783 - // Only count applied orders when a limit is actually configured.
784 - if ( ! empty( $discount_limit ) && $discount_limit >= 0 ) {
785 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
786 -
787 - if ( $total_applied_campaign >= $discount_limit ) {
788 - continue;
789 - }
846 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
847 + continue;
790 848 }
791 849
792 850 $items = $this->get_items_for_discount( $cart, $intent->campaign );
793 851 $get_discounts = $intent->get_discounts( $items, $cart );
@@ -795,10 +853,18 @@
795 853 if ( empty( $get_discounts ) ) {
796 854 continue;
797 855 }
798 856
799 - // Only stage campaigns whose discount actually applied to the cart.
800 - ( new UserLimit )->disco_start_session_on_checkout( $intent->campaign->id );
857 + /**
858 + * Collect the campaign now, stage it later.
859 + *
860 + * What this method finally grants is decided well below: the rewards
861 + * are unioned across campaigns and a BuyXGetY (products) campaign can
862 + * then override the whole result authoritatively. Crediting a campaign
863 + * here would consume its usage limit even when the final result grants
864 + * it nothing.
865 + */
866 + $candidate_campaign_ids[] = (int) $intent->campaign->id;
801 867
802 868 foreach ( $get_discounts as $item_id => $discount ) {
803 869 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] ); // phpcs:ignore
804 870 $discounts[ $item_id ]['free'] = $discount['free']; // phpcs:ignore
@@ -885,10 +951,26 @@
885 951 $discounts['get_qty'] = empty( $reward_map ) ? 0 : max( $reward_map ); // phpcs:ignore
886 952 $discounts['free'] = ! empty( $reward_map ); // phpcs:ignore
887 953 $discounts['bogo_type'] = $bogo_type;
888 954 $discounts['free_item_selection'] = $intent->campaign->get_free_item_selection();
955 +
956 + /**
957 + * This branch is authoritative for the whole result, so the campaign
958 + * that owns it is the only one that can be credited.
959 + */
960 + $candidate_campaign_ids = array( (int) $intent->campaign->id );
889 961 }
890 962
963 + /**
964 + * Stage the campaigns only once the result is final, and only when it
965 + * actually grants free items.
966 + */
967 + if ( ! empty( $discounts['free'] ) ) {
968 + foreach ( array_unique( $candidate_campaign_ids ) as $applied_campaign_id ) {
969 + ( new UserLimit )->disco_start_session_on_checkout( $applied_campaign_id );
970 + }
971 + }
972 +
891 973 return $discounts;
892 974 }
893 975
894 976 /**
@@ -1433,10 +1515,10 @@
1433 1515 */
1434 1516 private function verify_bundle_rule( int $quantity, array $rule ): bool {
1435 1517 $rule_basis = abs( $rule['min'] );
1436 1518
1437 - if ( $rule['recursive'] === 'yes' ) {
1438 - if ( $quantity >= $rule_basis && ( $quantity % $rule_basis === 0 || $quantity > $rule_basis ) ) {
1519 + if ( 'yes' === $rule['recursive'] ) {
1520 + if ( $quantity >= $rule_basis && ( 0 === $quantity % $rule_basis || $quantity > $rule_basis ) ) {
1439 1521 return true;
1440 1522 }
1441 1523 } elseif ( $quantity >= $rule_basis ) {
1442 1524 return true;
@@ -1455,16 +1537,16 @@
1455 1537 private function verify_buyxgetx_rule( float $quantity, array $item, array $rule ): bool {//phpcs:ignore
1456 1538
1457 1539 $verified = ( $quantity >= $rule['min'] && $quantity <= $rule['max'] );
1458 1540
1459 - if ( $verified && $rule['recursive'] === 'no' ) {
1541 + if ( $verified && 'no' === $rule['recursive'] ) {
1460 1542 return true;
1461 1543 }
1462 1544
1463 1545 $base_quantity = abs( $rule['min'] );
1464 1546
1465 - if ( $rule['recursive'] === 'yes' && ! isset( $rule['max'] ) ) {
1466 - if ( $quantity >= $base_quantity && $quantity % $base_quantity === 0 ) {
1547 + if ( 'yes' === $rule['recursive'] && ! isset( $rule['max'] ) ) {
1548 + if ( $quantity >= $base_quantity && 0 === $quantity % $base_quantity ) {
1467 1549 return true;
1468 1550 }
1469 1551 } elseif ( $quantity >= $base_quantity ) {
1470 1552 return true;
@@ -1491,9 +1573,9 @@
1491 1573
1492 1574 /**
1493 1575 * This code use for automatically apply free items rule for all product BOGO campaigns.
1494 1576 */
1495 - if ( $rule['discount_type'] === 'free' && 'all' === $campaign->get_bogo_type() ) {
1577 + if ( 'free' === $rule['discount_type'] && 'all' === $campaign->get_bogo_type() ) {
1496 1578 return $this->verify_buyxgetx_rule( $quantity, $item, $rule );
1497 1579 }
1498 1580
1499 1581 /**
@@ -1498,9 +1580,9 @@
1498 1580
1499 1581 /**
1500 1582 * This code use for automatically apply free items rule product based BOGO campaigns.
1501 1583 */
1502 - if ( $rule['discount_type'] === 'free' && 'products' === $campaign->get_bogo_type() ) {
1584 + if ( 'free' === $rule['discount_type'] && 'products' === $campaign->get_bogo_type() ) {
1503 1585 return $this->verify_xproduct_cart_rule( $campaign, $rule );
1504 1586 }
1505 1587
1506 1588 if ( 'products' === $campaign->get_bogo_type() && in_array( $id, $rule_ids, true ) ) {