| @@ -493,8 +493,37 @@ | ||
| 493 | 493 | } |
| 494 | 494 | |
| 495 | 495 | $discountable_units = $this->grouped_discount_budget( $rule, $total_qualifying_units ); |
| 496 | 496 | |
| 497 | + /** | |
| 498 | + * A flat amount is charged for the pooled group as a whole. | |
| 499 | + * | |
| 500 | + * Pooling exists so the cart together can reach the tier minimum, so | |
| 501 | + * the rule qualifies for the group rather than for each line that | |
| 502 | + * contributed quantity. The Calc layer returns the rule value whole | |
| 503 | + * for every line it is handed, because a flat amount does not scale | |
| 504 | + * with units, so spreading it across lines multiplied the discount by | |
| 505 | + * the line count: a flat 10 came out as 20 over two lines. | |
| 506 | + * | |
| 507 | + * It is applied to the first eligible line carrying the entire pooled | |
| 508 | + * budget, which is what lets a recursive rule still charge once per | |
| 509 | + * bundle: the Calc layer derives the bundle count from that quantity. | |
| 510 | + * Per unit types keep every line, since their amount is meant to | |
| 511 | + * scale with the units. | |
| 512 | + */ | |
| 513 | + if ( in_array( $rule['discount_type'], array( 'fixed', 'fixed_price' ), true ) ) { | |
| 514 | + $first_line = reset( $eligible_lines ); | |
| 515 | + | |
| 516 | + if ( ! empty( $first_line['item'] ) && $discountable_units > 0 ) { | |
| 517 | + $item = $first_line['item']; | |
| 518 | + $item['disco_forced_qty'] = $discountable_units; | |
| 519 | + | |
| 520 | + $this->apply_rule_to_discount( $discounts, $campaign, $rule, $item, $cart, $total_applicable_qty ); | |
| 521 | + } | |
| 522 | + | |
| 523 | + continue; | |
| 524 | + } | |
| 525 | + | |
| 497 | 526 | // Hand out the discountable units across the eligible lines in cart order. |
| 498 | 527 | foreach ( $eligible_lines as $eligible_line ) { |
| 499 | 528 | if ( $discountable_units <= 0 ) { |
| 500 | 529 | break; |
| @@ -663,13 +692,19 @@ | ||
| 663 | 692 | if ( empty( $get_discounts ) ) { |
| 664 | 693 | continue; |
| 665 | 694 | } |
| 666 | 695 | |
| 667 | - // Only stage campaigns whose discount actually applied to the cart. | |
| 668 | - ( new UserLimit )->disco_start_session_on_checkout( $intent->campaign->id ); | |
| 669 | - | |
| 670 | 696 | foreach ( $get_discounts as $item_id => $discount ) { |
| 697 | + /** | |
| 698 | + * Keep the campaign alongside its amount at the same index. | |
| 699 | + * | |
| 700 | + * Staging cannot happen yet: several campaigns can offer an amount | |
| 701 | + * for the same item and only one of them survives the min/max | |
| 702 | + * reduction below. Crediting every campaign here would consume the | |
| 703 | + * usage limit of campaigns whose discount never reached the cart. | |
| 704 | + */ | |
| 671 | 705 | $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] ); |
| 706 | + $discounts[ $item_id ]['campaigns'][] = (int) $intent->campaign->id; | |
| 672 | 707 | } |
| 673 | 708 | } |
| 674 | 709 | |
| 675 | 710 | // Check if the discounts are empty. |
| @@ -676,13 +711,27 @@ | ||
| 676 | 711 | if ( empty( $discounts ) ) { |
| 677 | 712 | return false; |
| 678 | 713 | } |
| 679 | 714 | |
| 715 | + $applied_campaign_ids = array(); | |
| 716 | + | |
| 680 | 717 | // Get the min or max discount amount for each item. |
| 681 | 718 | foreach ( $discounts as $item_id => $discount ) { |
| 682 | - $discounts[ $item_id ] = $this->min_max_average( $discount['discounts'] ); | |
| 719 | + $winning_amount = $this->min_max_average( $discount['discounts'] ); | |
| 720 | + $winning_index = array_search( $winning_amount, $discount['discounts'], true ); | |
| 721 | + | |
| 722 | + if ( false !== $winning_index && isset( $discount['campaigns'][ $winning_index ] ) ) { | |
| 723 | + $applied_campaign_ids[ $discount['campaigns'][ $winning_index ] ] = true; | |
| 724 | + } | |
| 725 | + | |
| 726 | + $discounts[ $item_id ] = $winning_amount; | |
| 683 | 727 | } |
| 684 | 728 | |
| 729 | + // Stage only the campaigns that actually won an item. | |
| 730 | + foreach ( array_keys( $applied_campaign_ids ) as $applied_campaign_id ) { | |
| 731 | + ( new UserLimit )->disco_start_session_on_checkout( $applied_campaign_id ); | |
| 732 | + } | |
| 733 | + | |
| 685 | 734 | return $discounts; |
| 686 | 735 | } |
| 687 | 736 | |
| 688 | 737 | /** |
| @@ -761,9 +810,10 @@ | ||
| 761 | 810 | if ( empty( $intents ) ) { |
| 762 | 811 | return false; |
| 763 | 812 | } |
| 764 | 813 | |
| 765 | - $discounts = array(); | |
| 814 | + $discounts = array(); | |
| 815 | + $candidate_campaign_ids = array(); | |
| 766 | 816 | |
| 767 | 817 | // Loop through the intents. |
| 768 | 818 | foreach ( $intents as $intent ) { |
| 769 | 819 | /** |
| @@ -781,10 +831,18 @@ | ||
| 781 | 831 | if ( empty( $get_discounts ) ) { |
| 782 | 832 | continue; |
| 783 | 833 | } |
| 784 | 834 | |
| 785 | - // Only stage campaigns whose discount actually applied to the cart. | |
| 786 | - ( new UserLimit )->disco_start_session_on_checkout( $intent->campaign->id ); | |
| 835 | + /** | |
| 836 | + * Collect the campaign now, stage it later. | |
| 837 | + * | |
| 838 | + * What this method finally grants is decided well below: the rewards | |
| 839 | + * are unioned across campaigns and a BuyXGetY (products) campaign can | |
| 840 | + * then override the whole result authoritatively. Crediting a campaign | |
| 841 | + * here would consume its usage limit even when the final result grants | |
| 842 | + * it nothing. | |
| 843 | + */ | |
| 844 | + $candidate_campaign_ids[] = (int) $intent->campaign->id; | |
| 787 | 845 | |
| 788 | 846 | foreach ( $get_discounts as $item_id => $discount ) { |
| 789 | 847 | $discounts[ $item_id ]['discounts'][] = max( $discount['discounts'] ); // phpcs:ignore |
| 790 | 848 | $discounts[ $item_id ]['free'] = $discount['free']; // phpcs:ignore |
| @@ -871,8 +929,24 @@ | ||
| 871 | 929 | $discounts['get_qty'] = empty( $reward_map ) ? 0 : max( $reward_map ); // phpcs:ignore |
| 872 | 930 | $discounts['free'] = ! empty( $reward_map ); // phpcs:ignore |
| 873 | 931 | $discounts['bogo_type'] = $bogo_type; |
| 874 | 932 | $discounts['free_item_selection'] = $intent->campaign->get_free_item_selection(); |
| 933 | + | |
| 934 | + /** | |
| 935 | + * This branch is authoritative for the whole result, so the campaign | |
| 936 | + * that owns it is the only one that can be credited. | |
| 937 | + */ | |
| 938 | + $candidate_campaign_ids = array( (int) $intent->campaign->id ); | |
| 939 | + } | |
| 940 | + | |
| 941 | + /** | |
| 942 | + * Stage the campaigns only once the result is final, and only when it | |
| 943 | + * actually grants free items. | |
| 944 | + */ | |
| 945 | + if ( ! empty( $discounts['free'] ) ) { | |
| 946 | + foreach ( array_unique( $candidate_campaign_ids ) as $applied_campaign_id ) { | |
| 947 | + ( new UserLimit )->disco_start_session_on_checkout( $applied_campaign_id ); | |
| 948 | + } | |
| 875 | 949 | } |
| 876 | 950 | |
| 877 | 951 | return $discounts; |
| 878 | 952 | } |