$discount, 'total_purchase_amount' => $orderTotal, 'applied_discounts' => $appliedDiscountsList ]; } public function storeAppliedCouponData($appliedCouponList, $appliedDiscountsList, $orderId) { for ($i = 0; $i < count($appliedCouponList); $i++) { $appliedCoupon = self::getSingleCouponDetails($appliedCouponList[$i], $appliedDiscountsList[$i]); $appliedCouponSnapShot[] = [ 'order_id' => $orderId, 'coupon_id' => $appliedCoupon['id'], 'title' => $appliedCoupon['title'], 'code' => $appliedCoupon['code'], 'status' => $appliedCoupon['status'], 'type' => $appliedCoupon['type'], 'amount' => $appliedCoupon['amount'], 'discounted_amount' => $appliedCoupon['discounted_amount'], 'stackable' => $appliedCoupon['stackable'], 'priority' => $appliedCoupon['priority'], 'max_uses' => $appliedCoupon['max_uses'], 'use_count' => $appliedCoupon['use_count'], 'max_per_customer' => $appliedCoupon['max_per_customer'], 'min_purchase_amount' => $appliedCoupon['min_purchase_amount'], 'max_discount_amount' => $appliedCoupon['max_discount_amount'], 'notes' => $appliedCoupon['max_discount_amount'] ]; } // $orderMetaData = [ // 'order_id' => $order['id'], // 'key' => 'applied_coupon', // 'value'=> json_encode($appliedCouponSnapShot) // ]; // OrderMetaResource::create($orderMetaData); return AppliedCouponResource::create($appliedCouponSnapShot); } /** * Calculates the discount based on the coupon code and purchase amount. * * @param string $couponCode The code of the coupon. * @param float $purchaseAmount The total purchase amount. * * @return float The calculated discount amount. */ public static function calculateDiscount($coupon, $applicableTotalWithItems) { $type = Arr::get($coupon, 'type', null); $max_discount = Arr::get($coupon, 'max_discount_amount', null); $min_purchase_amount = Arr::get($coupon, 'min_purchase_amount'); $discountAmount = floatval(Arr::get($coupon, 'amount', null)); $lineData = Arr::get($applicableTotalWithItems, 'lineTotalWithItems', []); $applicableTotal = Arr::get($applicableTotalWithItems, 'applicableTotal', null); $totalApplicableDiscount = 0; foreach ($lineData as $key => $value) { if ($type === 'percentage') { $totalApplicableDiscount = Helper::toDecimal($applicableTotal * $discountAmount); if ($totalApplicableDiscount > $max_discount && $max_discount != 0) { $totalApplicableDiscount = $max_discount; } $discountPercentage = Helper::toCent($totalApplicableDiscount / $applicableTotal); $discount = Helper::toDecimal($discountPercentage) * $value; $lineData[$key] = [ 'discountAmount' => $discount, 'originalAmount' => $value, 'afterDiscountAmount' => $value - $discount, ]; continue; } if ($type === 'fixed') { if ($min_purchase_amount === null || $applicableTotal > $min_purchase_amount) { if ($discountAmount >= $applicableTotal) { $discountAmount = $applicableTotal; } $totalApplicableDiscount = $discountAmount; $discountPercentage = Helper::toCent($discountAmount / $applicableTotal); $discount = Helper::toDecimal($discountPercentage) * $value; $lineData[$key] = [ 'discountAmount' => $discount, 'originalAmount' => $value, 'afterDiscountAmount' => $value - $discount, ]; continue; } } } return [ 'lineData' => $lineData, 'totalApplicableDiscount' => $totalApplicableDiscount ]; } /** * Cancels a coupon and adjusts the purchase amount if needed. * * @param object $cancelledCoupon The coupon object to cancel. * * @return array|null An array with the adjusted purchase amount if the coupon is canceled. */ public static function checkStackability($appliedCoupon, $appliedCouponList, $coupon) { if (in_array($appliedCoupon, $appliedCouponList)) { return ['code' => 400, 'message' => sprintf( /* translators: %s is the coupon code */ __('%s coupon can only be applied once per order', 'fluent-cart'), $appliedCoupon)]; } if (count($appliedCouponList) > 0) { $lastCouponOfArray = end($appliedCouponList); $lastCouponStackability = static::getQuery()->where('code', $lastCouponOfArray)->first()['stackable']; if ($lastCouponStackability == 'no') { return ['code' => 400, 'message' => sprintf( /* translators: %s is the coupon code */ __('%s coupon cannot be used with other coupon', 'fluent-cart'), end($appliedCouponList))]; } } if (count($appliedCouponList) > 0 && $coupon->stackable == 'no') { /** * Error message when a coupon cannot be used with another coupon. * * %1$s - Applied Coupon (e.g., "DISCOUNT10") * * This string is shown when the applied coupon cannot be used together with another coupon. */ return [ 'code' => 400, 'message' => sprintf( /* translators: %s is the applied coupon code */ __('This %s coupon cannot be used with other coupon', 'fluent-cart'), $appliedCoupon // %s: Applied Coupon (e.g., "DISCOUNT10") ) ]; } } //Returning false from this method meaning the coupon is valid public static function checkUsageLimit(Coupon $coupon, $customerEmail = '', $trigger = null) { $code = Arr::get($coupon, 'code', null); $maxUsesLimitAllCustomer = Arr::get($coupon, 'max_uses', null); $maxUsesLimitPerCustomer = Arr::get($coupon, 'max_per_customer', null); $maxUsesLimitAllCustomer = $maxUsesLimitAllCustomer !== null ? intval($maxUsesLimitAllCustomer) : null; $maxUsesLimitPerCustomer = $maxUsesLimitPerCustomer !== null ? intval($maxUsesLimitPerCustomer) : null; $totalUsedByAllCustomer = AppliedCoupon::where('code', $code)->get()->count(); if (($maxUsesLimitAllCustomer === null && $maxUsesLimitPerCustomer === null)) { return false; } $customer = ($trigger == 'on_checkout') ? Customer::query()->where('email', $customerEmail)->first() : ( CartCheckoutHelper::make())->getCustomer($customerEmail); if (!$customer) { if (($totalUsedByAllCustomer >= $maxUsesLimitAllCustomer && $maxUsesLimitAllCustomer !== null) || $maxUsesLimitAllCustomer === 0 || $maxUsesLimitPerCustomer === 0) { return true; } if (($maxUsesLimitAllCustomer === null && $maxUsesLimitPerCustomer > 0) || ($maxUsesLimitAllCustomer < $totalUsedByAllCustomer && $maxUsesLimitPerCustomer > 0)) { return false; } return false; } else { $orderIds = Order::where('customer_id', $customer->id)->pluck('id'); $totalUsedCouponPerCustomer = AppliedCoupon::whereIn('order_id', $orderIds)->where('code', $code)->get()->count(); if ( ($totalUsedByAllCustomer < $maxUsesLimitAllCustomer && $maxUsesLimitPerCustomer === null) || ($maxUsesLimitAllCustomer === null && $totalUsedCouponPerCustomer < $maxUsesLimitPerCustomer) ) { return false; } if ( ($maxUsesLimitAllCustomer === 0 && $maxUsesLimitPerCustomer === 0) || ($maxUsesLimitAllCustomer === null && $maxUsesLimitPerCustomer === 0) || $maxUsesLimitAllCustomer === 0 || $maxUsesLimitPerCustomer === 0 || $totalUsedByAllCustomer >= $maxUsesLimitAllCustomer || $totalUsedCouponPerCustomer >= $maxUsesLimitPerCustomer ) { return true; } } return false; } public static function sortByPriority($appliedCouponList = []) { usort( $appliedCouponList, function ($a, $b) { $priorityA = self::getCouponPriority($a); $priorityB = self::getCouponPriority($b); return $priorityA - $priorityB; } ); return $appliedCouponList; } public static function getSingleCouponDetails($coupon, $discount) { $couponDetails = static::getQuery()->where('code', $coupon)->first(); $couponDetails['discounted_amount'] = $discount; return $couponDetails; } public static function getCouponPriority($couponCode) { $coupon = static::getQuery()->where('code', $couponCode)->first(); return Arr::get($coupon, 'priority', 0); } public function prepareCouponCalculation($order) { $appliedCouponList = array_map(function ($coupon) { return $coupon['code']; }, $order['applied_coupon']); $calculatedDiscountResult = $this->calculateCoupon($appliedCouponList, $order['subtotal']); $discount = Arr::get($calculatedDiscountResult, 'total_discount'); return [ 'discount' => $discount, 'subTotal' => $order['subtotal'], 'appliedCouponList' => $appliedCouponList ]; } /** * Coupon restriction lists live inside the JSON `conditions` column. The model accessor * decodes them to arrays, but rows written before that accessor existed can still hold a * JSON string, so accept both shapes. * * @param mixed $value * @return array */ protected static function normalizeConditionList($value): array { if (is_array($value)) { return array_values($value); } if (is_string($value) && $value !== '') { $decoded = json_decode($value, true); return is_array($decoded) ? array_values($decoded) : []; } return []; } public static function checkProductEligibility($productId, $couponCode, $origin) { $coupon = static::getQuery()->where('code', $couponCode)->first(); if (!$coupon) { return [ 'isApplicable' => false, 'message' => __('This coupon is no longer available.', 'fluent-cart'), ]; } $productModel = Product::with('wp_terms')->find($productId); if (!$productModel) { return [ 'isApplicable' => false, 'message' => __('Product not found.', 'fluent-cart'), ]; } return static::evaluateProductEligibility($productModel, $coupon); } /** * Verdict for one already-loaded product/coupon pair, so a caller checking several * coupons at once hydrates the product and the coupon rows a single time. * * @param \FluentCart\App\Models\Product $productModel * @param \FluentCart\App\Models\Coupon $coupon * @return array */ public static function evaluateProductEligibility($productModel, $coupon): array { $productId = $productModel->getKey(); $couponCode = $coupon->code; $conditions = $coupon->conditions; $excludedCategories = static::normalizeConditionList(Arr::get($conditions, 'excluded_categories')); $includedCategories = static::normalizeConditionList(Arr::get($conditions, 'included_categories')); $excludedProducts = static::normalizeConditionList(Arr::get($conditions, 'excluded_products')); $includedProducts = static::normalizeConditionList(Arr::get($conditions, 'included_products')); $product = $productModel->toArray(); $productCategories = array_map(function ($productCategories) { return $productCategories['term_taxonomy_id']; }, $product['wp_terms']); if (!empty($includedProducts) && !in_array($productId, $includedProducts)) { if ($includedCategories !== null && !empty(array_intersect($includedCategories, $productCategories))) { return [ 'isApplicable' => true, ]; } return [ 'isApplicable' => false, ]; } if (empty($excludedCategories) && empty($excludedProducts) && empty($includedCategories) && empty($includedProducts)) { return [ 'isApplicable' => true ]; } if (empty($productCategories) && !in_array($productId, $excludedProducts)) { return [ 'isApplicable' => true ]; } // Check if the product is in the excluded products list if ($excludedProducts !== null && in_array($productId, $excludedProducts)) { return [ 'isApplicable' => false, ]; } // Check if the product belongs to any included product list if ($includedProducts !== null && in_array($productId, $includedProducts)) { return [ 'isApplicable' => true, ]; } /** * Error message when a coupon conflicts with a product. * * %1$s - Product Title (e.g., "Product A") * %2$s - Coupon Code (e.g., "DISCOUNT10") * * This string is shown when a coupon conflicts with a product, and the user * needs to remove the coupon first. */ $message = sprintf( /* translators: %1$s: Product Title, %2$s: Coupon Code */ __('%1$s conflicts with %2$s coupon. Remove the coupon first.', 'fluent-cart'), $product['post_title'], // %1$s: Product Title (e.g., "Product A") $couponCode // %2$s: Coupon Code (e.g., "DISCOUNT10") ); // Check if the product belongs to any excluded categories if ($excludedCategories != null && array_intersect($excludedCategories, $productCategories)) { return [ 'isApplicable' => false, 'message' => $message ]; } //check if the product belongs to any included categories if ($includedCategories !== null && empty(array_intersect($includedCategories, $productCategories))) { return [ 'isApplicable' => false, 'message' => __('This coupon can not be applied to some items in your cart', 'fluent-cart'), ]; } return [ 'isApplicable' => true, ]; } public static function getCouponApplicableItemsWithLineTotal($coupon, $modifiedOrderItems) { $items = $modifiedOrderItems; $origin = null; $applicableTotalWithItems = []; // Initialize the array for storing item totals $applicableTotal = 0; foreach ($items as $item) { $isEligible = self::checkProductEligibility($item['post_id'], $coupon, $origin); if ($isEligible['isApplicable'] === true && !isset($item['object_type'])) { if ($item['other_info']['payment_type'] === 'subscription' && $item['other_info']['manage_setup_fee'] === 'yes' && $item['other_info']['signup_fee'] > 0) { if ($item['other_info']['setup_fee_per_item'] === 'yes') { $setup_fee = $item['other_info']['signup_fee'] * $item['quantity']; } else { $setup_fee = $item['other_info']['signup_fee']; } } if (isset($item['other_info']['manage_setup_fee']) === 'yes') { $lineTotal = $item['discounted_price'] * $item['quantity'] + $setup_fee; } else { $lineTotal = $item['discounted_price'] * $item['quantity']; } $applicableTotalWithItems[$item['post_id']] = $lineTotal; // Store the total for this item $applicableTotal += $lineTotal; } } return [ 'lineTotalWithItems' => $applicableTotalWithItems, 'applicableTotal' => $applicableTotal, 'orderItems' => $items, ]; } public static function updateCouponStatus($coupon = []) { if (empty($coupon)) { return; } $startDate = $coupon->start_date; $endDate = $coupon->end_date; $status = $coupon->status; $now = DateTime::gmtNow(); $startDateTime = (!empty($startDate) && $startDate !== '0000-00-00 00:00:00') ? DateTime::anyTimeToGmt($startDate) : null; $endDateTime = (!empty($endDate) && $endDate !== '0000-00-00 00:00:00') ? DateTime::anyTimeToGmt($endDate) : null; // Mark expired if end date passed if ($endDateTime && $endDateTime < $now) { if ($status !== 'expired') { $coupon->setStatus('expired'); $coupon->save(); } return; } // Set status to scheduled if start date is in the future if ($startDateTime && $startDateTime > $now && !in_array($status, ['disabled', 'scheduled'])) { $coupon->setStatus('scheduled'); $coupon->save(); return; } // Activate if start date passed and status is not already active/disabled if ($startDateTime && $startDateTime <= $now && !in_array($status, ['disabled', 'active'])) { $coupon->setStatus('active'); $coupon->save(); } } }