PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.14
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.14
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 1.3.45 All 177 releases
disco / app / Disco.php

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

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