PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.20
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.20
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 1.3.52 All 184 releases
← All changes | app/Intents/IntentHelper.php +110 -14 1.4.14 → 1.4.20 View file →
@@ -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.
@@ -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;
@@ -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,8 +951,24 @@
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 );
961 + }
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 + }
875 971 }
876 972
877 973 return $discounts;
878 974 }