PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / classes / coupon.php

coupon.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/classes/coupon.php

697 lines 21.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Classes;
4
5 use Exception;
6 use StoreEngine\Classes\Exceptions\StoreEngineException;
7 use StoreEngine\Classes\Order\OrderItemProduct;
8 use StoreEngine\Utils\Caching;
9 use StoreEngine\Utils\Formatting;
10 use StoreEngine\Utils\Helper;
11 use StoreEngine\Utils\NumberUtil;
12 use StoreEngine\Utils\StringUtil;
13 use StoreEngine\Utils\TaxUtil;
14 use WP_Error;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit;
18 }
19
20 class Coupon {
21
22 public int $id = 0;
23
24 public string $code = '';
25
26 public string $name = '';
27
28 public string $status = 'draft';
29
30 public string $created = '0000-00-00 00:00:00';
31
32 public string $created_gmt = '0000-00-00 00:00:00';
33
34 public ?array $settings = null;
35
36 protected array $internal_meta_keys = [
37 '_storeengine_coupon_name',
38 '_storeengine_coupon_type',
39 '_storeengine_coupon_amount',
40 '_storeengine_coupon_time_type',
41 '_storeengine_coupon_usage_limit',
42 '_storeengine_coupon_customer_usage_limit',
43 '_storeengine_coupon_type_of_min_requirement',
44 '_storeengine_coupon_min_purchase_quantity',
45 '_storeengine_coupon_min_purchase_amount',
46 '_storeengine_coupon_who_can_use',
47 '_storeengine_coupon_start_date_time',
48 '_storeengine_coupon_end_date_time',
49 '_storeengine_coupon_usage_count',
50 '_storeengine_coupon_used_by',
51 '_storeengine_coupon_valid_customers',
52 '_storeengine_coupon_valid_prices',
53 // Product / category restrictions. NOTE: these intentionally omit the
54 // `coupon_` segment so Coupon::get() maps them to the settings keys the
55 // getters below already read (e.g. `product_categories`, not
56 // `coupon_product_categories`).
57 '_storeengine_product_ids',
58 '_storeengine_excluded_product_ids',
59 '_storeengine_product_categories',
60 '_storeengine_excluded_product_categories',
61 '_storeengine_exclude_sale_items',
62 '_storeengine_email_restrictions',
63 '_storeengine_coupon_recurring_discount',
64 '_storeengine_coupon_recurring_discount_limit',
65 '_storeengine_coupon_auto_apply',
66 ];
67
68 /**
69 * Coupon meta keys loaded into `$settings`.
70 *
71 * Add-ons that add their own coupon fields must register their meta keys
72 * here — a key that is not listed never reaches `get_settings()`, so the
73 * add-on could not read its own configuration back.
74 *
75 * Keys are mapped to settings keys by stripping the `_storeengine_` prefix
76 * (see get()), so `_storeengine_coupon_foo` is read as `coupon_foo`.
77 *
78 * @return string[]
79 */
80 public function get_internal_meta_keys(): array {
81 /**
82 * Filters the coupon meta keys loaded into the coupon's settings.
83 *
84 * @param string[] $keys Meta keys.
85 * @param Coupon $coupon Coupon instance.
86 */
87 return (array) apply_filters( 'storeengine/coupon/internal_meta_keys', $this->internal_meta_keys, $this );
88 }
89
90 /**
91 * Sorting.
92 *
93 * Used by `CartTotals::get_coupons_from_cart` to sort coupons.
94 *
95 * @see CartTotals::get_coupons_from_cart()
96 *
97 * @var int
98 */
99 public int $sort = 0;
100
101 public function __construct( ?string $code = null ) {
102 if ( ! StringUtil::is_null_or_whitespace( $code ) && ! is_numeric( $code ) ) {
103 $this->code = $code;
104 } elseif ( is_numeric( $code ) && absint( $code ) > 0 ) {
105 $this->code = get_post_meta( absint( $code ), '_storeengine_coupon_name', true );
106 }
107
108 if ( $this->code ) {
109 $this->get();
110 }
111 }
112
113 public function get_id(): int {
114 return $this->id;
115 }
116
117 public function get_code(): string {
118 return $this->code;
119 }
120
121 public function get_name(): string {
122 return $this->name;
123 }
124
125 public function get_status(): string {
126 return $this->status;
127 }
128
129 public function get_created(): string {
130 return $this->created;
131 }
132
133 public function get_created_gmt(): string {
134 return $this->created_gmt;
135 }
136
137 public function get_settings(): ?array {
138 return $this->settings;
139 }
140
141 /**
142 * Get coupon amount.
143 *
144 * @return string
145 */
146 public function get_amount(): string {
147 return Formatting::format_decimal( $this->settings['coupon_amount'] ?? 0 );
148 }
149
150 public function get_discount_type(): string {
151 return $this->settings['coupon_type'] ?? '';
152 }
153
154 public function get_maximum_amount(): float {
155 return abs( floatval( $this->settings['maximum_amount'] ?? 0 ) );
156 }
157
158 public function get_minimum_amount(): float {
159 return abs( floatval( $this->settings['minimum_amount'] ?? 0 ) );
160 }
161
162 public function get_product_ids(): array {
163 return $this->settings['product_ids'] ?? [];
164 }
165
166 public function get_excluded_product_ids(): array {
167 return $this->settings['excluded_product_ids'] ?? [];
168 }
169
170 public function get_product_categories(): array {
171 return $this->settings['product_categories'] ?? [];
172 }
173
174 public function get_excluded_product_categories(): array {
175 if ( ! empty( $this->settings['excluded_product_categories'] ) && is_array( $this->settings['excluded_product_categories'] ) ) {
176 return $this->settings['excluded_product_categories'];
177 }
178
179 return [];
180 }
181
182 public function get_exclude_sale_items(): bool {
183 return ! empty( $this->settings['exclude_sale_items'] );
184 }
185
186 /**
187 * Whether this coupon's discount should keep applying to subscription renewals.
188 */
189 public function get_recurring_discount(): bool {
190 return ! empty( $this->settings['coupon_recurring_discount'] );
191 }
192
193 /**
194 * Number of renewals to keep the recurring discount for. 0 = forever.
195 */
196 public function get_recurring_discount_limit(): int {
197 return absint( $this->settings['coupon_recurring_discount_limit'] ?? 0 );
198 }
199
200 /**
201 * Whether this coupon should be applied to the cart automatically.
202 */
203 public function get_auto_apply(): bool {
204 return ! empty( $this->settings['coupon_auto_apply'] );
205 }
206
207 public function get_email_restrictions() {
208 if ( ! empty( $this->settings['email_restrictions'] ) && is_array( $this->settings['email_restrictions'] ) ) {
209 return $this->settings['email_restrictions'];
210 }
211
212 return false;
213 }
214
215 public function get_valid_customer_ids() {
216 return $this->settings['coupon_valid_customers'] ?? [];
217 }
218
219 public function get_valid_price_data() {
220 return $this->settings['coupon_valid_prices'] ?? [];
221 }
222
223 /**
224 * @param string|array $type
225 *
226 * @return bool
227 */
228 public function is_type( $type ): bool {
229 return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type, true ) ) );
230 }
231
232 /**
233 * @throws Exception
234 * @throws StoreEngineException
235 */
236 public function get_date_expires() {
237 if ( ! $this->settings ) {
238 return false;
239 }
240 if ( 'forever_time' === $this->settings['coupon_time_type'] ) {
241 // Early bail, no time limit on this coupon.
242 return false;
243 }
244
245 if ( empty( $this->settings['coupon_end_date_time'] ) || empty( $this->settings['coupon_end_date_time']['date'] ) ) {
246 // End datetime not saved.
247 return false;
248 }
249
250 [
251 'date' => $end_date,
252 'time' => $end_time,
253 ] = $this->settings['coupon_end_date_time'];
254
255 if ( ! $end_date ) {
256 // End datetime not saved.
257 return false;
258 }
259
260 $end_datetime = trim( $end_date . ' ' . $end_time );
261
262 if ( ! $end_datetime ) {
263 throw new StoreEngineException( esc_html__( 'We’re sorry, but the coupon code you entered isn’t valid or already expired.', 'storeengine' ), 'invalid-end-datetime', null, 400 );
264 }
265
266 return new StoreengineDatetime( $end_datetime );
267 }
268
269 public function validate_coupon( $check_total_usage = true, $check_min_requirement = true ) {
270 try {
271 $this->is_valid();
272
273 $this->check_customers_requirement();
274 if ( $check_total_usage ) {
275 $this->check_usage_limit();
276 $this->check_customer_usage_limit();
277 }
278
279 $this->check_coupon_between_start_and_end_date();
280 // The minimum-spend/quantity requirement reads the live cart, so a
281 // caller validating outside a cart context (e.g. the dashboard
282 // "available coupons" notice, which surfaces the threshold itself)
283 // can skip it to avoid a false "invalid" on an empty cart.
284 if ( $check_min_requirement ) {
285 $this->check_coupon_minimum_requirement();
286 }
287 $this->check_user_can_usage();
288 do_action( 'storeengine/validate_coupon', $this );
289 } catch ( StoreEngineException $e ) {
290 // Expected validation outcomes (coupon not found / inactive / expired /
291 // usage limit / not applicable) are surfaced to the customer as a
292 // WP_Error. They are not system errors, so don't pollute the error log.
293 return $e->toWpError();
294 } catch ( \Throwable $e ) {
295 // Catch \Throwable (not just Exception) so a PHP Error raised deep in a
296 // validation hook — e.g. a method call on a not-yet-ready cart — degrades
297 // to "coupon not applied" instead of a site-wide fatal. Unexpected, so log.
298 Helper::log_error( $e );
299
300 return new WP_Error( 'coupon_validation_error', $e->getMessage() );
301 }
302
303 return true;
304 }
305
306 public function calculate( $amount ) {
307 if ( ! empty( $this->settings['coupon_type'] ) ) {
308 if ( 'percentage' === $this->settings['coupon_type'] ) {
309 return $amount * ( $this->settings['coupon_amount'] / 100 );
310 }
311
312 if ( 'fixedAmount' === $this->settings['coupon_type'] ) {
313 return $this->settings['coupon_amount'];
314 }
315 }
316
317 return 0;
318 }
319
320 private function get() {
321 if ( ! $this->code ) {
322 return;
323 }
324 $coupons_query = get_posts( [
325 'posts_per_page' => 1,
326 'post_type' => Helper::COUPON_POST_TYPE,
327 'post_status' => 'publish',
328 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
329 [
330 'key' => '_storeengine_coupon_name',
331 'value' => $this->code,
332 'compare' => '=',
333 ],
334 ],
335 ] );
336
337 // Check if the coupon is active
338 if ( empty( $coupons_query ) ) {
339 return;
340 }
341
342 $this->id = $coupons_query[0]->ID;
343 $this->name = $coupons_query[0]->post_title;
344 $this->status = $coupons_query[0]->post_status;
345 $this->created = $coupons_query[0]->post_date;
346 $this->created_gmt = $coupons_query[0]->post_date_gmt;
347 $this->settings = [];
348
349 $internal_meta_keys = $this->get_internal_meta_keys();
350
351 foreach ( get_post_meta( $coupons_query[0]->ID ) as $key => $value ) {
352 if ( ! in_array( $key, $internal_meta_keys, true ) ) {
353 continue;
354 }
355
356 if ( '_storeengine_coupon_used_by' === $key ) {
357 $this->settings['coupon_used_by'] = array_map( 'absint', $value );
358 continue;
359 }
360
361 if ( 1 === count( $value ) ) {
362 $value = maybe_unserialize( $value[0] );
363 }
364
365 $this->settings[ str_replace( '_' . Helper::DB_PREFIX, '', $key ) ] = $value;
366 }
367 }
368
369 public static function get_by_code( string $code, $exclude = 0 ) {
370 if ( StringUtil::is_null_or_whitespace( $code ) ) {
371 return null;
372 }
373
374 // @TODO purge cache on post update.
375 $ids = wp_cache_get( Caching::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, 'coupons' );
376
377 if ( false === $ids ) {
378 global $wpdb;
379
380 // Sort by date for latest coupon if there is more than one with same code.
381 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
382 $ids = $wpdb->get_col(
383 $wpdb->prepare(
384 "
385 SELECT p.ID
386 FROM wp_posts p
387 RIGHT JOIN wp_postmeta m ON m.post_id = p.ID
388 WHERE
389 post_type = %s AND
390 m.meta_key = '_storeengine_coupon_name' AND
391 m.meta_value = %s
392 ORDER BY post_date DESC;
393 ",
394 Helper::COUPON_POST_TYPE,
395 Formatting::sanitize_coupon_code( $code )
396 )
397 );
398
399 if ( $ids ) {
400 wp_cache_set( Caching::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, $ids, 'coupons' );
401 }
402 }
403
404 $exclude = absint( $exclude );
405 $ids = array_diff( array_filter( array_map( 'absint', (array) $ids ) ), [ $exclude ] );
406
407 return apply_filters( 'storeengine/get_coupon_id_from_code', absint( current( $ids ) ), $code, $exclude );
408 }
409
410 public function get_used_by(): array {
411 return $this->settings['coupon_used_by'] ?? [];
412 }
413
414 public function get_usage_by_user_id( int $user_id ): int {
415 return count( array_filter( $this->get_used_by(), fn( $used_by ) => $used_by === $user_id ) );
416 }
417
418 private function is_valid() {
419 if ( ! $this->id ) {
420 throw new StoreEngineException( esc_html__( 'Coupon does not exist!', 'storeengine' ), 'coupon-not-found', null, 404 );
421 }
422
423 if ( 'publish' !== $this->status ) {
424 throw new StoreEngineException( esc_html__( 'Coupon does not exist!', 'storeengine' ), 'coupon-is-not-active' );
425 }
426
427 if ( ! $this->settings ) {
428 throw new StoreEngineException( esc_html__( 'Invalid Coupon!', 'storeengine' ), 'invalid-coupon', null, 400 );
429 }
430 }
431
432 /**
433 * @return int|null
434 */
435 public function get_limit_usage_to_x_items(): ?int {
436 // @TODO implement limit_usage_to_x_items
437 return null;
438 }
439
440 /**
441 * Check if the coupon can be used with specified product.
442 *
443 * @param int $product_id
444 * @param int $price_id
445 * @param CartItem|OrderItemProduct $object
446 *
447 * @return bool
448 */
449 public function is_valid_for_product( int $product_id, int $price_id, $object ): bool {
450 // @TODO check coupon type once shipping coupon is implemented.
451 $is_valid = true;
452 $product_ids = $this->get_valid_price_data();
453
454 if ( $product_ids ) {
455 $is_valid = false;
456 foreach ( $product_ids as [ 'product_id' => $product, 'price_ids' => $price_ids ] ) {
457 if ( $product_id === $product ) {
458 $is_valid = empty( $price_ids ) || ! is_array( $price_ids ) || in_array( $price_id, $price_ids, true );
459 break;
460 }
461 }
462 }
463
464 return apply_filters( 'storeengine/coupon/is_valid_for_product', $is_valid, $product_id, $price_id, $object );
465 }
466
467 public function is_valid_for_cart(): bool {
468 // @TODO implement based on coupon type & restriction.
469 // Will be useful for implementing coupon for recurring (subscription) cart.
470 return apply_filters( 'storeengine/coupon/is_valid_for_cart', true );
471 }
472
473 /**
474 * @throws StoreEngineException
475 */
476 private function check_customers_requirement() {
477 if ( 'specificCustomer' !== $this->settings['coupon_who_can_use'] ) {
478 return;
479 }
480
481 if ( ! is_user_logged_in() ) {
482 throw new StoreEngineException( esc_html__( 'Sorry, You can not use this coupon.', 'storeengine' ), 'coupon-user-cannot-use', null, 400 );
483 }
484
485 if ( ! in_array( get_current_user_id(), $this->get_valid_customer_ids(), true ) ) {
486 throw new StoreEngineException( esc_html__( 'Sorry, You can not use this coupon.', 'storeengine' ), 'coupon-user-cannot-use', null, 400 );
487 }
488 }
489
490 private function check_usage_limit() {
491 if ( 0 >= $this->get_usage_limit() ) {
492 return;
493 }
494
495 if ( $this->get_usage_count() >= $this->get_usage_limit() ) {
496 throw new StoreEngineException(
497 sprintf(
498 // translators: %s: Coupon Code.
499 esc_html__( 'Sorry, Coupon "%s" has reached its limit.', 'storeengine' ),
500 esc_html( $this->name )
501 ),
502 'coupon-limit-reached',
503 null,
504 400
505 );
506 }
507 }
508
509 /**
510 * @throws StoreEngineException
511 */
512 private function check_customer_usage_limit() {
513 if ( 0 < $this->get_usage_limit_per_user() && is_user_logged_in() ) {
514 if ( $this->get_usage_by_user_id( get_current_user_id() ) >= $this->get_usage_limit_per_user() ) {
515 throw new StoreEngineException(
516 sprintf(
517 // translators: %s: Coupon Code.
518 esc_html__( 'Sorry, Coupon "%s" has reached its limit', 'storeengine' ),
519 esc_html( $this->name )
520 ),
521 'coupon-limit-reached',
522 null,
523 400
524 );
525 }
526 }
527 }
528
529 /**
530 * @throws StoreEngineException
531 */
532 private function check_coupon_minimum_requirement() {
533 if ( 'none' !== $this->settings['coupon_type_of_min_requirement'] ) {
534 $cart = Helper::cart();
535 $cart_items = $cart->get_cart_items();
536 $cart_items_count = count( $cart_items );
537 $minimum_purchase_quantity = $this->settings['coupon_min_purchase_quantity'];
538 if ( 'quantity' === $this->settings['coupon_type_of_min_requirement'] && $cart_items_count < $minimum_purchase_quantity ) {
539 throw new StoreEngineException( esc_html(
540 sprintf(
541 // translators: %d: minimum purchase quantity.
542 __( 'Sorry, Coupon has minimum purchase quantity of %d', 'storeengine' ),
543 $minimum_purchase_quantity
544 )
545 ), 'min-purchase-qty', null, 400 );
546 }
547
548 if ( 'amount' === $this->settings['coupon_type_of_min_requirement'] ) {
549 $cart_total = $cart->get_total( 'coupon' );
550 $minimum_purchase_amount = $this->settings['coupon_min_requirement_amount'] ?? 0;
551 if ( $cart_total < $minimum_purchase_amount ) {
552 throw new StoreEngineException( esc_html(
553 sprintf(
554 // translators: %s: minimum purchase amount.
555 __( 'Sorry, Coupon has minimum purchase amount of %s', 'storeengine' ),
556 Formatting::price( $minimum_purchase_amount )
557 )
558 ), 'min-purchase-amount', null, 400 );
559 }
560 }
561 }
562 }
563
564 private function check_coupon_between_start_and_end_date() {
565 if ( 'forever_time' === $this->settings['coupon_time_type'] ) {
566 // Early bail, no time limit on this coupon.
567 return;
568 }
569
570 [
571 'date' => $start_date,
572 'time' => $start_time,
573 ] = $this->settings['coupon_start_date_time'];
574
575 if ( ! $start_date || ! $start_time ) {
576 throw new StoreEngineException( esc_html__( 'We’re sorry, but the coupon code you entered isn’t valid.', 'storeengine' ), 'empty-start-datetime', null, 500 );
577 }
578
579 $start_datetime = strtotime( trim( $start_date . ' ' . $start_time ) );
580
581 if ( ! $start_datetime ) {
582 throw new StoreEngineException( esc_html__( 'We’re sorry, but the coupon code you entered isn’t valid.', 'storeengine' ), 'invalid-start-datetime', null, 500 );
583 }
584
585 if ( $start_datetime > time() ) {
586 // @TODO update the error message and datetime format.
587 throw new StoreEngineException(
588 sprintf(
589 /* translators: %s. Coupon start date. */
590 esc_html__( 'We’re sorry, but the coupon code you entered will be valid starting %s. Please try again later.', 'storeengine' ),
591 esc_html( wp_date( 'Y-m-d H:i:s', $start_datetime ) )
592 ),
593 'coupon-unavailable',
594 null,
595 400
596 );
597 }
598
599 if ( empty( $this->settings['coupon_end_date_time'] ) ) {
600 // End datetime not saved.
601 return;
602 }
603
604 [
605 'date' => $end_date,
606 'time' => $end_time,
607 ] = $this->settings['coupon_end_date_time'];
608
609 if ( ! $end_date ) {
610 // End datetime not saved.
611 return;
612 }
613
614 $end_datetime = strtotime( trim( $end_date . ' ' . $end_time ) );
615
616 if ( ! $end_datetime ) {
617 throw new StoreEngineException( esc_html__( 'We’re sorry, but the coupon code you entered isn’t valid or already expired.', 'storeengine' ), 'invalid-end-datetime', null, 400 );
618 }
619
620 if ( $end_datetime < time() ) {
621 throw new StoreEngineException( esc_html__( 'We’re sorry, but the coupon code you entered is expired.', 'storeengine' ), 'coupon-expired', null, 400 );
622 }
623 }
624
625 private function check_user_can_usage() {
626 $coupon_who_can_use = $this->settings['coupon_who_can_use'];
627 if ( ( 'newCustomer' === $coupon_who_can_use && get_current_user_id() ) || ( 'currentCustomer' === $coupon_who_can_use && ! get_current_user_id() ) ) {
628 throw new StoreEngineException( esc_html__( 'Sorry, You can not use this coupon.', 'storeengine' ), 'coupon-user-cannot-use', null, 400 );
629 }
630 }
631
632 public function get_usage_count(): int {
633 return absint( $this->settings['coupon_usage_count'] ?? 0 );
634 }
635
636 public function get_usage_limit(): int {
637 return absint( $this->settings['coupon_usage_limit'] ?? 0 );
638 }
639
640 public function get_usage_limit_per_user(): int {
641 return absint( $this->settings['coupon_customer_usage_limit'] ?? 0 );
642 }
643
644 /**
645 * Get discount amount for a cart item.
646 *
647 * @param float|int $discounting_amount Amount the coupon is being applied to.
648 * @param array|null $cart_item Cart item being discounted if applicable.
649 * @param boolean $single True if discounting a single qty item, false if its the line.
650 *
651 * @return float Amount this coupon has discounted.
652 */
653 public function get_discount_amount( $discounting_amount, $cart_item = null, bool $single = false ): float {
654 $discount = 0;
655 $cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity'];
656
657 if ( $this->is_type( [ 'percent', 'percentage' ] ) ) {
658 $discount = (float) $this->get_amount() * ( $discounting_amount / 100 );
659 } elseif ( $this->is_type( [
660 'fixed_cart',
661 'fixedAmount'
662 ] ) && ! is_null( $cart_item ) && Helper::cart()->get_subtotal() ) {
663 /**
664 * This is the most complex discount - we need to divide the discount between rows based on their price in.
665 * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows.
666 * with no price (free) don't get discounted.
667 *
668 * Get item discount by dividing item cost by subtotal to get a %.
669 *
670 * Uses price inc tax if prices include tax to work around known tax-rounding edge cases.
671 */
672 if ( TaxUtil::prices_include_tax() ) {
673 $discount_percent = ( Formatting::get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal;
674 } else {
675 $discount_percent = ( Formatting::get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax;
676 }
677 $discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty;
678 } elseif ( $this->is_type( 'fixed_product' ) ) {
679 $discount = min( $this->get_amount(), $discounting_amount );
680 $discount = $single ? $discount : $discount * $cart_item_qty;
681 }
682
683 return (float) apply_filters(
684 'storeengine/coupon_get_discount_amount',
685 NumberUtil::round( min( $discount, $discounting_amount ), Formatting::get_rounding_precision() ),
686 $discounting_amount,
687 $cart_item,
688 $single,
689 $this
690 );
691 }
692
693 public function get_free_shipping(): bool {
694 return false;
695 }
696 }
697