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/Disco.php +94 -51 1.3.52 → 1.4.21 View file →
@@ -10,8 +10,9 @@
10 10 namespace Disco\App;
11 11
12 12 use Disco\App\Calc\CalcFactory;
13 13 use Disco\App\Features\UserLimit;
14 +use Disco\App\Utility\Config;
14 15 use Disco\App\Utility\Settings;
15 16
16 17 /**
17 18 * Class Disco
@@ -64,10 +65,8 @@
64 65 }
65 66
66 67 $discounts = array();
67 68
68 - $prev_discount = PHP_INT_MAX;
69 -
70 69 // Foreach intent, apply the discount.
71 70 foreach ( $this->intents as $intent ) {
72 71 /**
73 72 * Get a discount limit form campaign.
@@ -73,18 +72,9 @@
73 72 * Get a discount limit form campaign.
74 73 *
75 74 * Compare with total product meta and apply discount
76 75 */
77 - $discount_limit = $intent->campaign->discount_max_user;
78 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
79 -
80 - if (
81 - ! empty( $discount_limit )
82 - && (
83 - $discount_limit >= 0
84 - && $total_applied_campaign >= $discount_limit
85 - )
86 - ) {
76 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
87 77 continue;
88 78 }
89 79
90 80 $discount = $intent->get_discounts( (float) $price, $product );
@@ -92,15 +82,30 @@
92 82 if ( empty( $discount ) ) {
93 83 continue;
94 84 }
95 85
96 - // Get applied discount campaign.
97 - if ( $discount > 0 && $prev_discount > $discount ) {
98 - $this->applied_campaign_id = $intent->campaign->id;
99 - $prev_discount = $discount;
100 - }
101 -
102 - $discounts[] = $discount;
86 + /**
87 + * Normalize each campaign's amount before the amounts compete.
88 + *
89 + * A percent amount is derived from the product price, which a
90 + * currency switcher has already converted, so it arrives in the
91 + * active currency. A fixed amount is the rule value as typed, so
92 + * it arrives in the store's base currency. Comparing the two
93 + * directly picks a winner by exchange rate rather than by size:
94 + * percent scales with the rate and fixed does not, so `max` would
95 + * select percent on almost every product and `min` would select
96 + * fixed. Converting here, with each campaign's own discount type,
97 + * puts every amount in the same currency before min/max runs.
98 + *
99 + * Filtering per campaign also matches what CartIntent and the
100 + * Calc classes already do, so every path now compares like for
101 + * like.
102 + */
103 + $discounts[ $intent->campaign->id ] = (float) apply_filters(
104 + 'disco_final_discounted_amount',
105 + (float) $discount,
106 + $this->campaign_discount_type( $intent->campaign )
107 + );
103 108 }
104 109
105 110 // If no discount is applied, return the original price.
106 111 if ( empty( $discounts ) ) {
@@ -106,16 +111,37 @@
106 111 if ( empty( $discounts ) ) {
107 112 return $price;
108 113 }
109 114
110 - $discount_type = $intent->campaign->discount_rules[0]['discount_type'];
111 -
112 115 /**
113 - * Get the min or max discount amount according to plugin settings.
114 - * Apply the filter to modify final discounted amount based on discount types.
116 + * Only amounts the product can actually carry take part in the choice.
117 + *
118 + * A flat amount larger than the price cannot be applied, and it used
119 + * to win the min/max selection anyway and then be rejected by the
120 + * guard below, leaving the product at full price even though another
121 + * campaign offered a discount that would have fitted. With a 70%
122 + * campaign and a 50.00 campaign, everything priced under 50.00 was
123 + * discounted by nothing at all while a 50.00 product went free.
124 + *
125 + * Dropping the ones that do not fit first means `max` picks the
126 + * largest discount that can be given rather than the largest that was
127 + * offered, which is what asking for the maximum discount means.
115 128 */
116 - $discounted_amount = apply_filters( 'disco_final_discounted_amount', $this->min_max_average( $discounts ), $discount_type );
129 + $applicable = array_filter(
130 + $discounts,
131 + static function ( $amount ) use ( $price ) {
132 + return (float) $amount <= (float) $price;
133 + }
134 + );
117 135
136 + if ( empty( $applicable ) ) {
137 + return $price;
138 + }
139 +
140 + // Pick the winning discount amount, then map it back to the campaign that produced it.
141 + $discounted_amount = $this->min_max_average( array_values( $applicable ) );
142 + $this->applied_campaign_id = (int) array_search( $discounted_amount, $applicable, true );
143 +
118 144 if ( $discounted_amount <= $price ) {
119 145 // Get an applied campaign from DiscountLimit class.
120 146 ( new UserLimit )->disco_start_session_on_checkout( $this->applied_campaign_id );
121 147
@@ -152,18 +178,9 @@
152 178 * Get a discount limit form campaign.
153 179 *
154 180 * Compare with total product meta and apply discount
155 181 */
156 - $discount_limit = $intent->campaign->discount_max_user;
157 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
158 -
159 - if (
160 - ! empty( $discount_limit )
161 - && (
162 - $discount_limit >= 0
163 - && $total_applied_campaign >= $discount_limit
164 - )
165 - ) {
182 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
166 183 continue;
167 184 }
168 185
169 186 $items = $this->get_items_for_discount( $cart, $intent->campaign );
@@ -203,8 +220,10 @@
203 220 if ( ! empty( $discount_label ) ) {
204 221 $label = $discount_label;
205 222 }
206 223
224 + $cart_fee = $this->get_discount_exclude_tax( $cart_fee, $cart );
225 +
207 226 $cart->add_fee( $label, -$cart_fee );
208 227 $cart->set_session();
209 228 }
210 229 }
@@ -257,8 +276,10 @@
257 276 $total_discount += $discounts[ $id ];
258 277 }
259 278
260 279 if ( $total_discount > 0 ) {
280 + $total_discount = $this->get_discount_exclude_tax( $total_discount, $cart );
281 +
261 282 $cart->add_fee( __( 'Discount', 'disco' ), -$total_discount );
262 283 }
263 284
264 285 $cart->set_session();
@@ -274,21 +295,21 @@
274 295 * @param \WC_Cart $cart WooCommerce cart object to calculate discounts for.
275 296 * @return array|false|\WC_Cart Cart object with applied discounts or false if no discounts are applicable.
276 297 */
277 298 public function get_cart_items_discount_for_bogo( $cart ) {//phpcs:ignore
278 - if ( ! $this->cart_is_valid() ) {
279 - return $cart;
280 - }
299 + if ( ! $this->cart_is_valid() ) {
300 + return $cart;
301 + }
281 302
282 - if ( ! defined( 'DOING_AJAX' ) && is_admin() ) {
283 - return $cart;
284 - }
303 + if ( ! defined( 'DOING_AJAX' ) && is_admin() ) {
304 + return $cart;
305 + }
285 306
286 - // Init Cart - Based Intents except Product & Shipping Intent.
287 - $this->intents = $this->prepare_intents( array( 'BOGO' ) );
307 + // Init Cart - Based Intents except Product & Shipping Intent.
308 + $this->intents = $this->prepare_intents( array( 'BOGO' ) );
288 309
289 310 return $this->prepare_item_discounts_bogo_free( $this->intents, $cart );
290 - }
311 + }
291 312
292 313 /**
293 314 * Apply the discount to shipping.
294 315 *
@@ -328,16 +349,10 @@
328 349 * Get a discount limit from campaign.
329 350 *
330 351 * Compare with total applied count and skip if the user limit is hit.
331 352 */
332 - $discount_limit = $intent->campaign->discount_max_user;
333 -
334 - if ( ! empty( $discount_limit ) ) {
335 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
336 -
337 - if ( $total_applied_campaign >= $discount_limit ) {
338 - continue;
339 - }
353 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
354 + continue;
340 355 }
341 356
342 357 $items = $this->get_items_for_discount( $cart, $intent->campaign );
343 358
@@ -464,7 +479,35 @@
464 479 return false;
465 480 }
466 481
467 482 return in_array( $page_name, $pages, true );
483 + }
484 +
485 + /**
486 + * Resolve a campaign's discount type.
487 + *
488 + * Read through get_discount_rules() rather than the raw config property.
489 + * That accessor is what the intents use, so the type reported here is the
490 + * one the discount was actually calculated with: it decodes a rules payload
491 + * stored as JSON and fills in the defaults, neither of which the magic
492 + * property does. The property is also undeclared, so PHPStan cannot see it.
493 + *
494 + * @param \Disco\App\Utility\Config $campaign Campaign config.
495 + * @return string Discount type, or an empty string when it cannot be determined.
496 + */
497 + private function campaign_discount_type( Config $campaign ): string {
498 + $rules = $campaign->get_discount_rules();
499 +
500 + if ( ! is_array( $rules ) || ! isset( $rules[0] ) || ! is_object( $rules[0] ) ) {
501 + return '';
502 + }
503 +
504 + $discount_type = $rules[0]->discount_type ?? '';
505 +
506 + if ( ! is_scalar( $discount_type ) ) {
507 + return '';
508 + }
509 +
510 + return (string) $discount_type;
468 511 }
469 512
470 513 }