PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.21
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.21
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 +114 -32 1.4.7 → 1.4.21 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;
@@ -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,8 +951,24 @@
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 );
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 + }
889 971 }
890 972
891 973 return $discounts;
892 974 }