PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
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 trunk 1.2.0 All 47 releases
fluent-cart / app / Helpers / CouponHelper.php

CouponHelper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Helpers/CouponHelper.php

527 lines 19.6 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\Helpers;
4
5 use FluentCart\Api\Resource\AppliedCouponResource;
6 use FluentCart\Api\Resource\CouponResource;
7 use FluentCart\App\Models\AppliedCoupon;
8 use FluentCart\App\Models\Coupon;
9 use FluentCart\App\Models\Customer;
10 use FluentCart\App\Models\Order;
11 use FluentCart\App\Models\Product;
12 use FluentCart\App\Services\DateTime\DateTime;
13 use FluentCart\Framework\Database\Orm\Builder;
14 use FluentCart\Framework\Support\Arr;
15
16 /**
17 * Class CouponHelper
18 *
19 * This class provides utility functions related to coupons and their validation, cancellation, and calculation of discounts.
20 */
21 class CouponHelper
22 {
23
24 /**
25 * Validates a coupon against various criteria.
26 *
27 * @param object $appliedCoupon The coupon object to be validated.
28 *
29 * @return array An array containing validation results.
30 */
31
32 public static function getQuery(): Builder
33 {
34 return Coupon::query();
35 }
36
37 public function calculateCoupon($appliedCouponList, $orderTotal)
38 {
39 $calculatedDiscountResult = CouponResource::calculateTotalDiscountAndPurchaseAmount($appliedCouponList, $orderTotal);
40 $discount = Arr::get($calculatedDiscountResult, 'total_discount');
41 $orderTotal = Arr::get($calculatedDiscountResult, 'total_purchase_amount');
42 $appliedDiscountsList = Arr::get($calculatedDiscountResult, 'applied_discounts');
43
44 return [
45 'total_discount' => $discount,
46 'total_purchase_amount' => $orderTotal,
47 'applied_discounts' => $appliedDiscountsList
48 ];
49 }
50
51 public function storeAppliedCouponData($appliedCouponList, $appliedDiscountsList, $orderId)
52 {
53 for ($i = 0; $i < count($appliedCouponList); $i++) {
54 $appliedCoupon = self::getSingleCouponDetails($appliedCouponList[$i], $appliedDiscountsList[$i]);
55
56 $appliedCouponSnapShot[] = [
57 'order_id' => $orderId,
58 'coupon_id' => $appliedCoupon['id'],
59 'title' => $appliedCoupon['title'],
60 'code' => $appliedCoupon['code'],
61 'status' => $appliedCoupon['status'],
62 'type' => $appliedCoupon['type'],
63 'amount' => $appliedCoupon['amount'],
64 'discounted_amount' => $appliedCoupon['discounted_amount'],
65 'stackable' => $appliedCoupon['stackable'],
66 'priority' => $appliedCoupon['priority'],
67 'max_uses' => $appliedCoupon['max_uses'],
68 'use_count' => $appliedCoupon['use_count'],
69 'max_per_customer' => $appliedCoupon['max_per_customer'],
70 'min_purchase_amount' => $appliedCoupon['min_purchase_amount'],
71 'max_discount_amount' => $appliedCoupon['max_discount_amount'],
72 'notes' => $appliedCoupon['max_discount_amount']
73 ];
74
75 }
76
77 // $orderMetaData = [
78 // 'order_id' => $order['id'],
79 // 'key' => 'applied_coupon',
80 // 'value'=> json_encode($appliedCouponSnapShot)
81 // ];
82 // OrderMetaResource::create($orderMetaData);
83 return AppliedCouponResource::create($appliedCouponSnapShot);
84 }
85
86
87 /**
88 * Calculates the discount based on the coupon code and purchase amount.
89 *
90 * @param string $couponCode The code of the coupon.
91 * @param float $purchaseAmount The total purchase amount.
92 *
93 * @return float The calculated discount amount.
94 */
95
96 public static function calculateDiscount($coupon, $applicableTotalWithItems)
97 {
98 $type = Arr::get($coupon, 'type', null);
99 $max_discount = Arr::get($coupon, 'max_discount_amount', null);
100 $min_purchase_amount = Arr::get($coupon, 'min_purchase_amount');
101 $discountAmount = floatval(Arr::get($coupon, 'amount', null));
102 $lineData = Arr::get($applicableTotalWithItems, 'lineTotalWithItems', []);
103 $applicableTotal = Arr::get($applicableTotalWithItems, 'applicableTotal', null);
104 $totalApplicableDiscount = 0;
105
106 foreach ($lineData as $key => $value) {
107 if ($type === 'percentage') {
108 $totalApplicableDiscount = Helper::toDecimal($applicableTotal * $discountAmount);
109 if ($totalApplicableDiscount > $max_discount && $max_discount != 0) {
110 $totalApplicableDiscount = $max_discount;
111 }
112
113 $discountPercentage = Helper::toCent($totalApplicableDiscount / $applicableTotal);
114 $discount = Helper::toDecimal($discountPercentage) * $value;
115
116 $lineData[$key] =
117 [
118 'discountAmount' => $discount,
119 'originalAmount' => $value,
120 'afterDiscountAmount' => $value - $discount,
121 ];
122
123 continue;
124 }
125
126 if ($type === 'fixed') {
127 if ($min_purchase_amount === null || $applicableTotal > $min_purchase_amount) {
128 if ($discountAmount >= $applicableTotal) {
129 $discountAmount = $applicableTotal;
130 }
131 $totalApplicableDiscount = $discountAmount;
132 $discountPercentage = Helper::toCent($discountAmount / $applicableTotal);
133 $discount = Helper::toDecimal($discountPercentage) * $value;
134
135 $lineData[$key] =
136 [
137 'discountAmount' => $discount,
138 'originalAmount' => $value,
139 'afterDiscountAmount' => $value - $discount,
140 ];
141
142 continue;
143 }
144 }
145 }
146
147 return [
148 'lineData' => $lineData,
149 'totalApplicableDiscount' => $totalApplicableDiscount
150 ];
151 }
152
153 /**
154 * Cancels a coupon and adjusts the purchase amount if needed.
155 *
156 * @param object $cancelledCoupon The coupon object to cancel.
157 *
158 * @return array|null An array with the adjusted purchase amount if the coupon is canceled.
159 */
160
161 public static function checkStackability($appliedCoupon, $appliedCouponList, $coupon)
162 {
163
164 if (in_array($appliedCoupon, $appliedCouponList)) {
165 return
166 ['code' => 400, 'message' => sprintf(
167 /* translators: %s is the coupon code */
168 __('%s coupon can only be applied once per order', 'fluent-cart'), $appliedCoupon)];
169 }
170
171 if (count($appliedCouponList) > 0) {
172 $lastCouponOfArray = end($appliedCouponList);
173 $lastCouponStackability = static::getQuery()->where('code', $lastCouponOfArray)->first()['stackable'];
174 if ($lastCouponStackability == 'no') {
175 return
176 ['code' => 400, 'message' => sprintf(
177 /* translators: %s is the coupon code */
178 __('%s coupon cannot be used with other coupon', 'fluent-cart'), end($appliedCouponList))];
179 }
180 }
181
182 if (count($appliedCouponList) > 0 && $coupon->stackable == 'no') {
183
184 /**
185 * Error message when a coupon cannot be used with another coupon.
186 *
187 * %1$s - Applied Coupon (e.g., "DISCOUNT10")
188 *
189 * This string is shown when the applied coupon cannot be used together with another coupon.
190 */
191 return [
192 'code' => 400,
193 'message' => sprintf(
194 /* translators: %s is the applied coupon code */
195 __('This %s coupon cannot be used with other coupon', 'fluent-cart'),
196 $appliedCoupon // %s: Applied Coupon (e.g., "DISCOUNT10")
197 )
198 ];
199
200 }
201 }
202
203 //Returning false from this method meaning the coupon is valid
204 public static function checkUsageLimit(Coupon $coupon, $customerEmail = '', $trigger = null)
205 {
206 $code = Arr::get($coupon, 'code', null);
207
208 $maxUsesLimitAllCustomer = Arr::get($coupon, 'max_uses', null);
209 $maxUsesLimitPerCustomer = Arr::get($coupon, 'max_per_customer', null);
210
211 $maxUsesLimitAllCustomer = $maxUsesLimitAllCustomer !== null ? intval($maxUsesLimitAllCustomer) : null;
212 $maxUsesLimitPerCustomer = $maxUsesLimitPerCustomer !== null ? intval($maxUsesLimitPerCustomer) : null;
213
214
215 $totalUsedByAllCustomer = AppliedCoupon::where('code', $code)->get()->count();
216
217 if (($maxUsesLimitAllCustomer === null && $maxUsesLimitPerCustomer === null)) {
218 return false;
219 }
220
221 $customer = ($trigger == 'on_checkout')
222 ? Customer::query()->where('email', $customerEmail)->first()
223 : ( CartCheckoutHelper::make())->getCustomer($customerEmail);
224
225 if (!$customer) {
226 if (($totalUsedByAllCustomer >= $maxUsesLimitAllCustomer && $maxUsesLimitAllCustomer !== null) || $maxUsesLimitAllCustomer === 0 || $maxUsesLimitPerCustomer === 0) {
227 return true;
228 }
229 if (($maxUsesLimitAllCustomer === null && $maxUsesLimitPerCustomer > 0) || ($maxUsesLimitAllCustomer < $totalUsedByAllCustomer && $maxUsesLimitPerCustomer > 0)) {
230 return false;
231 }
232 return false;
233 } else {
234 $orderIds = Order::where('customer_id', $customer->id)->pluck('id');
235 $totalUsedCouponPerCustomer = AppliedCoupon::whereIn('order_id', $orderIds)->where('code', $code)->get()->count();
236
237 if (
238 ($totalUsedByAllCustomer < $maxUsesLimitAllCustomer && $maxUsesLimitPerCustomer === null) ||
239 ($maxUsesLimitAllCustomer === null && $totalUsedCouponPerCustomer < $maxUsesLimitPerCustomer)
240 ) {
241 return false;
242 }
243
244 if (
245 ($maxUsesLimitAllCustomer === 0 && $maxUsesLimitPerCustomer === 0)
246 || ($maxUsesLimitAllCustomer === null && $maxUsesLimitPerCustomer === 0)
247 || $maxUsesLimitAllCustomer === 0 || $maxUsesLimitPerCustomer === 0
248 || $totalUsedByAllCustomer >= $maxUsesLimitAllCustomer
249 || $totalUsedCouponPerCustomer >= $maxUsesLimitPerCustomer
250 ) {
251 return true;
252 }
253 }
254
255 return false;
256
257 }
258
259 public static function sortByPriority($appliedCouponList = [])
260 {
261 usort(
262 $appliedCouponList,
263 function ($a, $b) {
264 $priorityA = self::getCouponPriority($a);
265 $priorityB = self::getCouponPriority($b);
266
267 return $priorityA - $priorityB;
268 }
269 );
270 return $appliedCouponList;
271 }
272
273 public static function getSingleCouponDetails($coupon, $discount)
274 {
275 $couponDetails = static::getQuery()->where('code', $coupon)->first();
276 $couponDetails['discounted_amount'] = $discount;
277 return $couponDetails;
278 }
279
280 public static function getCouponPriority($couponCode)
281 {
282 $coupon = static::getQuery()->where('code', $couponCode)->first();
283 return Arr::get($coupon, 'priority', 0);
284 }
285
286 public function prepareCouponCalculation($order)
287 {
288 $appliedCouponList = array_map(function ($coupon) {
289 return $coupon['code'];
290 }, $order['applied_coupon']);
291
292 $calculatedDiscountResult = $this->calculateCoupon($appliedCouponList, $order['subtotal']);
293
294 $discount = Arr::get($calculatedDiscountResult, 'total_discount');
295 return [
296 'discount' => $discount,
297 'subTotal' => $order['subtotal'],
298 'appliedCouponList' => $appliedCouponList
299 ];
300
301 }
302
303 /**
304 * Coupon restriction lists live inside the JSON `conditions` column. The model accessor
305 * decodes them to arrays, but rows written before that accessor existed can still hold a
306 * JSON string, so accept both shapes.
307 *
308 * @param mixed $value
309 * @return array
310 */
311 protected static function normalizeConditionList($value): array
312 {
313 if (is_array($value)) {
314 return array_values($value);
315 }
316
317 if (is_string($value) && $value !== '') {
318 $decoded = json_decode($value, true);
319 return is_array($decoded) ? array_values($decoded) : [];
320 }
321
322 return [];
323 }
324
325 public static function checkProductEligibility($productId, $couponCode, $origin)
326 {
327 $coupon = static::getQuery()->where('code', $couponCode)->first();
328
329 if (!$coupon) {
330 return [
331 'isApplicable' => false,
332 'message' => __('This coupon is no longer available.', 'fluent-cart'),
333 ];
334 }
335
336 $productModel = Product::with('wp_terms')->find($productId);
337
338 if (!$productModel) {
339 return [
340 'isApplicable' => false,
341 'message' => __('Product not found.', 'fluent-cart'),
342 ];
343 }
344
345 return static::evaluateProductEligibility($productModel, $coupon);
346 }
347
348 /**
349 * Verdict for one already-loaded product/coupon pair, so a caller checking several
350 * coupons at once hydrates the product and the coupon rows a single time.
351 *
352 * @param \FluentCart\App\Models\Product $productModel
353 * @param \FluentCart\App\Models\Coupon $coupon
354 * @return array
355 */
356 public static function evaluateProductEligibility($productModel, $coupon): array
357 {
358 $productId = $productModel->getKey();
359 $couponCode = $coupon->code;
360 $conditions = $coupon->conditions;
361 $excludedCategories = static::normalizeConditionList(Arr::get($conditions, 'excluded_categories'));
362 $includedCategories = static::normalizeConditionList(Arr::get($conditions, 'included_categories'));
363 $excludedProducts = static::normalizeConditionList(Arr::get($conditions, 'excluded_products'));
364 $includedProducts = static::normalizeConditionList(Arr::get($conditions, 'included_products'));
365
366 $product = $productModel->toArray();
367
368 $productCategories = array_map(function ($productCategories) {
369 return $productCategories['term_taxonomy_id'];
370 }, $product['wp_terms']);
371
372 if (!empty($includedProducts) && !in_array($productId, $includedProducts)) {
373
374 if ($includedCategories !== null && !empty(array_intersect($includedCategories, $productCategories))) {
375 return [
376 'isApplicable' => true,
377 ];
378 }
379 return [
380 'isApplicable' => false,
381 ];
382 }
383
384 if (empty($excludedCategories) && empty($excludedProducts) && empty($includedCategories) && empty($includedProducts)) {
385 return [
386 'isApplicable' => true
387 ];
388 }
389
390 if (empty($productCategories) && !in_array($productId, $excludedProducts)) {
391 return [
392 'isApplicable' => true
393 ];
394 }
395
396 // Check if the product is in the excluded products list
397 if ($excludedProducts !== null && in_array($productId, $excludedProducts)) {
398 return [
399 'isApplicable' => false,
400 ];
401 }
402
403 // Check if the product belongs to any included product list
404
405 if ($includedProducts !== null && in_array($productId, $includedProducts)) {
406 return [
407 'isApplicable' => true,
408 ];
409 }
410
411 /**
412 * Error message when a coupon conflicts with a product.
413 *
414 * %1$s - Product Title (e.g., "Product A")
415 * %2$s - Coupon Code (e.g., "DISCOUNT10")
416 *
417 * This string is shown when a coupon conflicts with a product, and the user
418 * needs to remove the coupon first.
419 */
420 $message = sprintf(
421 /* translators: %1$s: Product Title, %2$s: Coupon Code */
422 __('%1$s conflicts with %2$s coupon. Remove the coupon first.', 'fluent-cart'),
423 $product['post_title'], // %1$s: Product Title (e.g., "Product A")
424 $couponCode // %2$s: Coupon Code (e.g., "DISCOUNT10")
425 );
426
427 // Check if the product belongs to any excluded categories
428 if ($excludedCategories != null && array_intersect($excludedCategories, $productCategories)) {
429 return [
430 'isApplicable' => false,
431 'message' => $message
432 ];
433 }
434
435 //check if the product belongs to any included categories
436 if ($includedCategories !== null && empty(array_intersect($includedCategories, $productCategories))) {
437 return [
438 'isApplicable' => false,
439 'message' => __('This coupon can not be applied to some items in your cart', 'fluent-cart'),
440 ];
441 }
442 return [
443 'isApplicable' => true,
444 ];
445 }
446
447 public static function getCouponApplicableItemsWithLineTotal($coupon, $modifiedOrderItems)
448 {
449 $items = $modifiedOrderItems;
450 $origin = null;
451 $applicableTotalWithItems = []; // Initialize the array for storing item totals
452 $applicableTotal = 0;
453
454 foreach ($items as $item) {
455 $isEligible = self::checkProductEligibility($item['post_id'], $coupon, $origin);
456
457 if ($isEligible['isApplicable'] === true && !isset($item['object_type'])) {
458 if ($item['other_info']['payment_type'] === 'subscription' && $item['other_info']['manage_setup_fee'] === 'yes' && $item['other_info']['signup_fee'] > 0) {
459 if ($item['other_info']['setup_fee_per_item'] === 'yes') {
460 $setup_fee = $item['other_info']['signup_fee'] * $item['quantity'];
461 } else {
462 $setup_fee = $item['other_info']['signup_fee'];
463 }
464 }
465 if (isset($item['other_info']['manage_setup_fee']) === 'yes') {
466 $lineTotal = $item['discounted_price'] * $item['quantity'] + $setup_fee;
467
468 } else {
469 $lineTotal = $item['discounted_price'] * $item['quantity'];
470 }
471
472 $applicableTotalWithItems[$item['post_id']] = $lineTotal; // Store the total for this item
473 $applicableTotal += $lineTotal;
474 }
475 }
476
477 return [
478 'lineTotalWithItems' => $applicableTotalWithItems,
479 'applicableTotal' => $applicableTotal,
480 'orderItems' => $items,
481 ];
482 }
483 public static function updateCouponStatus($coupon = [])
484 {
485 if (empty($coupon)) {
486 return;
487 }
488
489 $startDate = $coupon->start_date;
490 $endDate = $coupon->end_date;
491 $status = $coupon->status;
492
493 $now = DateTime::gmtNow();
494
495 $startDateTime = (!empty($startDate) && $startDate !== '0000-00-00 00:00:00')
496 ? DateTime::anyTimeToGmt($startDate)
497 : null;
498
499 $endDateTime = (!empty($endDate) && $endDate !== '0000-00-00 00:00:00')
500 ? DateTime::anyTimeToGmt($endDate)
501 : null;
502
503 // Mark expired if end date passed
504 if ($endDateTime && $endDateTime < $now) {
505 if ($status !== 'expired') {
506 $coupon->setStatus('expired');
507 $coupon->save();
508 }
509 return;
510 }
511
512 // Set status to scheduled if start date is in the future
513 if ($startDateTime && $startDateTime > $now && !in_array($status, ['disabled', 'scheduled'])) {
514 $coupon->setStatus('scheduled');
515 $coupon->save();
516 return;
517 }
518
519 // Activate if start date passed and status is not already active/disabled
520 if ($startDateTime && $startDateTime <= $now && !in_array($status, ['disabled', 'active'])) {
521 $coupon->setStatus('active');
522 $coupon->save();
523 }
524 }
525
526 }
527