PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.0
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Services / Coupon / Concerns / CanValidateCoupon.php

CanValidateCoupon.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.0, at app/Services/Coupon/Concerns/CanValidateCoupon.php

291 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Coupon\Concerns;
4
5 use FluentCart\App\Models\AppliedCoupon;
6 use FluentCart\App\Models\Coupon;
7 use FluentCart\App\Models\Customer;
8 use FluentCart\App\Services\DateTime\DateTime;
9 use FluentCart\App\Services\OrderService;
10 use FluentCart\Framework\Support\Arr;
11 use FluentCart\Framework\Support\Collection;
12 use WP_Error;
13
14 trait CanValidateCoupon
15 {
16 /**
17 * @return bool|WP_Error
18 */
19 public function validate($couponCode)
20 {
21 $couponCode = apply_filters('fluent_cart/coupon/validating_coupon', $couponCode,
22 [
23 'coupon_code' => $couponCode,
24 'line_items' => $this->lineItems,
25 'couponService' => $this
26 ]
27 );
28
29 if (is_wp_error($couponCode)) {
30 return $couponCode;
31 }
32
33 if (empty($couponCode)) {
34 return $this->makeError(__('Invalid Coupon Code', 'fluent-cart'), 404);
35 }
36
37 $coupon = $this->applicableCoupons->firstWhere('code', $couponCode);
38
39 if (empty($coupon)) {
40 return $this->makeError(__('Coupon Not Found', 'fluent-cart'), 404);
41 }
42
43 if ($this->isAlreadyApplied($coupon)) {
44 return $this->makeError(__('Coupon already applied', 'fluent-cart'), 401);
45 }
46
47 //Return true at this point is not using cart, because at this point coupon is already used in an order
48 if (!$this->usingCart && $this->previouslyAppliedCoupons->has($couponCode)) {
49 return true;
50 }
51
52 if (!$this->canBeStacked($coupon)) {
53 return $this->makeError(__('Can not apply the coupons together', 'fluent-cart'), 401);
54 }
55
56 if (!$this->isCouponActive($coupon)) {
57
58 return $this->makeError(__('This coupon has expired or is not valid', 'fluent-cart'), 401);
59 }
60
61 if ($this->requiredUserToBeLoggedIn($coupon)) {
62
63 if (!is_user_logged_in()) {
64 return $this->makeError(__('You Need To be logged in', 'fluent-cart'), 403);
65 }
66
67 if (!$this->hasMaxPerUserLimit($coupon)) {
68 return $this->makeError(__('Coupon Uses Limit Exceeded', 'fluent-cart'), 401);
69 }
70 }
71
72 if (!$this->hasUseLimit($coupon)) {
73 return $this->makeError(__('Coupon Uses Limit Exceeded', 'fluent-cart'), 403);
74 }
75
76 if (!$this->ensureMinimumPurchaseAmount($coupon)) {
77 return $this->makeError(__('Purchase amount is smaller than required amount', 'fluent-cart'), 403);
78 }
79
80 if (!$this->isProductValidated($coupon)) {
81 return $this->makeError(__('No applicable products in the cart for this coupon', 'fluent-cart'), 403);
82 }
83
84 return true;
85 }
86
87 public function isProductValidated($coupon): bool
88 {
89 foreach ($this->lineItems as $item) {
90 if ($this->isApplicableToProduct($coupon, $item['post_id'], $item['id'])) {
91 return true;
92 }
93 }
94 return false;
95 }
96
97 public function canBeStacked(Coupon $coupon): bool
98 {
99 //If there is no applied coupon return true;
100 if (empty($this->previouslyAppliedCouponCodes)) {
101 return true;
102 }
103 //If there is any applied coupon, that is not stackable return false;
104 foreach ($this->previouslyAppliedCoupons as $lcoupon) {
105 if (Arr::get($lcoupon, 'stackable') === 'no') {
106 return false;
107 }
108 }
109
110 //return true if the coupon is stackable
111 return (Arr::get($coupon, 'stackable') !== 'no');
112 }
113
114 public function isAlreadyApplied(Coupon $coupon): bool
115 {
116 return in_array($coupon->code, $this->previouslyAppliedCouponCodes);
117 }
118
119 public function ensureMinimumPurchaseAmount(Coupon $coupon): bool
120 {
121 $min_purchase_amount = Arr::get($coupon->conditions, 'min_purchase_amount', null);
122 if (empty($min_purchase_amount)) {
123 return true;
124 } else {
125 // The coupon's min_amount_basis ('subtotal' | 'total') governs whether shipping counts
126 // toward this gate. This admin order-editing path operates on line items only and has no
127 // live shipping context, so the check is always against the items subtotal here — the
128 // 'total' (shipping-inclusive) basis takes effect on the storefront checkout path
129 // (see DiscountService::isCouponValid).
130 $orderTotal = OrderService::getItemsAmountTotal($this->lineItems, false, false);
131 return $min_purchase_amount <= $orderTotal;
132 }
133 }
134
135 public function isCouponActive(Coupon $coupon): bool
136 {
137 $status = $coupon->status;
138 $now = DateTime::gmtNow();
139
140 $hasExpirationDate = true;
141 $hasStartDate = true;
142
143 if (empty($coupon->start_date) || $coupon->start_date === '0000-00-00 00:00:00') {
144 $hasStartDate = false;
145 }
146
147 if (empty($coupon->end_date) || $coupon->end_date === '0000-00-00 00:00:00') {
148 $hasExpirationDate = false;
149 }
150
151
152 if ($status === 'active' && !$hasExpirationDate && !$hasStartDate) {
153 return true;
154 }
155
156
157 if ($status === 'scheduled' || $status === 'active') {
158
159 $startDate = null;
160 $endDate = null;
161 $couponStarted = false;
162 $couponEnded = false;
163
164 if (!empty($coupon->start_date)) {
165 $startDate = DateTime::parse($coupon->start_date);
166 $couponStarted = $startDate <= $now;
167 }
168
169 if (!empty($coupon->end_date)) {
170 $endDate = DateTime::parse($coupon->end_date);
171 $couponEnded = $endDate < $now;
172 }
173
174
175
176
177 if (empty($startDate))
178 return !$couponEnded;
179 if (empty($endDate))
180 return $couponStarted;
181
182 return $couponStarted && !$couponEnded;
183 }
184
185 return false;
186 }
187
188 public function hasUseLimit(Coupon $coupon): bool
189 {
190 $maxUse = Arr::get($coupon->conditions, 'max_uses', null);
191 if (empty($maxUse)) {
192 return true;
193 }
194
195 if (empty($coupon->use_count)) {
196 return true;
197 }
198
199 return $maxUse > $coupon->use_count;
200 }
201
202 public function requiredUserToBeLoggedIn(Coupon $coupon): bool
203 {
204 return Arr::get($coupon->conditions, 'max_per_customer', 0);
205 }
206
207 public function hasMaxPerUserLimit(Coupon $coupon): bool
208 {
209 $maxPerCustomer = Arr::get($coupon->conditions, 'max_per_customer', 0);
210 if (empty($maxPerCustomer)) {
211 return true;
212 }
213
214 $userId = get_current_user_id();
215
216 $customer = Customer::query()->where('user_id', $userId)->first();
217
218 if (empty($customer)) {
219 return true;
220 }
221
222 $appliedCoupons = AppliedCoupon::query()
223 ->where('code', $coupon->code)
224 ->where('customer_id', $customer->id)
225 ->get();
226
227 return $maxPerCustomer > $appliedCoupons->count();
228 }
229
230 public function isApplicableToProduct(Coupon $coupon, $productId, $variationsId): bool
231 {
232 if (empty($variationsId)) {
233 return false;
234 }
235
236 $categories = Arr::get($this->products, ($productId . '') . '.categories');
237
238 $categoryIds = Collection::make($categories)->pluck('term_id')->toArray();
239
240 $canBeApplied = true;
241
242 $conditions = $coupon->conditions;
243
244 $excludedCategories = $this->getArrayValue($conditions, 'excluded_categories');
245 $includedCategories = $this->getArrayValue($conditions, 'included_categories');
246 $excludedProducts = $this->getArrayValue($conditions, 'excluded_products');
247 $includedProducts = $this->getArrayValue($conditions, 'included_products');
248
249 if (in_array($variationsId, $includedProducts)) {
250 return true;
251 }
252
253 if (in_array($variationsId, $excludedProducts)) {
254 return false;
255 }
256
257 foreach ($categoryIds as $categoryId) {
258 if (empty($categoryId)) {
259 continue;
260 }
261 if (in_array($categoryId, $excludedCategories)) {
262 $canBeApplied = false;
263 }
264 if (!empty($includedCategories) && !in_array($categoryId, $includedCategories)) {
265 $canBeApplied = false;
266 }
267
268 if (!$canBeApplied) {
269 break;
270 }
271 }
272
273 if (!empty($includedProducts) && !in_array($variationsId, $includedProducts)) {
274 return false;
275 }
276
277
278 return $canBeApplied;
279 }
280
281 protected function makeError(string $message, $code = null): WP_Error
282 {
283 return new WP_Error($code, $message);
284 }
285
286 private function getArrayValue($array, $key, $defaultValue = [])
287 {
288 return isset($array[$key]) && is_array($array[$key]) ? $array[$key] : $defaultValue;
289 }
290 }
291