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 / frontend / coupon.php

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

225 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Frontend;
4
5 use StoreEngine\classes\Exceptions\StoreEngineException;
6 use StoreEngine\Classes\Coupon as CouponObject;
7 use StoreEngine\Utils\Formatting;
8 use StoreEngine\Utils\Helper;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 class Coupon {
15
16 public static function init() {
17 add_action( 'storeengine/validate_coupon', [ __CLASS__, 'check_cart_minimum_requirement' ] );
18 add_action( 'storeengine/validate_coupon', [ __CLASS__, 'check_prices_requirement' ] );
19 // Coupons passed via URL (?coupon=CODE / ?storeengine_coupon=CODE).
20 add_action( 'template_redirect', [ __CLASS__, 'apply_url_coupon' ] );
21 // Coupons flagged "auto apply" are added to every eligible cart.
22 add_action( 'storeengine/cart/before_calculate_totals', [ __CLASS__, 'apply_auto_coupons' ] );
23 }
24
25 /**
26 * Apply a coupon code supplied via the URL to the current cart.
27 */
28 public static function apply_url_coupon(): void {
29 if ( is_admin() ) {
30 return;
31 }
32
33 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- shared/email link, read-only intent.
34 $code = sanitize_text_field( wp_unslash( $_GET['storeengine_coupon'] ?? $_GET['coupon'] ?? '' ) );
35 // phpcs:enable WordPress.Security.NonceVerification.Recommended
36
37 if ( '' === $code ) {
38 return;
39 }
40
41 $cart = Helper::cart();
42 if ( ! $cart || $cart->is_coupon_applied( $code ) ) {
43 return;
44 }
45
46 $applied = $cart->apply_coupon( $code );
47 if ( ! is_wp_error( $applied ) ) {
48 $cart->store_on_database();
49 }
50 }
51
52 /**
53 * Add every valid auto-apply coupon to the cart before totals are computed.
54 *
55 * @param \StoreEngine\Classes\Cart $cart
56 */
57 public static function apply_auto_coupons( $cart ): void {
58 // Auto-apply is a Pro ("Advanced Coupons") feature.
59 if ( ! apply_filters( 'storeengine/advanced_coupon/enabled', false ) ) {
60 return;
61 }
62
63 // Defensive: this fires from calculate_cart_totals(), so a cart is expected,
64 // but never fatal if it is missing/invalid.
65 if ( ! $cart instanceof \StoreEngine\Classes\Cart ) {
66 return;
67 }
68
69 try {
70 foreach ( self::get_auto_apply_codes() as $code ) {
71 if ( $cart->is_coupon_applied( $code ) ) {
72 continue;
73 }
74
75 $coupon = new CouponObject( $code );
76 if ( ! $coupon->get_id() || ! $coupon->get_auto_apply() ) {
77 continue;
78 }
79
80 // apply_coupon() validates; invalid coupons are silently skipped.
81 $cart->apply_coupon( $code );
82 }
83 } catch ( \Throwable $e ) {
84 // Auto-apply must never take the site down. If something unexpected
85 // blows up here, log it and self-heal by clearing this user's cart data
86 // so subsequent requests start clean instead of re-triggering the error.
87 Helper::log_error( $e );
88
89 if ( method_exists( $cart, 'clear_cart' ) ) {
90 $cart->clear_cart();
91 if ( method_exists( $cart, 'store_on_database' ) ) {
92 $cart->store_on_database();
93 }
94 }
95 }
96 }
97
98 /**
99 * Codes of all published coupons flagged for auto-apply.
100 *
101 * @return string[]
102 */
103 protected static function get_auto_apply_codes(): array {
104 $codes = wp_cache_get( 'storeengine_auto_apply_coupon_codes', 'coupons' );
105 if ( is_array( $codes ) ) {
106 return $codes;
107 }
108
109 $ids = get_posts( [
110 'post_type' => Helper::COUPON_POST_TYPE,
111 'post_status' => 'publish',
112 'posts_per_page' => 100,
113 'fields' => 'ids',
114 'no_found_rows' => true,
115 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
116 [
117 'key' => '_storeengine_coupon_auto_apply',
118 'value' => '1',
119 ],
120 ],
121 ] );
122
123 $codes = array_values( array_filter( array_map( static function ( $id ) {
124 return get_post_meta( $id, '_storeengine_coupon_name', true );
125 }, $ids ) ) );
126
127 wp_cache_set( 'storeengine_auto_apply_coupon_codes', $codes, 'coupons' );
128
129 return $codes;
130 }
131
132 /**
133 * @throws StoreEngineException
134 */
135 public static function check_cart_minimum_requirement( \StoreEngine\classes\Coupon $coupon ) {
136 if ( 'none' !== $coupon->settings['coupon_type_of_min_requirement'] ) {
137 $cart = Helper::cart();
138 if ( ! $cart ) {
139 // No cart to validate against (should not happen once the cart is
140 // ready). Skip rather than fatal on a null cart.
141 return;
142 }
143 $cart_subtotal = (float) $cart->get_subtotal();
144
145 self::check_minimum_requirements( $coupon, $cart->get_count(), $cart_subtotal );
146 }
147 }
148
149
150 /**
151 * @param \StoreEngine\Classes\Coupon $coupon
152 * @param $total_quantity
153 * @param $subtotal
154 *
155 * @return void
156 * @throws StoreEngineException
157 */
158 public static function check_minimum_requirements( \StoreEngine\Classes\Coupon $coupon, $total_quantity, $subtotal ) {
159 $minimum_purchase_quantity = $coupon->settings['coupon_min_purchase_quantity'];
160 if ( 'quantity' === $coupon->settings['coupon_type_of_min_requirement'] && $total_quantity < $minimum_purchase_quantity ) {
161 throw new StoreEngineException( esc_html(
162 sprintf(
163 // translators: %d: minimum purchase quantity.
164 __( 'Sorry, Coupon has minimum purchase quantity of %d', 'storeengine' ),
165 $minimum_purchase_quantity
166 )
167 ), 'min-purchase-qty', null, 400 );
168 }
169
170 $minimum_purchase_amount = $coupon->settings['coupon_min_purchase_amount'] ?? 0;
171 if ( 'amount' === $coupon->settings['coupon_type_of_min_requirement'] && $subtotal < (float) $minimum_purchase_amount ) {
172 throw new StoreEngineException( esc_html(
173 sprintf(
174 // translators: %s: minimum purchase amount.
175 __( 'Sorry, Coupon has minimum purchase amount of %s', 'storeengine' ),
176 Formatting::price( $minimum_purchase_amount, [
177 'in_span' => false
178 ] )
179 )
180 ), 'min-purchase-amount', null, 400 );
181 }
182 }
183
184 /**
185 * @throws StoreEngineException
186 */
187 public static function check_prices_requirement( \StoreEngine\classes\Coupon $coupon ) {
188 if ( empty( $coupon->get_valid_price_data() ) ) {
189 return;
190 }
191
192 $cart = Helper::cart();
193 if ( ! $cart ) {
194 // No cart to validate against (should not happen once the cart is
195 // ready). Skip rather than fatal on a null cart.
196 return;
197 }
198 $is_valid = false;
199 foreach ( $coupon->get_valid_price_data() as $valid_price_data ) {
200 if ( $cart->is_product_in_cart( $valid_price_data['product_id'] ) ) {
201 if ( empty( $valid_price_data['price_ids'] ) ) {
202 $is_valid = true;
203 break;
204 }
205
206 foreach ( $valid_price_data['price_ids'] as $price_id ) {
207 if ( $cart->is_price_in_cart( $price_id ) ) {
208 $is_valid = true;
209 break;
210 }
211 }
212
213 if ( $is_valid ) {
214 break;
215 }
216 }
217 }
218
219 if ( ! $is_valid ) {
220 throw new StoreEngineException( esc_html__( "Sorry, Coupon isn't valid for your cart.", 'storeengine' ), 'min-purchase-amount', null, 400 );
221 }
222 }
223
224 }
225