PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.1
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.1
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 1.3.46 All 178 releases
disco / app / Disco.php

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

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