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 +122 -26 1.4.14 → 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'],
@@ -663,13 +714,19 @@
663 714 if ( empty( $get_discounts ) ) {
664 715 continue;
665 716 }
666 717
667 - // Only stage campaigns whose discount actually applied to the cart.
668 - ( new UserLimit )->disco_start_session_on_checkout( $intent->campaign->id );
669 -
670 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 + */
671 727 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] );
728 + $discounts[ $item_id ]['campaigns'][] = (int) $intent->campaign->id;
672 729 }
673 730 }
674 731
675 732 // Check if the discounts are empty.
@@ -676,13 +733,27 @@
676 733 if ( empty( $discounts ) ) {
677 734 return false;
678 735 }
679 736
737 + $applied_campaign_ids = array();
738 +
680 739 // Get the min or max discount amount for each item.
681 740 foreach ( $discounts as $item_id => $discount ) {
682 - $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;
683 749 }
684 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 +
685 756 return $discounts;
686 757 }
687 758
688 759 /**
@@ -761,9 +832,10 @@
761 832 if ( empty( $intents ) ) {
762 833 return false;
763 834 }
764 835
765 - $discounts = array();
836 + $discounts = array();
837 + $candidate_campaign_ids = array();
766 838
767 839 // Loop through the intents.
768 840 foreach ( $intents as $intent ) {
769 841 /**
@@ -781,10 +853,18 @@
781 853 if ( empty( $get_discounts ) ) {
782 854 continue;
783 855 }
784 856
785 - // Only stage campaigns whose discount actually applied to the cart.
786 - ( 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;
787 867
788 868 foreach ( $get_discounts as $item_id => $discount ) {
789 869 $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] ); // phpcs:ignore
790 870 $discounts[ $item_id ]['free'] = $discount['free']; // phpcs:ignore
@@ -871,10 +951,26 @@
871 951 $discounts['get_qty'] = empty( $reward_map ) ? 0 : max( $reward_map ); // phpcs:ignore
872 952 $discounts['free'] = ! empty( $reward_map ); // phpcs:ignore
873 953 $discounts['bogo_type'] = $bogo_type;
874 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 );
875 961 }
876 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 +
877 973 return $discounts;
878 974 }
879 975
880 976 /**
@@ -1419,10 +1515,10 @@
1419 1515 */
1420 1516 private function verify_bundle_rule( int $quantity, array $rule ): bool {
1421 1517 $rule_basis = abs( $rule['min'] );
1422 1518
1423 - if ( $rule['recursive'] === 'yes' ) {
1424 - 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 ) ) {
1425 1521 return true;
1426 1522 }
1427 1523 } elseif ( $quantity >= $rule_basis ) {
1428 1524 return true;
@@ -1441,16 +1537,16 @@
1441 1537 private function verify_buyxgetx_rule( float $quantity, array $item, array $rule ): bool {//phpcs:ignore
1442 1538
1443 1539 $verified = ( $quantity >= $rule['min'] && $quantity <= $rule['max'] );
1444 1540
1445 - if ( $verified && $rule['recursive'] === 'no' ) {
1541 + if ( $verified && 'no' === $rule['recursive'] ) {
1446 1542 return true;
1447 1543 }
1448 1544
1449 1545 $base_quantity = abs( $rule['min'] );
1450 1546
1451 - if ( $rule['recursive'] === 'yes' && ! isset( $rule['max'] ) ) {
1452 - 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 ) {
1453 1549 return true;
1454 1550 }
1455 1551 } elseif ( $quantity >= $base_quantity ) {
1456 1552 return true;
@@ -1477,9 +1573,9 @@
1477 1573
1478 1574 /**
1479 1575 * This code use for automatically apply free items rule for all product BOGO campaigns.
1480 1576 */
1481 - if ( $rule['discount_type'] === 'free' && 'all' === $campaign->get_bogo_type() ) {
1577 + if ( 'free' === $rule['discount_type'] && 'all' === $campaign->get_bogo_type() ) {
1482 1578 return $this->verify_buyxgetx_rule( $quantity, $item, $rule );
1483 1579 }
1484 1580
1485 1581 /**
@@ -1484,9 +1580,9 @@
1484 1580
1485 1581 /**
1486 1582 * This code use for automatically apply free items rule product based BOGO campaigns.
1487 1583 */
1488 - if ( $rule['discount_type'] === 'free' && 'products' === $campaign->get_bogo_type() ) {
1584 + if ( 'free' === $rule['discount_type'] && 'products' === $campaign->get_bogo_type() ) {
1489 1585 return $this->verify_xproduct_cart_rule( $campaign, $rule );
1490 1586 }
1491 1587
1492 1588 if ( 'products' === $campaign->get_bogo_type() && in_array( $id, $rule_ids, true ) ) {