582 lines
18 KiB
PHP
582 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Webkul\CartRule\Helpers;
|
|
|
|
use Carbon\Carbon;
|
|
use Webkul\Checkout\Facades\Cart;
|
|
use Webkul\Rule\Helpers\Validator;
|
|
use Webkul\Checkout\Models\CartItem;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Webkul\CartRule\Repositories\CartRuleRepository;
|
|
use Webkul\Customer\Repositories\CustomerGroupRepository;
|
|
use Webkul\CartRule\Repositories\CartRuleCouponRepository;
|
|
use Webkul\CartRule\Repositories\CartRuleCustomerRepository;
|
|
use Webkul\CartRule\Repositories\CartRuleCouponUsageRepository;
|
|
|
|
class CartRule
|
|
{
|
|
/**
|
|
* @var array
|
|
*/
|
|
protected $itemTotals = [];
|
|
|
|
/**
|
|
* Create a new helper instance.
|
|
*
|
|
* @param \Webkul\CartRule\Repositories\CartRuleRepository $cartRuleRepository
|
|
* @param \Webkul\CartRule\Repositories\CartRuleCouponRepository $cartRuleCouponRepository
|
|
* @param \Webkul\CartRule\Repositories\CartRuleCouponUsageRepository $cartRuleCouponUsageRepository
|
|
* @param \Webkul\CartRule\Repositories\CartRuleCustomerRepository $cartRuleCustomerRepository
|
|
* @param \Webkul\Customer\Repositories\CustomerGroupRepository $customerGroupRepository
|
|
* @param \Webkul\Rule\Helpers\Validator $validator
|
|
*
|
|
* @return void
|
|
*/
|
|
public function __construct(
|
|
protected CartRuleRepository $cartRuleRepository,
|
|
protected CartRuleCouponRepository $cartRuleCouponRepository,
|
|
protected CartRuleCouponUsageRepository $cartRuleCouponUsageRepository,
|
|
protected CartRuleCustomerRepository $cartRuleCustomerRepository,
|
|
protected CustomerGroupRepository $customerGroupRepository,
|
|
protected Validator $validator
|
|
)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Collect discount on cart
|
|
*
|
|
* @param \Webkul\Cart\Contracts\Cart $cart
|
|
* @return void
|
|
*/
|
|
public function collect($cart)
|
|
{
|
|
$appliedCartRuleIds = [];
|
|
|
|
$this->calculateCartItemTotals($cart, $cart->items);
|
|
|
|
foreach ($cart->items as $item) {
|
|
$itemCartRuleIds = $this->process($cart, $item);
|
|
|
|
$appliedCartRuleIds = array_merge($appliedCartRuleIds, $itemCartRuleIds);
|
|
|
|
if (
|
|
$item->children()->count()
|
|
&& $item->product->getTypeInstance()->isChildrenCalculated()
|
|
) {
|
|
$this->divideDiscount($item);
|
|
}
|
|
}
|
|
|
|
$cart->applied_cart_rule_ids = implode(',', array_unique($appliedCartRuleIds, SORT_REGULAR));
|
|
$cart->save();
|
|
$cart->refresh();
|
|
|
|
$this->processShippingDiscount($cart);
|
|
|
|
$this->processFreeShippingDiscount($cart);
|
|
|
|
if (! $this->checkCouponCode($cart)) {
|
|
cart()->removeCouponCode();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns cart rules
|
|
*
|
|
* @param \Webkul\Cart\Contracts\Cart $cart
|
|
* @return \Illuminate\Support\Collection
|
|
*/
|
|
public function getCartRules($cart)
|
|
{
|
|
$staticCartRules = new class() {
|
|
public static $cartRules;
|
|
public static $cartID;
|
|
};
|
|
|
|
if (
|
|
$staticCartRules::$cartID === $cart->id
|
|
&& $staticCartRules::$cartRules
|
|
) {
|
|
return $staticCartRules::$cartRules;
|
|
}
|
|
|
|
$staticCartRules::$cartID = $cart->id;
|
|
|
|
$customerGroupId = null;
|
|
|
|
if (auth()->guard()->check()) {
|
|
$customerGroupId = auth()->guard()->user()->customer_group_id;
|
|
} else {
|
|
$customerGuestGroup = $this->customerGroupRepository->getCustomerGuestGroup();
|
|
|
|
if ($customerGuestGroup) {
|
|
$customerGroupId = $customerGuestGroup->id;
|
|
}
|
|
}
|
|
|
|
$cartRules = $this->getCartRuleQuery($customerGroupId, core()->getCurrentChannel()->id);
|
|
|
|
$staticCartRules::$cartRules = $cartRules;
|
|
|
|
return $cartRules;
|
|
}
|
|
|
|
/**
|
|
* Check if cart rule can be applied
|
|
*
|
|
* @param \Webkul\Cart\Contracts\Cart $cart
|
|
* @param \Webkul\CartRule\Contracts\CartRule $rule
|
|
* @return bool
|
|
*/
|
|
public function canProcessRule($cart, $rule): bool
|
|
{
|
|
if ($rule->coupon_type) {
|
|
if (strlen($cart->coupon_code)) {
|
|
/** @var \Webkul\CartRule\Models\CartRule $rule */
|
|
// Laravel relation is used instead of repository for performance
|
|
// reasons (cart_rule_coupon-relation is pre-loaded by self::getCartRuleQuery())
|
|
$coupon = $rule->cart_rule_coupon()->where('code', $cart->coupon_code)->first();
|
|
|
|
if (
|
|
$coupon
|
|
&& $coupon->code === $cart->coupon_code
|
|
) {
|
|
if (
|
|
$coupon->usage_limit
|
|
&& $coupon->times_used >= $coupon->usage_limit
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
$cart->customer_id
|
|
&& $coupon->usage_per_customer
|
|
) {
|
|
$couponUsage = $this->cartRuleCouponUsageRepository->findOneWhere([
|
|
'cart_rule_coupon_id' => $coupon->id,
|
|
'customer_id' => $cart->customer_id,
|
|
]);
|
|
|
|
if (
|
|
$couponUsage
|
|
&& $couponUsage->times_used >= $coupon->usage_per_customer
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ($rule->usage_per_customer) {
|
|
$ruleCustomer = $this->cartRuleCustomerRepository->findOneWhere([
|
|
'cart_rule_id' => $rule->id,
|
|
'customer_id' => $cart->customer_id,
|
|
]);
|
|
|
|
if (
|
|
$ruleCustomer
|
|
&& $ruleCustomer->times_used >= $rule->usage_per_customer
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Cart item discount calculation process
|
|
*
|
|
* @param \Webkul\Cart\Contracts\Cart $cart
|
|
* @param \Webkul\Checkout\Models\CartItem $item
|
|
* @return array
|
|
*/
|
|
public function process($cart, CartItem $item): array
|
|
{
|
|
$item->discount_percent = 0;
|
|
$item->discount_amount = 0;
|
|
$item->base_discount_amount = 0;
|
|
|
|
$appliedRuleIds = [];
|
|
|
|
foreach ($rules = $this->getCartRules($cart) as $rule) {
|
|
if (! $this->canProcessRule($cart, $rule)) {
|
|
continue;
|
|
}
|
|
|
|
if (! $this->validator->validate($rule, $item)) {
|
|
continue;
|
|
}
|
|
|
|
if ($rule->coupon_code) {
|
|
$item->coupon_code = $rule->coupon_code;
|
|
}
|
|
|
|
$quantity = $rule->discount_quantity ? min($item->quantity, $rule->discount_quantity) : $item->quantity;
|
|
|
|
$discountAmount = $baseDiscountAmount = 0;
|
|
|
|
switch ($rule->action_type) {
|
|
case 'by_percent':
|
|
$rulePercent = min(100, $rule->discount_amount);
|
|
|
|
$discountAmount = ($quantity * $item->price + $item->tax_amount - $item->discount_amount) * ($rulePercent / 100);
|
|
|
|
$baseDiscountAmount = ($quantity * $item->base_price + $item->base_tax_amount - $item->base_discount_amount) * ($rulePercent / 100);
|
|
|
|
if (
|
|
! $rule->discount_quantity
|
|
|| $rule->discount_quantity > $quantity
|
|
) {
|
|
$discountPercent = min(100, $item->discount_percent + $rulePercent);
|
|
|
|
$item->discount_percent = $discountPercent;
|
|
}
|
|
|
|
break;
|
|
|
|
case 'by_fixed':
|
|
$discountAmount = $quantity * core()->convertPrice($rule->discount_amount);
|
|
|
|
$baseDiscountAmount = $quantity * $rule->discount_amount;
|
|
|
|
break;
|
|
|
|
case 'cart_fixed':
|
|
if ($this->itemTotals[$rule->id]['total_items'] <= 1) {
|
|
$discountAmount = core()->convertPrice($rule->discount_amount);
|
|
|
|
$baseDiscountAmount = min($item->base_price * $quantity, $rule->discount_amount);
|
|
} else {
|
|
$discountRate = $item->base_price * $quantity / $this->itemTotals[$rule->id]['base_total_price'];
|
|
|
|
$maxDiscount = $rule->discount_amount * $discountRate;
|
|
|
|
$discountAmount = core()->convertPrice($maxDiscount);
|
|
|
|
$baseDiscountAmount = min($item->base_price * $quantity, $maxDiscount);
|
|
}
|
|
|
|
break;
|
|
|
|
case 'buy_x_get_y':
|
|
if (
|
|
! $rule->discount_step
|
|
|| $rule->discount_amount > $rule->discount_step
|
|
) {
|
|
break;
|
|
}
|
|
|
|
$buyAndDiscountQty = $rule->discount_step + $rule->discount_amount;
|
|
|
|
$qtyPeriod = floor($quantity / $buyAndDiscountQty);
|
|
|
|
$freeQty = $quantity - $qtyPeriod * $buyAndDiscountQty;
|
|
|
|
$discountQty = $qtyPeriod * $rule->discount_amount;
|
|
|
|
if ($freeQty > $rule->discount_step) {
|
|
$discountQty += $freeQty - $rule->discount_step;
|
|
}
|
|
|
|
$discountAmount = $discountQty * $item->price;
|
|
|
|
$baseDiscountAmount = $discountQty * $item->base_price;
|
|
|
|
break;
|
|
}
|
|
|
|
$item->discount_amount = min(
|
|
$item->discount_amount + $discountAmount,
|
|
$item->price * $quantity + $item->tax_amount
|
|
);
|
|
$item->base_discount_amount = min(
|
|
$item->base_discount_amount + $baseDiscountAmount,
|
|
$item->base_price * $quantity + $item->base_tax_amount
|
|
);
|
|
|
|
$appliedRuleIds[$rule->id] = $rule->id;
|
|
|
|
if ($rule->end_other_rules) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$item->applied_cart_rule_ids = implode(',', $appliedRuleIds);
|
|
|
|
$item->save();
|
|
|
|
return $appliedRuleIds;
|
|
}
|
|
|
|
/**
|
|
* Cart shipping discount calculation process
|
|
*
|
|
* @param \Webkul\Checkout\Contracts\Cart $cart
|
|
* @return void
|
|
*/
|
|
public function processShippingDiscount($cart)
|
|
{
|
|
if (! $selectedShipping = $cart->selected_shipping_rate) {
|
|
return;
|
|
}
|
|
|
|
$selectedShipping->discount_amount = 0;
|
|
$selectedShipping->base_discount_amount = 0;
|
|
|
|
$appliedRuleIds = [];
|
|
|
|
foreach ($this->getCartRules($cart) as $rule) {
|
|
if (! $this->canProcessRule($cart, $rule)) {
|
|
continue;
|
|
}
|
|
|
|
if (! $this->validator->validate($rule, $cart)) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
! $rule
|
|
|| ! $rule->apply_to_shipping
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$discountAmount = $baseDiscountAmount = 0;
|
|
|
|
switch ($rule->action_type) {
|
|
case 'by_percent':
|
|
$rulePercent = min(100, $rule->discount_amount);
|
|
|
|
$discountAmount = ($selectedShipping->price - $selectedShipping->discount_amount) * $rulePercent / 100;
|
|
|
|
$baseDiscountAmount = ($selectedShipping->base_price - $selectedShipping->base_discount_amount) * $rulePercent / 100;
|
|
|
|
break;
|
|
|
|
case 'by_fixed':
|
|
$discountAmount = core()->convertPrice($rule->discount_amount);
|
|
|
|
$baseDiscountAmount = $rule->discount_amount;
|
|
|
|
break;
|
|
}
|
|
|
|
$selectedShipping->discount_amount = min($selectedShipping->discount_amount + $discountAmount, $selectedShipping->price);
|
|
|
|
$selectedShipping->base_discount_amount = min(
|
|
$selectedShipping->base_discount_amount + $baseDiscountAmount,
|
|
$selectedShipping->base_price
|
|
);
|
|
|
|
$selectedShipping->save();
|
|
|
|
$appliedRuleIds[$rule->id] = $rule->id;
|
|
|
|
if ($rule->end_other_rules) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$selectedShipping->save();
|
|
|
|
$cartAppliedCartRuleIds = array_merge(explode(',', $cart->applied_cart_rule_ids), $appliedRuleIds);
|
|
|
|
$cartAppliedCartRuleIds = array_filter($cartAppliedCartRuleIds);
|
|
|
|
$cartAppliedCartRuleIds = array_unique($cartAppliedCartRuleIds);
|
|
|
|
$cart->applied_cart_rule_ids = implode(',', $cartAppliedCartRuleIds);
|
|
|
|
$cart->save();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Cart free shipping discount calculation process
|
|
*
|
|
* @param \Webkul\Checkout\Contracts\Cart $cart
|
|
* @return void
|
|
*/
|
|
public function processFreeShippingDiscount($cart)
|
|
{
|
|
if (! $selectedShipping = $cart->selected_shipping_rate) {
|
|
return;
|
|
}
|
|
|
|
$selectedShipping->discount_amount = 0;
|
|
|
|
$selectedShipping->base_discount_amount = 0;
|
|
|
|
$appliedRuleIds = [];
|
|
|
|
foreach ($cart->items->all() as $item) {
|
|
|
|
foreach ($this->getCartRules($cart) as $rule) {
|
|
|
|
if (! $this->canProcessRule($cart, $rule)) {
|
|
continue;
|
|
}
|
|
|
|
/* given CartItem instance to the validator */
|
|
if (! $this->validator->validate($rule, $item)) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
! $rule
|
|
|| ! $rule->free_shipping
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$selectedShipping->price = 0;
|
|
|
|
$selectedShipping->base_price = 0;
|
|
|
|
$selectedShipping->save();
|
|
|
|
$appliedRuleIds[$rule->id] = $rule->id;
|
|
|
|
if ($rule->end_other_rules) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
$cartAppliedCartRuleIds = array_merge(explode(',', $cart->applied_cart_rule_ids), $appliedRuleIds);
|
|
|
|
$cartAppliedCartRuleIds = array_filter($cartAppliedCartRuleIds);
|
|
|
|
$cartAppliedCartRuleIds = array_unique($cartAppliedCartRuleIds);
|
|
|
|
$cart->applied_cart_rule_ids = join(',', $cartAppliedCartRuleIds);
|
|
|
|
$cart->save();
|
|
}
|
|
|
|
/**
|
|
* Calculate cart item totals for each rule
|
|
*
|
|
* @param \Webkul\Cart\Contracts\Cart $cart
|
|
* @param \Illuminate\Support\Collecton $items
|
|
* @return \Webkul\Rule\Helpers\Validator
|
|
*/
|
|
public function calculateCartItemTotals($cart, $items)
|
|
{
|
|
foreach ($this->getCartRules($cart) as $rule) {
|
|
if ($rule->action_type == 'cart_fixed') {
|
|
$totalPrice = $totalBasePrice = $validCount = 0;
|
|
|
|
foreach ($items as $item) {
|
|
if (! $this->canProcessRule($cart, $rule)) {
|
|
continue;
|
|
}
|
|
|
|
if (! $this->validator->validate($rule, $item)) {
|
|
continue;
|
|
}
|
|
|
|
$quantity = $rule->discount_quantity ? min($item->quantity, $rule->discount_quantity) : $item->quantity;
|
|
|
|
$totalBasePrice += $item->base_price * $quantity;
|
|
|
|
$validCount++;
|
|
}
|
|
|
|
$this->itemTotals[$rule->id] = [
|
|
'base_total_price' => $totalBasePrice,
|
|
'total_items' => $validCount,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if coupon code is applied or not
|
|
*
|
|
* @param \Webkul\Cart\Contracts\Cart $cart
|
|
* @return bool
|
|
*/
|
|
public function checkCouponCode($cart): bool
|
|
{
|
|
if (! $cart->coupon_code) {
|
|
return true;
|
|
}
|
|
|
|
$coupons = $this->cartRuleCouponRepository->where(['code' => $cart->coupon_code])->get();
|
|
|
|
foreach ($coupons as $coupon) {
|
|
if (in_array($coupon->cart_rule_id, explode(',', $cart->applied_cart_rule_ids))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Divide discount amount to children
|
|
*
|
|
* @param \Webkul\Checkout\Contracts\CartItem $item
|
|
* @return void
|
|
*/
|
|
protected function divideDiscount($item)
|
|
{
|
|
foreach ($item->children as $child) {
|
|
$ratio = $item->base_total != 0 ? $child->base_total / $item->base_total : 0;
|
|
|
|
foreach (['discount_amount', 'base_discount_amount'] as $column) {
|
|
if (! $item->{$column}) {
|
|
continue;
|
|
}
|
|
|
|
$child->{$column} = round(($item->{$column} * $ratio), 4);
|
|
|
|
$child->save();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $customerGroupId
|
|
* @param $channelId
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Collection
|
|
*/
|
|
public function getCartRuleQuery($customerGroupId, $channelId): \Illuminate\Database\Eloquent\Collection
|
|
{
|
|
return $this->cartRuleRepository->scopeQuery(function ($query) use ($customerGroupId, $channelId) {
|
|
/** @var Builder $query */
|
|
return $query->leftJoin('cart_rule_customer_groups', 'cart_rules.id', '=',
|
|
'cart_rule_customer_groups.cart_rule_id')
|
|
->leftJoin('cart_rule_channels', 'cart_rules.id', '=', 'cart_rule_channels.cart_rule_id')
|
|
->where('cart_rule_customer_groups.customer_group_id', $customerGroupId)
|
|
->where('cart_rule_channels.channel_id', $channelId)
|
|
->where(function ($query1) {
|
|
/** @var Builder $query1 */
|
|
$query1->where('cart_rules.starts_from', '<=', Carbon::now()->format('Y-m-d H:m:s'))
|
|
->orWhereNull('cart_rules.starts_from');
|
|
})
|
|
->where(function ($query2) {
|
|
/** @var Builder $query2 */
|
|
$query2->where('cart_rules.ends_till', '>=', Carbon::now()->format('Y-m-d H:m:s'))
|
|
->orWhereNull('cart_rules.ends_till');
|
|
})
|
|
->with([
|
|
'cart_rule_customer_groups',
|
|
'cart_rule_channels',
|
|
'cart_rule_coupon'
|
|
])
|
|
->orderBy('sort_order', 'asc');
|
|
})->findWhere(['status' => 1]);
|
|
}
|
|
}
|