PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.16
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.16
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 1.3.47 All 179 releases
disco / app / Disco.php

Disco.php in Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO 1.4.16, at app/Disco.php

489 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Disco Class File.
4 *
5 * @package Disco
6 * @subpackage \App\Disco.php
7 * @since 1.0.0
8 */
9
10 namespace Disco\App;
11
12 use Disco\App\Calc\CalcFactory;
13 use Disco\App\Features\UserLimit;
14 use Disco\App\Utility\Config;
15 use Disco\App\Utility\Settings;
16
17 /**
18 * Class Disco
19 *
20 * @package Disco
21 * @subpackage \App
22 * @author Ohidul Islam <wahid0003@gmail.com>
23 * @link https://webappick.com
24 * @license https://opensource.org/licenses/gpl-license.php GNU Public License
25 * @category Discount
26 */
27 class Disco {
28
29 use \Disco\App\Intents\IntentHelper;
30
31 /**
32 * @var array $intents Intents.
33 */
34 private $intents;
35
36 /**
37 * @var int $applied_campaign_id Applied Campaign.
38 */
39 private $applied_campaign_id;
40
41
42 /**
43 * Apply discount to product.
44 *
45 * @param float $price Product Price.
46 * @param \WC_Product $product Product Object.
47 * @return float Product Price.
48 * @throws \Exception If setting is not found.
49 */
50 public function get_product_discounted_price( $price, $product ) {//phpcs:ignore
51 if ( $product instanceof \WC_Product ) {
52 // Init product base intents and exclude cart-based intents.
53 $this->intents = $this->prepare_intents( array( 'Product' ) );
54
55 if ( ! $product->is_type( 'variable' ) ) {
56 $price = CalcFactory::get_product_price( $product );
57 }
58
59 // If the product is not on sale, update the meta to indicate it's not on sale.
60 // update_post_meta( $product->get_id(), '_disco_on_sale', 'no' );
61
62 // If no intent is available, return the original price.
63 if ( empty( $this->intents ) ) {
64 return $price;
65 }
66
67 $discounts = array();
68
69 // Foreach intent, apply the discount.
70 foreach ( $this->intents as $intent ) {
71 /**
72 * Get a discount limit form campaign.
73 *
74 * Compare with total product meta and apply discount
75 */
76 if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
77 continue;
78 }
79
80 $discount = $intent->get_discounts( (float) $price, $product );
81
82 if ( empty( $discount ) ) {
83 continue;
84 }
85
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 );
108 }
109
110 // If no discount is applied, return the original price.
111 if ( empty( $discounts ) ) {
112 return $price;
113 }
114
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 );
118
119 if ( $discounted_amount <= $price ) {
120 // Get an applied campaign from DiscountLimit class.
121 ( new UserLimit )->disco_start_session_on_checkout( $this->applied_campaign_id );
122
123 // Mark as disco custom on sale
124 // update_post_meta( $product->get_id(), '_disco_on_sale', 'yes' );
125
126 return $this->discounted_price( $price, $discounted_amount );
127 }
128 }
129
130 return $price;
131 }
132
133 /** Get Cart Discount.
134 *
135 * @param \WC_Cart $cart Cart Object.
136 * @return \WC_Cart Cart Object.
137 * @throws \Exception If the cart is empty.
138 */
139 public function get_cart_discount( $cart ) { //phpcs:ignore
140 if ( $cart->is_empty() ) {
141 return $cart;
142 }
143
144 // Init Cart - Based Intents except Product & Shipping Intent.
145 $this->intents = $this->prepare_intents( array( 'Cart' ) );
146
147 if ( ! empty( $this->intents ) && $cart->get_cart_contents_count() > 0 ) {
148 $discounts = array();
149
150 foreach ( $this->intents as $intent ) { // Foreach intent, apply the discount.
151
152 /**
153 * Get a discount limit form campaign.
154 *
155 * Compare with total product meta and apply discount
156 */
157 if ( ( new UserLimit )->disco_is_limit_reached( $intent->campaign ) ) {
158 continue;
159 }
160
161 $items = $this->get_items_for_discount( $cart, $intent->campaign );
162
163 $discount = $intent->get_discounts( $items, $cart );
164
165 // Get intent id for discount limit.
166 $intent_id = $intent->campaign->id;
167
168 if ( empty( $discount ) ) {
169 continue;
170 }
171
172 // Store discount with intent id key value.
173 $discounts[ $intent_id ] = $discount;
174 }
175
176 // Apply discount to cart subtotal .
177 if ( ! empty( $discounts ) ) {
178 /**
179 * New code.
180 * Get the min or max discount amount according to plugin settings.
181 */
182 $cart_fee = $this->min_max_average( array_values( $discounts ) );
183
184 // Get applied campaign from discount array.
185 $applied_campaign_id = array_search( $cart_fee, $discounts, true );
186 $applied_campaign = ( new Campaign )->get_campaign( $applied_campaign_id );
187 $discount_label = $applied_campaign->discount_rules[0]['discount_label'] ?? '';
188
189 // Get an applied campaign from DiscountLimit class.
190 ( new UserLimit )->disco_start_session_on_checkout( (int) $applied_campaign_id );
191
192 // TODO: The Discount must be less than cart subtotal.
193 $label = __( 'Discount', 'disco' );
194
195 if ( ! empty( $discount_label ) ) {
196 $label = $discount_label;
197 }
198
199 $cart_fee = $this->get_discount_exclude_tax( $cart_fee, $cart );
200
201 $cart->add_fee( $label, -$cart_fee );
202 $cart->set_session();
203 }
204 }
205
206 return $cart;
207 }
208
209 /**
210 * Apply Bulk & Bundle discount to cart items.
211 *
212 * @param \WC_Cart $cart Cart Object.
213 * @return \WC_Cart|void Cart Object.
214 * @throws \Exception If translation not found.
215 */
216 public function get_cart_items_discount( $cart ) { // phpcs:ignore
217 if ( ! $this->cart_is_valid() ) {
218 return $cart;
219 }
220
221 if ( ! defined( 'DOING_AJAX' ) && is_admin() ) {
222 return $cart;
223 }
224
225 // Init Cart - Based Intents except Product & Shipping Intent.
226 $this->intents = $this->prepare_intents( array( 'Bulk', 'Bundle', 'BOGO' ) );
227
228 $discounts = $this->prepare_item_discounts( $this->intents, $cart );
229
230 if ( empty( $discounts ) ) {
231 return $cart;
232 }
233
234 $total_discount = 0;
235 // Identify valid BOGO + category item ID (if any)
236 $found_bogo_category_item_id = $this->get_valid_bogo_category_item_id( $this->intents, $cart, $discounts );
237
238 // Apply only the valid BOGO category discount, or all if none matched
239 foreach ( $cart->get_cart() as $item ) {
240 $id = $this->get_cart_item_id( $item );
241
242 // Skip all others if BOGO + category item is found
243 if ( $found_bogo_category_item_id !== null && $id !== $found_bogo_category_item_id ) {
244 continue;
245 }
246
247 if ( empty( $discounts[ $id ] ) ) {
248 continue;
249 }
250
251 $total_discount += $discounts[ $id ];
252 }
253
254 if ( $total_discount > 0 ) {
255 $total_discount = $this->get_discount_exclude_tax( $total_discount, $cart );
256
257 $cart->add_fee( __( 'Discount', 'disco' ), -$total_discount );
258 }
259
260 $cart->set_session();
261
262 return $cart;
263 }
264
265 /**
266 * Get cart items discount for BOGO free.
267 *
268 * Calculates which items in the cart are eligible for a free product under the BOGO offer.
269 *
270 * @param \WC_Cart $cart WooCommerce cart object to calculate discounts for.
271 * @return array|false|\WC_Cart Cart object with applied discounts or false if no discounts are applicable.
272 */
273 public function get_cart_items_discount_for_bogo( $cart ) {//phpcs:ignore
274 if ( ! $this->cart_is_valid() ) {
275 return $cart;
276 }
277
278 if ( ! defined( 'DOING_AJAX' ) && is_admin() ) {
279 return $cart;
280 }
281
282 // Init Cart - Based Intents except Product & Shipping Intent.
283 $this->intents = $this->prepare_intents( array( 'BOGO' ) );
284
285 return $this->prepare_item_discounts_bogo_free( $this->intents, $cart );
286 }
287
288 /**
289 * Apply the discount to shipping.
290 *
291 * @return bool
292 */
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() {
310 if ( ! $this->cart_is_valid() ) {
311 return array();
312 }
313
314 $cart = WC()->cart;
315 $this->intents = $this->prepare_intents( array( 'Shipping' ) );
316 $campaign_ids = array();
317
318 if ( empty( $this->intents ) ) {
319 return $campaign_ids;
320 }
321
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 }
331
332 $items = $this->get_items_for_discount( $cart, $intent->campaign );
333
334 if ( 'free_shipping' !== $intent->get_discounts( $items, $cart ) ) {
335 continue;
336 }
337
338 $campaign_ids[] = (int) $intent->campaign->id;
339 }
340
341 return $campaign_ids;
342 }
343
344 /**
345 * Get offers for a product.
346 *
347 * @param array|null $items item ids.
348 * @return string
349 */
350 public function get_offers( $items = null ) {
351 // Init Cart-Based Intents except Product & Shipping Intent.
352 $this->intents = $this->prepare_intents( array( 'Product', 'Shipping' ) );
353 $table = "<table style='border-collapse: collapse;' class=''>";
354 $table .= '<tr><th>Quantity</th><th>Discount</th></tr>';
355
356 $cart = WC()->cart;
357
358 foreach ( $this->intents as $intent ) {
359 if ( is_null( $items ) || ! $cart->is_empty() ) {
360 $items = $this->get_items_for_discount( $cart, $intent->campaign );
361 } else {
362 $items = array();
363 }
364
365 $discounts = $intent->get_offers( $items, $cart );
366
367 if ( empty( $discounts ) ) {
368 continue;
369 }
370
371 foreach ( $discounts as $discount ) {
372 $table .= '<tr style="font-size: smaller;line-height:15px"><td>' . $discount['condition'] . '</td><td>' . $discount['discount'] . '</td></tr>';
373 }
374 }
375
376 return $table . '</table>';
377 }
378
379 /**
380 * Get Premium Status.
381 */
382 public static function is_pro(): bool {
383 /**
384 * Here use class_exists to check whether Disco Pro is active or not.
385 * is_plugin_active() function may not work in some cases.
386 */
387 return class_exists( 'Disco_Pro' );
388 }
389
390 /**
391 * Get shop, category, or checkout page name.
392 *
393 * @return string Page name: 'category', 'shop', 'checkout', or product page if none match.
394 */
395 public static function get_page_name(): string {
396 if ( is_product_category() || is_product_tag() ) {
397 return 'category';
398 }
399
400 if ( is_shop() ) {
401 return 'shop';
402 }
403
404 if ( is_page( 'cart' ) || is_page( 'checkout' ) ) {
405 return 'cart';
406 }
407
408 return 'product';
409 }
410
411 /**
412 * Get pages name where strike through price should be shown.
413 *
414 * @return array Pages where strike through price should be shown.
415 */
416 public static function get_strike_through_pages(): array {
417 $settings = Settings::get( 'show_strike_through' );
418
419 if ( ! is_array( $settings ) ) {
420 return array();
421 }
422
423 $pages = array();
424
425 if ( isset( $settings['shop_page'] ) && $settings['shop_page'] ) {
426 $pages[] = 'shop';
427 }
428
429 if ( isset( $settings['product_pages'] ) && $settings['product_pages'] ) {
430 $pages[] = 'product';
431 }
432
433 if ( isset( $settings['category_page'] ) && $settings['category_page'] ) {
434 $pages[] = 'category';
435 }
436
437 if ( isset( $settings['cart_page'] ) && $settings['cart_page'] ) {
438 $pages[] = 'cart';
439 }
440
441 return $pages;
442 }
443
444 /**
445 * Get is strike through applicable for current page.
446 *
447 * @param string $page_name Page name to check.
448 * @return bool True if strike through is applicable, false otherwise.
449 */
450 public static function is_strike_through_applicable( $page_name ): bool {
451 $pages = self::get_strike_through_pages();
452
453 if ( empty( $pages ) || empty( $page_name ) ) {
454 return false;
455 }
456
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;
486 }
487
488 }
489