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

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

471 lines 17.9 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 public static function checkProductEligibility($productId, $couponCode, $origin)
304 {
305 $coupon = static::getQuery()->where('code', $couponCode)->first();
306 $excludedCategories = json_decode(Arr::get($coupon, 'excluded_categories', '[]'), true) ?? [];
307 $includedCategories = json_decode(Arr::get($coupon, 'included_categories', '[]'), true) ?? [];
308 $excludedProducts = json_decode(Arr::get($coupon, 'excluded_products', '[]'), true) ?? [];
309 $includedProducts = json_decode(Arr::get($coupon, 'included_products', '[]'), true) ?? [];
310 $product = Product::with('wp_terms')->find($productId)->toArray();
311
312 $productCategories = array_map(function ($productCategories) {
313 return $productCategories['term_taxonomy_id'];
314 }, $product['wp_terms']);
315
316 if (!empty($includedProducts) && !in_array($productId, $includedProducts)) {
317
318 if ($includedCategories !== null && !empty(array_intersect($includedCategories, $productCategories))) {
319 return [
320 'isApplicable' => true,
321 ];
322 }
323 return [
324 'isApplicable' => false,
325 ];
326 }
327
328 if (empty($excludedCategories) && empty($excludedProducts) && empty($includedCategories) && empty($includedProducts)) {
329 return [
330 'isApplicable' => true
331 ];
332 }
333
334 if (empty($productCategories) && !in_array($productId, $excludedProducts)) {
335 return [
336 'isApplicable' => true
337 ];
338 }
339
340 // Check if the product is in the excluded products list
341 if ($excludedProducts !== null && in_array($productId, $excludedProducts)) {
342 return [
343 'isApplicable' => false,
344 ];
345 }
346
347 // Check if the product belongs to any included product list
348
349 if ($includedProducts !== null && in_array($productId, $includedProducts)) {
350 return [
351 'isApplicable' => true,
352 ];
353 }
354
355 /**
356 * Error message when a coupon conflicts with a product.
357 *
358 * %1$s - Product Title (e.g., "Product A")
359 * %2$s - Coupon Code (e.g., "DISCOUNT10")
360 *
361 * This string is shown when a coupon conflicts with a product, and the user
362 * needs to remove the coupon first.
363 */
364 $message = sprintf(
365 /* translators: %1$s: Product Title, %2$s: Coupon Code */
366 __('%1$s conflicts with %2$s coupon. Remove the coupon first.', 'fluent-cart'),
367 $product['post_title'], // %1$s: Product Title (e.g., "Product A")
368 $couponCode // %2$s: Coupon Code (e.g., "DISCOUNT10")
369 );
370
371 // Check if the product belongs to any excluded categories
372 if ($excludedCategories != null && array_intersect($excludedCategories, $productCategories)) {
373 return [
374 'isApplicable' => false,
375 'message' => $message
376 ];
377 }
378
379 //check if the product belongs to any included categories
380 if ($includedCategories !== null && empty(array_intersect($includedCategories, $productCategories))) {
381 return [
382 'isApplicable' => false,
383 'message' => __('This coupon can not be applied to some items in your cart', 'fluent-cart'),
384 ];
385 }
386 return [
387 'isApplicable' => true,
388 ];
389 }
390
391 public static function getCouponApplicableItemsWithLineTotal($coupon, $modifiedOrderItems)
392 {
393 $items = $modifiedOrderItems;
394 $origin = null;
395 $applicableTotalWithItems = []; // Initialize the array for storing item totals
396 $applicableTotal = 0;
397
398 foreach ($items as $item) {
399 $isEligible = self::checkProductEligibility($item['post_id'], $coupon, $origin);
400
401 if ($isEligible['isApplicable'] === true && !isset($item['object_type'])) {
402 if ($item['other_info']['payment_type'] === 'subscription' && $item['other_info']['manage_setup_fee'] === 'yes' && $item['other_info']['signup_fee'] > 0) {
403 if ($item['other_info']['setup_fee_per_item'] === 'yes') {
404 $setup_fee = $item['other_info']['signup_fee'] * $item['quantity'];
405 } else {
406 $setup_fee = $item['other_info']['signup_fee'];
407 }
408 }
409 if (isset($item['other_info']['manage_setup_fee']) === 'yes') {
410 $lineTotal = $item['discounted_price'] * $item['quantity'] + $setup_fee;
411
412 } else {
413 $lineTotal = $item['discounted_price'] * $item['quantity'];
414 }
415
416 $applicableTotalWithItems[$item['post_id']] = $lineTotal; // Store the total for this item
417 $applicableTotal += $lineTotal;
418 }
419 }
420
421 return [
422 'lineTotalWithItems' => $applicableTotalWithItems,
423 'applicableTotal' => $applicableTotal,
424 'orderItems' => $items,
425 ];
426 }
427 public static function updateCouponStatus($coupon = [])
428 {
429 if (empty($coupon)) {
430 return;
431 }
432
433 $startDate = $coupon->start_date;
434 $endDate = $coupon->end_date;
435 $status = $coupon->status;
436
437 $now = DateTime::gmtNow();
438
439 $startDateTime = (!empty($startDate) && $startDate !== '0000-00-00 00:00:00')
440 ? DateTime::anyTimeToGmt($startDate)
441 : null;
442
443 $endDateTime = (!empty($endDate) && $endDate !== '0000-00-00 00:00:00')
444 ? DateTime::anyTimeToGmt($endDate)
445 : null;
446
447 // Mark expired if end date passed
448 if ($endDateTime && $endDateTime < $now) {
449 if ($status !== 'expired') {
450 $coupon->setStatus('expired');
451 $coupon->save();
452 }
453 return;
454 }
455
456 // Set status to scheduled if start date is in the future
457 if ($startDateTime && $startDateTime > $now && !in_array($status, ['disabled', 'scheduled'])) {
458 $coupon->setStatus('scheduled');
459 $coupon->save();
460 return;
461 }
462
463 // Activate if start date passed and status is not already active/disabled
464 if ($startDateTime && $startDateTime <= $now && !in_array($status, ['disabled', 'active'])) {
465 $coupon->setStatus('active');
466 $coupon->save();
467 }
468 }
469
470 }
471