PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.17
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.17
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 1.3.51 1.3.50 1.3.49 1.3.48 All 180 releases
← All changes | app/Disco.php +105 -96 1.3.481.4.17 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
@@ -46,12 +47,8 @@
46 47 * @return float Product Price.
47 48 * @throws \Exception If setting is not found.
48 49 */
49 50 public function get_product_discounted_price( $price, $product ) {//phpcs:ignore
50 - if ( Settings::get( 'discount_priority_type' ) === 'woocommerce_coupon' ) {
51 - return $price; // Return original price if WooCommerce coupon is used.
52 - }
53 -
54 51 if ( $product instanceof \WC_Product ) {
55 52 // Init product base intents and exclude cart-based intents.
56 53 $this->intents = $this->prepare_intents( array( 'Product' ) );
57 54
@@ -68,10 +65,8 @@
68 65 }
69 66
70 67 $discounts = array();
71 68
72 - $prev_discount = PHP_INT_MAX;
73 -
74 69 // Foreach intent, apply the discount.
75 70 foreach ( $this->intents as $intent ) {
76 71 /**
77 72 * Get a discount limit form campaign.
@@ -77,18 +72,9 @@
77 72 * Get a discount limit form campaign.
78 73 *
79 74 * Compare with total product meta and apply discount
80 75 */
81 - $discount_limit = $intent->campaign->discount_max_user;
82 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
83 -
84 - if (
85 - ! empty( $discount_limit )
86 - && (
87 - $discount_limit >= 0
88 - && $total_applied_campaign >= $discount_limit
89 - )
90 - ) {
76 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
91 77 continue;
92 78 }
93 79
94 80 $discount = $intent->get_discounts( (float) $price, $product );
@@ -96,15 +82,30 @@
96 82 if ( empty( $discount ) ) {
97 83 continue;
98 84 }
99 85
100 - // Get applied discount campaign.
101 - if ( $discount > 0 && $prev_discount > $discount ) {
102 - $this->applied_campaign_id = $intent->campaign->id;
103 - $prev_discount = $discount;
104 - }
105 -
106 - $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 + );
107 108 }
108 109
109 110 // If no discount is applied, return the original price.
110 111 if ( empty( $discounts ) ) {
@@ -110,16 +111,12 @@
110 111 if ( empty( $discounts ) ) {
111 112 return $price;
112 113 }
113 114
114 - $discount_type = $intent->campaign->discount_rules[0]['discount_type'];
115 + // Pick the winning discount amount, then map it back to the campaign that produced it.
116 + $discounted_amount = $this->min_max_average( array_values( $discounts ) );
117 + $this->applied_campaign_id = (int) array_search( $discounted_amount, $discounts, true );
115 118
116 - /**
117 - * Get the min or max discount amount according to plugin settings.
118 - * Apply the filter to modify final discounted amount based on discount types.
119 - */
120 - $discounted_amount = apply_filters( 'disco_final_discounted_amount', $this->min_max_average( $discounts ), $discount_type );
121 -
122 119 if ( $discounted_amount <= $price ) {
123 120 // Get an applied campaign from DiscountLimit class.
124 121 ( new UserLimit )->disco_start_session_on_checkout( $this->applied_campaign_id );
125 122
@@ -143,12 +140,8 @@
143 140 if ( $cart->is_empty() ) {
144 141 return $cart;
145 142 }
146 143
147 - if ( Settings::get( 'discount_priority_type' ) === 'woocommerce_coupon' ) {
148 - return $cart;
149 - }
150 -
151 144 // Init Cart - Based Intents except Product & Shipping Intent.
152 145 $this->intents = $this->prepare_intents( array( 'Cart' ) );
153 146
154 147 if ( ! empty( $this->intents ) && $cart->get_cart_contents_count() > 0 ) {
@@ -160,18 +153,9 @@
160 153 * Get a discount limit form campaign.
161 154 *
162 155 * Compare with total product meta and apply discount
163 156 */
164 - $discount_limit = $intent->campaign->discount_max_user;
165 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
166 -
167 - if (
168 - ! empty( $discount_limit )
169 - && (
170 - $discount_limit >= 0
171 - && $total_applied_campaign >= $discount_limit
172 - )
173 - ) {
157 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
174 158 continue;
175 159 }
176 160
177 161 $items = $this->get_items_for_discount( $cart, $intent->campaign );
@@ -211,8 +195,10 @@
211 195 if ( ! empty( $discount_label ) ) {
212 196 $label = $discount_label;
213 197 }
214 198
199 + $cart_fee = $this->get_discount_exclude_tax( $cart_fee, $cart );
200 +
215 201 $cart->add_fee( $label, -$cart_fee );
216 202 $cart->set_session();
217 203 }
218 204 }
@@ -235,12 +221,8 @@
235 221 if ( ! defined( 'DOING_AJAX' ) && is_admin() ) {
236 222 return $cart;
237 223 }
238 224
239 - if ( Settings::get( 'discount_priority_type' ) === 'woocommerce_coupon' ) {
240 - return $cart;
241 - }
242 -
243 225 // Init Cart - Based Intents except Product & Shipping Intent.
244 226 $this->intents = $this->prepare_intents( array( 'Bulk', 'Bundle', 'BOGO' ) );
245 227
246 228 $discounts = $this->prepare_item_discounts( $this->intents, $cart );
@@ -269,8 +251,10 @@
269 251 $total_discount += $discounts[ $id ];
270 252 }
271 253
272 254 if ( $total_discount > 0 ) {
255 + $total_discount = $this->get_discount_exclude_tax( $total_discount, $cart );
256 +
273 257 $cart->add_fee( __( 'Discount', 'disco' ), -$total_discount );
274 258 }
275 259
276 260 $cart->set_session();
@@ -286,25 +270,21 @@
286 270 * @param \WC_Cart $cart WooCommerce cart object to calculate discounts for.
287 271 * @return array|false|\WC_Cart Cart object with applied discounts or false if no discounts are applicable.
288 272 */
289 273 public function get_cart_items_discount_for_bogo( $cart ) {//phpcs:ignore
290 - if ( ! $this->cart_is_valid() ) {
291 - return $cart;
292 - }
274 + if ( ! $this->cart_is_valid() ) {
275 + return $cart;
276 + }
293 277
294 - if ( ! defined( 'DOING_AJAX' ) && is_admin() ) {
295 - return $cart;
296 - }
297 -
298 - if ( Settings::get( 'discount_priority_type' ) === 'woocommerce_coupon' ) {
299 - return $cart;
278 + if ( ! defined( 'DOING_AJAX' ) && is_admin() ) {
279 + return $cart;
300 280 }
301 281
302 - // Init Cart - Based Intents except Product & Shipping Intent.
303 - $this->intents = $this->prepare_intents( array( 'BOGO' ) );
282 + // Init Cart - Based Intents except Product & Shipping Intent.
283 + $this->intents = $this->prepare_intents( array( 'BOGO' ) );
304 284
305 285 return $this->prepare_item_discounts_bogo_free( $this->intents, $cart );
306 - }
286 + }
307 287
308 288 /**
309 289 * Apply the discount to shipping.
310 290 *
@@ -310,55 +290,56 @@
310 290 *
311 291 * @return bool
312 292 */
313 293 public function apply_free_shipping() { //phpcs:ignore
294 + return ! empty( $this->get_applied_free_shipping_campaign_ids() );
295 + }
296 +
297 + /**
298 + * Evaluate the Shipping intents and return the IDs of every campaign that
299 + * currently grants free shipping (respecting per-user usage limits).
300 + *
301 + * This is the single source of truth for "which free-shipping campaigns
302 + * apply right now". It is used both to decide whether to add a free shipping
303 + * rate and to persist the campaign IDs to the order meta at checkout. It does
304 + * NOT rely on the WC session or cached shipping rates, so it is safe to call
305 + * during order creation.
306 + *
307 + * @return array<int> Applied free-shipping campaign IDs.
308 + */
309 + public function get_applied_free_shipping_campaign_ids() {
314 310 if ( ! $this->cart_is_valid() ) {
315 - return false;
311 + return array();
316 312 }
317 313
318 - $cart = WC()->cart;
314 + $cart = WC()->cart;
315 + $this->intents = $this->prepare_intents( array( 'Shipping' ) );
316 + $campaign_ids = array();
319 317
320 - if ( Settings::get( 'discount_priority_type' ) === 'woocommerce_coupon' ) {
321 - return false; // Return false if WooCommerce coupon is used.
318 + if ( empty( $this->intents ) ) {
319 + return $campaign_ids;
322 320 }
323 321
324 - $this->intents = $this->prepare_intents( array( 'Shipping' ) );
325 - $shippings = array();
322 + foreach ( $this->intents as $intent ) {
323 + /**
324 + * Get a discount limit from campaign.
325 + *
326 + * Compare with total applied count and skip if the user limit is hit.
327 + */
328 + if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
329 + continue;
330 + }
326 331
327 - if ( ! empty( $this->intents ) ) {
328 - foreach ( $this->intents as $intent ) {
329 - /**
330 - * Get a discount limit form campaign.
331 - *
332 - * Compare with total product meta and apply discount
333 - */
334 - $discount_limit = $intent->campaign->discount_max_user;
335 - $applied_campaign_id = $intent->campaign->id;
336 - $total_applied_campaign = ( new UserLimit )->disco_get_total_applied_campaign( $intent->campaign->id );
332 + $items = $this->get_items_for_discount( $cart, $intent->campaign );
337 333
338 - if (
339 - ! empty( $discount_limit )
340 - && (
341 - $discount_limit >= 0
342 - && $total_applied_campaign >= $discount_limit
343 - )
344 - ) {
345 - continue;
346 - }
334 + if ( 'free_shipping' !== $intent->get_discounts( $items, $cart ) ) {
335 + continue;
336 + }
347 337
348 - $items = $this->get_items_for_discount( $cart, $intent->campaign );
349 - $shippings[] = $intent->get_discounts( $items, $cart );
350 -
351 - if ( !in_array( 'free_shipping', $shippings, true ) ) {
352 - continue;
353 - }
354 -
355 - // Get an applied campaign from DiscountLimit class.
356 - ( new UserLimit )->disco_start_session_on_checkout( $applied_campaign_id );
357 - }
338 + $campaign_ids[] = (int) $intent->campaign->id;
358 339 }
359 340
360 - return in_array( 'free_shipping', $shippings, true );
341 + return $campaign_ids;
361 342 }
362 343
363 344 /**
364 345 * Get offers for a product.
@@ -473,7 +454,35 @@
473 454 return false;
474 455 }
475 456
476 457 return in_array( $page_name, $pages, true );
458 + }
459 +
460 + /**
461 + * Resolve a campaign's discount type.
462 + *
463 + * Read through get_discount_rules() rather than the raw config property.
464 + * That accessor is what the intents use, so the type reported here is the
465 + * one the discount was actually calculated with: it decodes a rules payload
466 + * stored as JSON and fills in the defaults, neither of which the magic
467 + * property does. The property is also undeclared, so PHPStan cannot see it.
468 + *
469 + * @param \Disco\App\Utility\Config $campaign Campaign config.
470 + * @return string Discount type, or an empty string when it cannot be determined.
471 + */
472 + private function campaign_discount_type( Config $campaign ): string {
473 + $rules = $campaign->get_discount_rules();
474 +
475 + if ( ! is_array( $rules ) || ! isset( $rules[0] ) || ! is_object( $rules[0] ) ) {
476 + return '';
477 + }
478 +
479 + $discount_type = $rules[0]->discount_type ?? '';
480 +
481 + if ( ! is_scalar( $discount_type ) ) {
482 + return '';
483 + }
484 +
485 + return (string) $discount_type;
477 486 }
478 487
479 488 }