{{ __('Updating cart') }}
{{ __('Please wait while totals are refreshed') }}
@php
/* Pre-load promotion data for all gift items (one pass, no duplicate queries) */
$promotionMap = [];
$qualifyingProductIds = [];
$discountedLineOffers = [];
$cartContents = Cart::instance('cart')->content();
foreach ($cartContents as $_ci) {
if (!(bool)($_ci->options['is_free_gift'] ?? false)) continue;
$pid = (int)($_ci->options['promotion_id'] ?? 0);
if (!$pid || isset($promotionMap[$pid])) continue;
$promo = \Botble\Ecommerce\Models\Discount::with('products')->find($pid);
if (!$promo) continue;
$targetSpecific = in_array((string)$promo->target, [
\Botble\Ecommerce\Enums\DiscountTargetEnum::SPECIFIC_PRODUCT,
\Botble\Ecommerce\Enums\DiscountTargetEnum::PRODUCT_VARIANT,
]);
$promoProductIds = $targetSpecific
? $promo->products->pluck('id')->map(fn($id) => (int)$id)->toArray()
: [];
$promotionMap[$pid] = [
'title' => $promo->title,
'product_ids' => $promoProductIds,
'all_items' => !$targetSpecific,
];
$qualifyingProductIds = array_merge($qualifyingProductIds, $promoProductIds);
}
$qualifyingProductIds = array_unique($qualifyingProductIds);
$buyXGetYPercentPromotions = \Botble\Ecommerce\Models\Discount::query()
->where('type', \Botble\Ecommerce\Enums\DiscountTypeEnum::PROMOTION)
->where('type_option', \Botble\Ecommerce\Enums\DiscountTypeOptionEnum::BUY_X_GET_Y_DISCOUNT)
->where('start_date', '<=', \Carbon\Carbon::now())
->where(fn($query) => $query->whereNull('end_date')->orWhere('end_date', '>=', \Carbon\Carbon::now()))
->with('products')
->get();
foreach ($buyXGetYPercentPromotions as $promotion) {
$buyProductIds = $promotion->products->pluck('id')->map(fn($id) => (int)$id)->all();
if (empty($buyProductIds)) continue;
$buyQuantity = max((int)$promotion->product_quantity, 1);
$buyCartQuantity = 0;
foreach ($cartContents as $ci) {
if ((bool)($ci->options['is_free_gift'] ?? false)) continue;
$ciProduct = $products->find($ci->id);
if (!$ciProduct) continue;
if (
in_array((int)$ci->id, $buyProductIds, true) ||
in_array((int)$ciProduct->original_product->id, $buyProductIds, true)
) {
$buyCartQuantity += (int)$ci->qty;
}
}
$eligibleQty = (int)floor($buyCartQuantity / $buyQuantity);
if ($eligibleQty < 1) continue;
foreach (\Illuminate\Support\Arr::wrap(data_get($promotion->apply_conditions, 'free_products', [])) as $offerProduct) {
$offerProductId = (int)data_get($offerProduct, 'product_id');
$discountPercent = min(max((float)data_get($offerProduct, 'discount_percent', $promotion->value), 1), 100);
$remainingQty = $eligibleQty;
foreach ($cartContents as $ci) {
if ($remainingQty < 1 || (bool)($ci->options['is_free_gift'] ?? false)) continue;
$ciProduct = $products->find($ci->id);
if (!$ciProduct) continue;
if (
(int)$ci->id !== $offerProductId &&
(int)$ciProduct->original_product->id !== $offerProductId
) {
continue;
}
$discountQty = min((int)$ci->qty, $remainingQty);
$unitPrice = EcommerceHelper::isTaxEnabled() && $ci->priceTax > 0 ? (float)$ci->priceTax : (float)$ci->price;
$discountAmount = $unitPrice * ($discountPercent / 100) * $discountQty;
if ($discountAmount > 0) {
$discountedLineOffers[$ci->rowId] = [
'amount' => ($discountedLineOffers[$ci->rowId]['amount'] ?? 0) + $discountAmount,
'percent' => $discountPercent,
'discount_qty' => ($discountedLineOffers[$ci->rowId]['discount_qty'] ?? 0) + $discountQty,
'type' => 'buy_x_get_y_percent',
];
}
$remainingQty -= $discountQty;
}
}
}
$quantityBasedPromotions = \Botble\Ecommerce\Models\Discount::query()
->where('type', \Botble\Ecommerce\Enums\DiscountTypeEnum::PROMOTION)
->where('type_option', \Botble\Ecommerce\Enums\DiscountTypeOptionEnum::QUANTITY_BASED)
->where('start_date', '<=', \Carbon\Carbon::now())
->where(fn($query) => $query->whereNull('end_date')->orWhere('end_date', '>=', \Carbon\Carbon::now()))
->with(['products', 'productCollections', 'productCategories'])
->get();
$cartRawTotal = (float) Cart::instance('cart')->rawSubTotal();
foreach ($quantityBasedPromotions as $promotion) {
if (
$promotion->target === \Botble\Ecommerce\Enums\DiscountTargetEnum::MINIMUM_ORDER_AMOUNT &&
(float) $promotion->min_order_price > $cartRawTotal
) {
continue;
}
$eligibleCartItems = $cartContents
->reject(fn($ci) => (bool)($ci->options['is_free_gift'] ?? false))
->filter(function ($ci) use ($promotion, $products) {
$ciProduct = $products->find($ci->id);
return match ($promotion->target) {
\Botble\Ecommerce\Enums\DiscountTargetEnum::SPECIFIC_PRODUCT,
\Botble\Ecommerce\Enums\DiscountTargetEnum::PRODUCT_VARIANT => in_array(
(int)$ci->id,
$promotion->products->pluck('id')->map(fn($id) => (int)$id)->all(),
true
) || ($ciProduct && in_array(
(int)$ciProduct->original_product->id,
$promotion->products->pluck('id')->map(fn($id) => (int)$id)->all(),
true
)),
\Botble\Ecommerce\Enums\DiscountTargetEnum::PRODUCT_COLLECTIONS => $ciProduct && $ciProduct->original_product
->productCollections()
->pluck('ec_product_collections.id')
->intersect($promotion->productCollections->pluck('id'))
->isNotEmpty(),
\Botble\Ecommerce\Enums\DiscountTargetEnum::PRODUCT_CATEGORIES => $ciProduct && $ciProduct->original_product
->categories()
->pluck('ec_product_categories.id')
->intersect($promotion->productCategories->pluck('id'))
->isNotEmpty(),
default => true,
};
});
$requiredQuantity = max((int)$promotion->product_quantity, 1);
$discountPercent = min(max((float)$promotion->value, 0), 100);
if ($discountPercent <= 0 || $eligibleCartItems->sum('qty') < $requiredQuantity) {
continue;
}
foreach ($eligibleCartItems as $ci) {
$unitPrice = EcommerceHelper::isTaxEnabled() && $ci->priceTax > 0 ? (float)$ci->priceTax : (float)$ci->price;
$discountAmount = $unitPrice * (int)$ci->qty * ($discountPercent / 100);
if ($discountAmount > 0) {
$discountedLineOffers[$ci->rowId] = [
'amount' => ($discountedLineOffers[$ci->rowId]['amount'] ?? 0) + $discountAmount,
'percent' => $discountedLineOffers[$ci->rowId]['percent'] ?? $discountPercent,
'discount_qty' => (int) $ci->qty,
'type' => $discountedLineOffers[$ci->rowId]['type'] ?? 'quantity_based',
];
}
}
}
/* Sort: regular items first, free gifts at the bottom */
$cartContents = $cartContents->sortBy(fn($ci) => (bool)($ci->options['is_free_gift'] ?? false));
@endphp
@foreach($cartContents as $key => $cartItem)
@php
$product = $products->find($cartItem->id);
$isFreeGift = (bool) ($cartItem->options['is_free_gift'] ?? false);
$giftMessage = $cartItem->options['gift_message'] ?? __('Free gift with offer');
@endphp
@if (!empty($product))
@if ($isFreeGift)
{{-- Free gift row --}}
@php
$giftPromoId = (int)($cartItem->options['promotion_id'] ?? 0);
$giftPromoInfo = $promotionMap[$giftPromoId] ?? null;
$giftUnitPrice = (float)($cartItem->options['gift_original_price'] ?? 0);
/* Find qualifying non-gift cart items for this promotion */
$qualifyingCartItems = collect();
if ($giftPromoInfo) {
$qualifyingCartItems = $cartContents
->reject(fn($ci) => (bool)($ci->options['is_free_gift'] ?? false))
->filter(function($ci) use ($giftPromoInfo) {
if ($giftPromoInfo['all_items']) return true;
return in_array((int)$ci->id, $giftPromoInfo['product_ids']);
});
}
@endphp
{{-- Qualifying products header --}}
@if ($qualifyingCartItems->isNotEmpty())
🛒 {{ __('Free with your purchase of:') }}
@foreach($qualifyingCartItems as $qci)
@php $qProduct = $products->find($qci->id); @endphp
@if($qProduct)
@endif
@endforeach
@endif
{{-- Gift product row --}}
@if (is_plugin_active('marketplace') && $product->original_product->store->id)
{{ __('Sold by') }}: {{ $product->original_product->store->name }}
@endif
@if (!empty($cartItem->options['attributes']))
{{ $cartItem->options['attributes'] }}
@endif
{{ __('Qty') }}: {{ $cartItem->qty }}
{{ __('FREE') }}
@if ($giftUnitPrice > 0)
{{ format_price($giftUnitPrice) }}
@endif
@else
{{-- Regular cart row --}}
@php
$isQualifying = in_array((int)$cartItem->id, $qualifyingProductIds)
|| collect($promotionMap)->contains('all_items', true);
$lineUnitPrice = EcommerceHelper::isTaxEnabled()
? (float) $cartItem->priceTax
: (float) $cartItem->price;
$lineTotal = $lineUnitPrice * (int) $cartItem->qty;
$lineOffer = $discountedLineOffers[$cartItem->rowId] ?? null;
$lineOfferAmount = $lineOffer ? min((float)$lineOffer['amount'], (float)$lineTotal) : 0;
$lineOfferPercent = $lineOffer ? (float) $lineOffer['percent'] : 0;
$lineDiscountQty = $lineOffer ? min((int) ($lineOffer['discount_qty'] ?? $cartItem->qty), (int) $cartItem->qty) : 0;
$shouldSplitOfferLine = $lineOfferAmount > 0
&& ($lineOffer['type'] ?? null) === 'buy_x_get_y_percent'
&& $lineDiscountQty > 0
&& $lineDiscountQty < (int) $cartItem->qty;
$lineRegularQty = (int) $cartItem->qty - $lineDiscountQty;
$lineDiscountedTotal = ($lineUnitPrice * $lineDiscountQty) - $lineOfferAmount;
@endphp
@if ($shouldSplitOfferLine)
@if (is_plugin_active('marketplace') && $product->original_product->store->id)
{{ __('Sold by') }}: {{ $product->original_product->store->name }}
@endif
@if (!empty($cartItem->options['attributes']))
{{ $cartItem->options['attributes'] }}
@endif
@if (!empty($cartItem->options['options']))
{!! render_product_options_info($cartItem->options['options'], $product, false) !!}
@endif
@if (!empty($cartItem->options['extras']) && is_array($cartItem->options['extras']))
@foreach($cartItem->options['extras'] as $option)
@if (!empty($option['key']) && !empty($option['value']) && strtolower($option['key']) !== 'price')
{{ $option['key'] }}: {{ $option['value'] }}
@endif
@endforeach
@endif
{{ __('Qty') }}: {{ $lineDiscountQty }}
{{ format_price($lineUnitPrice * $lineDiscountQty) }}
{{ format_price($lineDiscountedTotal) }}
{{ __('Offer') }}
{{ $lineOfferPercent }}% {{ __('off') }}
@if (is_plugin_active('marketplace') && $product->original_product->store->id)
{{ __('Sold by') }}: {{ $product->original_product->store->name }}
@endif
@if (!empty($cartItem->options['attributes']))
{{ $cartItem->options['attributes'] }}
@endif
@if (!empty($cartItem->options['options']))
{!! render_product_options_info($cartItem->options['options'], $product, false) !!}
@endif
@if (!empty($cartItem->options['extras']) && is_array($cartItem->options['extras']))
@foreach($cartItem->options['extras'] as $option)
@if (!empty($option['key']) && !empty($option['value']) && strtolower($option['key']) !== 'price')
{{ $option['key'] }}: {{ $option['value'] }}
@endif
@endforeach
@endif
{{ format_price($lineUnitPrice * $lineRegularQty) }}
@else
@if (is_plugin_active('marketplace') && $product->original_product->store->id)
{{ __('Sold by') }}: {{ $product->original_product->store->name }}
@endif
@if (!empty($cartItem->options['attributes']))
{{ $cartItem->options['attributes'] }}
@endif
@if (!empty($cartItem->options['options']))
{!! render_product_options_info($cartItem->options['options'], $product, false) !!}
@endif
@if (!empty($cartItem->options['extras']) && is_array($cartItem->options['extras']))
@foreach($cartItem->options['extras'] as $option)
@if (!empty($option['key']) && !empty($option['value']) && strtolower($option['key']) !== 'price')
{{ $option['key'] }}: {{ $option['value'] }}
@endif
@endforeach
@endif
@php
$lineTotalAfterOffer = max((float)$lineTotal - $lineOfferAmount, 0);
@endphp
@if ($lineOfferAmount > 0)
{{ format_price($lineTotal) }}
{{ format_price($lineTotalAfterOffer) }}
{{ (float)$lineOffer['percent'] }}% {{ __('off') }}
@else
{{ format_price($lineTotal) }}
@endif
@endif
@endif
@endif
@endforeach