diff --git a/config/discount-rules.php b/config/discount-rules.php new file mode 100644 index 000000000..86897f513 --- /dev/null +++ b/config/discount-rules.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/Webkul/Admin/src/Resources/views/promotions/cart-rule/edit.blade.php b/packages/Webkul/Admin/src/Resources/views/promotions/cart-rule/edit.blade.php index 9236e653b..cb07e16f2 100644 --- a/packages/Webkul/Admin/src/Resources/views/promotions/cart-rule/edit.blade.php +++ b/packages/Webkul/Admin/src/Resources/views/promotions/cart-rule/edit.blade.php @@ -569,6 +569,8 @@ onSubmit: function (e) { if (this.conditions_list.length != 0) { + this.conditions_list.push({'criteria': this.match_criteria}); + this.all_conditions = JSON.stringify(this.conditions_list); } diff --git a/packages/Webkul/Checkout/src/Cart.php b/packages/Webkul/Checkout/src/Cart.php index fc42a4823..e8d596450 100755 --- a/packages/Webkul/Checkout/src/Cart.php +++ b/packages/Webkul/Checkout/src/Cart.php @@ -12,8 +12,6 @@ use Webkul\Tax\Repositories\TaxCategoryRepository; use Webkul\Checkout\Models\CartPayment; use Webkul\Customer\Repositories\WishlistRepository; use Webkul\Customer\Repositories\CustomerAddressRepository; -use Webkul\Discount\Repositories\CartRuleRepository as CartRule; -use Webkul\Discount\Helpers\Discount; use Webkul\Product\Helpers\Price; /** @@ -81,16 +79,6 @@ class Cart { */ protected $customerAddress; - /** - * CartRule Repository instance - */ - protected $cartRule; - - /** - * Discount helper instance - */ - protected $discount; - /** * Suppress the session flash messages */ @@ -125,8 +113,6 @@ class Cart { TaxCategoryRepository $taxCategory, WishlistRepository $wishlist, CustomerAddressRepository $customerAddress, - CartRule $cartRule, - Discount $discount, Price $price ) { @@ -146,10 +132,6 @@ class Cart { $this->customerAddress = $customerAddress; - $this->cartRule = $cartRule; - - $this->discount = $discount; - $this->price = $price; $this->suppressFlash = false; @@ -1235,95 +1217,4 @@ class Cart { } } } - - public function applyCoupon($code) - { - $result = $this->discount->applyCouponAbleRule($code); - - return $result; - } - - public function applyNonCoupon() - { - $result = $this->discount->applyNonCouponAbleRule(); - - return $result; - } - - /** - * Removes discount from the cart and calls collect totals - * - * @return void - */ - public function clearDiscount() - { - $cartItems = $this->getCart()->items; - - foreach($cartItems as $item) { - $item->update([ - 'coupon_code' => NULL, - 'discount_percent' => 0, - 'discount_amount' => 0, - 'base_discount_amount' => 0 - ]); - } - - $this->getCart()->update([ - 'coupon_code' => NULL, - 'discount_amount' => 0, - 'base_discount_amount' => 0 - ]); - - return true; - } - - public function removeCoupon() - { - $result = $this->discount->removeCoupon(); - - return $result; - } - - public function leastWorthItem() - { - $cart = $this->getCart(); - $leastValue = 999999999999; - $leastSubTotal = []; - - foreach ($cart->items as $item) { - if ($item->price < $leastValue) { - $leastValue = $item->price; - $leastSubTotal = [ - 'id' => $item->id, - 'total' => $item->total, - 'base_total' => $leastValue, - 'quantity' => $item->quantity, - 'price' => $item->price - ]; - } - } - - return $leastSubTotal; - } - - public function maxWorthItem() - { - $cart = $this->getCart(); - $maxValue = 0; - $maxSubTotal = []; - - foreach ($cart->items as $item) { - if ($item->base_total > $maxValue) { - $maxValue = $item->total; - $maxSubTotal = [ - 'id' => $item->id, - 'total' => $item->total, - 'base_total' => $maxValue, - 'quantity' => $item->quantity - ]; - } - } - - return $maxSubTotal; - } } \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Actions/Action.php b/packages/Webkul/Discount/src/Actions/Action.php new file mode 100644 index 000000000..053a3e7ea --- /dev/null +++ b/packages/Webkul/Discount/src/Actions/Action.php @@ -0,0 +1,8 @@ +action_type; // action type used + $disc_threshold = $rule->disc_threshold; // atleast quantity by default 1 --> may be omitted in near future + $disc_amount = $rule->disc_amount; // value of discount + $disc_quantity = $rule->disc_quantity; //max quantity allowed to be discounted + + $amountDiscounted = 0; + $leastWorthItem = $this->leastWorthItem(); + $realQty = $leastWorthItem['quantity']; + + if ($cart->items_qty >= $disc_threshold) { + $amountDiscounted = $disc_amount; + + if ($realQty > $disc_quantity) { + $amountDiscounted = $amountDiscounted * $disc_quantity; + } + + if ($amountDiscounted > $leastWorthItem['price']) { + $amountDiscounted = $leastWorthItem['price']; + } + } + + $report['discount'] = $amountDiscounted; + $report['formatted_discount'] = core()->formatPrice($amountDiscounted, $cart->cart_currency_code); + + return $report; + } +} \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Actions/FixedAmount.php b/packages/Webkul/Discount/src/Actions/FixedAmount.php new file mode 100644 index 000000000..4f5d713f3 --- /dev/null +++ b/packages/Webkul/Discount/src/Actions/FixedAmount.php @@ -0,0 +1,42 @@ +action_type; // action type used + $disc_threshold = $rule->disc_threshold; // atleast quantity by default 1 --> may be omitted in near future + $disc_amount = $rule->disc_amount; // value of discount + $disc_quantity = $rule->disc_quantity; //max quantity allowed to be discounted + + $amountDiscounted = 0; + + $realQty = $item['quantity']; + + if ($cart >= $disc_threshold) { + $amountDiscounted = $disc_amount; + + if ($realQty > $disc_quantity) { + $amountDiscounted = $amountDiscounted * $disc_quantity; + } + + if ($amountDiscounted > $item['price']) { + $amountDiscounted = $item['price']; + } + } + + $report['discount'] = $amountDiscounted; + $report['formatted_discount'] = core()->formatPrice($amountDiscounted, $cart->cart_currency_code); + + return $report; + } +} \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Actions/PercentOfProduct.php b/packages/Webkul/Discount/src/Actions/PercentOfProduct.php new file mode 100644 index 000000000..080de8b8e --- /dev/null +++ b/packages/Webkul/Discount/src/Actions/PercentOfProduct.php @@ -0,0 +1,35 @@ +disc_threshold; + $disc_amount = $rule->disc_amount; + $disc_quantity = $rule->disc_quantity; + + $realQty = $item['quantity']; + + if ($cart >= $disc_threshold) { + $amountDiscounted = $item['price'] * ($disc_amount / 100); + + if ($realQty > $disc_quantity) { + $amountDiscounted = $amountDiscounted * $disc_quantity; + } + + if ($amountDiscounted > $item['price']) { + $amountDiscounted = $item['price']; + } + } + + $report['discount'] = $amountDiscounted; + $report['formatted_discount'] = core()->formatPrice($amountDiscounted, $cart->cart_currency_code); + + return $report; + } +} \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Config/discount-rules.php b/packages/Webkul/Discount/src/Config/discount-rules.php new file mode 100644 index 000000000..40c4d59fa --- /dev/null +++ b/packages/Webkul/Discount/src/Config/discount-rules.php @@ -0,0 +1,7 @@ + 'Webkul\Discount\Actions\PercentOfProduct', + 'fixed_amount' => 'Webkul\Discount\Actions\FixedAmount', + 'buy_a_get_b' => 'Webkul\Discount\Actions\BuyAGetB' +]; \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Config/rule-conditions.php b/packages/Webkul/Discount/src/Config/rule-conditions.php index cc226fa11..0a4c705cc 100644 --- a/packages/Webkul/Discount/src/Config/rule-conditions.php +++ b/packages/Webkul/Discount/src/Config/rule-conditions.php @@ -35,7 +35,7 @@ return [ 'percent_of_product' => 'Percentage of product', 'fixed_amount' => 'Apply as fixed amount', 'buy_a_get_b' => 'Get B amount back', - // 'fixed_amount_cart' => 'Fixed amount for whole cart' + 'fixed_amount_cart' => 'Whole cart gets discounted' ], 'validation' => [ diff --git a/packages/Webkul/Discount/src/Helpers/CouponAbleRule.php b/packages/Webkul/Discount/src/Helpers/CouponAbleRule.php new file mode 100644 index 000000000..f1d22647d --- /dev/null +++ b/packages/Webkul/Discount/src/Helpers/CouponAbleRule.php @@ -0,0 +1,153 @@ +guard('customer')->check()) { + $rules = $this->cartRule->findWhere([ + 'use_coupon' => 1, + 'status' => 1 + ]); + } else { + $rules = $this->cartRule->findWhere([ + 'use_coupon' => 1, + 'is_guest' => 1, + 'status' => 1 + ]); + } + + $applicableRule = null; + + foreach($rules as $rule) { + if ($rule->use_coupon && ($rule->coupons->code == $code)) { + $applicableRule = $rule; + + break; + } + } + + $applicability = $this->checkApplicability($applicableRule); + + if ($applicability) { + $item = $this->leastWorthItem(); + + $actionInstance = new $this->rules[$rule->action_type]; + + $impact = $actionInstance->calculate($applicableRule, $item, $cart); + + $ifAlreadyApplied = $this->cartRuleCart->findWhere([ + 'cart_id' => $cart->id, + 'cart_rule_id' => $rule->id + ]); + + if ($ifAlreadyApplied->count() == 1) { + return false; + } + + $ifAlreadyApplied = $this->cartRuleCart->findWhere([ + 'cart_id' => $cart->id, + ]); + + if ($ifAlreadyApplied->count() == 0) { + $this->save($applicableRule); + + return $impact; + } + + $alreadyAppliedRule = $ifAlreadyApplied->first()->cart_rule; + + if ($alreadyAppliedRule->priority < $rule->priority) { + return false; + } else if ($alreadyAppliedRule->priority == $applicableRule->priority) { + // tie breaker case + + // end other rules + if ($alreadyAppliedRule->end_other_rules) { + return false; + } + + $actionInstance = new $this->rules[$alreadyAppliedRule->action_type]; + + $alreadyAppliedRuleImpact = $actionInstance->calculate($alreadyAppliedRule, $item, $cart); + + if ($alreadyAppliedRule['discount'] > $impact['discount']) { + return false; + } else if ($alreadyAppliedRule['discount'] < $impact['discount']) { + $this->save($applicableRule); + + return $impact; + } else { + // least id case + if ($applicableRule->id < $alreadyAppliedRule->id) { + return $impact; + } + } + } else { + $this->save($applicableRule); + + return $impact; + } + } else { + return false; + } + } + + /** + * Removes the already applied coupon on the current cart instance + * + * @return boolean + */ + public function remove() + { + dd('removing coupon'); + $cart = Cart::getCart(); + + $existingRule = $this->cartRuleCart->findWhere([ + 'cart_id' => $cart->id + ]); + + if ($existingRule->count()) { + if ($existingRule->first()->delete()) { + foreach ($cart->items as $item) { + if ($item->discount_amount > 0) { + $item->update([ + 'discount_amount' => 0, + 'base_discount_amount' => 0, + 'discount_percent' => 0, + 'coupon_code' => NULL + ]); + } + } + + $cart->update([ + 'coupon_code' => NULL, + 'discount_amount' => 0, + 'base_discount_amount' => 0 + ]); + + Cart::collectTotals(); + + return true; + } else { + return false; + } + } else { + return false; + } + } +} \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Helpers/Discount.php b/packages/Webkul/Discount/src/Helpers/Discount.php index ccd18c4a6..d6faa429f 100644 --- a/packages/Webkul/Discount/src/Helpers/Discount.php +++ b/packages/Webkul/Discount/src/Helpers/Discount.php @@ -5,9 +5,9 @@ namespace Webkul\Discount\Helpers; use Webkul\Discount\Repositories\CartRuleRepository as CartRule; use Webkul\Discount\Repositories\CartRuleCartRepository as CartRuleCart; use Carbon\Carbon; -use Arr; +use Cart; -class Discount +abstract class Discount { /** * To hold the cart rule repository instance @@ -24,6 +24,11 @@ class Discount */ protected $endRuleActive = false; + /** + * To hold the rule classes + */ + protected $rules; + /** * disable coupon */ @@ -32,436 +37,95 @@ class Discount public function __construct(CartRule $cartRule, CartRuleCart $cartRuleCart) { $this->cartRule = $cartRule; - $this->endRuleActive = false; + $this->cartRuleCart = $cartRuleCart; + + $this->rules = config('discount-rules'); } /** - * Applies the non couponable rule on the current cart - * - * @return mixed + * Abstract method apply */ - public function applyNonCouponAbleRule() + abstract public function apply($code); + + /** + * Checks whether coupon is getting applied on current cart instance or not + * + * @return boolean + */ + public function checkApplicability($rule) { $cart = \Cart::getCart(); - $previousRule = $this->cartRuleCart->findWhere([ - 'cart_id' => $cart->id - ]); + $timeBased = false; - if ($previousRule->count()) { - $previousRule = $previousRule->first()->cart_rule; - - if ($previousRule->use_coupon) { - - return 'false'; + // time based constraints + if ($rule->starts_from != null && $rule->ends_till == null) { + if (Carbon::parse($rule->starts_from) < now()) { + $timeBased = true; + } + } else if ($rule->starts_from == null && $rule->ends_till != null) { + if (Carbon::parse($rule->ends_till) > now()) { + $timeBased = true; + } + } else if ($rule->starts_from != null && $rule->ends_till != null) { + if (Carbon::parse($rule->starts_from) < now() && now() < Carbon::parse($rule->ends_till)) { + $timeBased = true; } } else { - \Cart::clearDiscount(); + $timeBased = true; } + $channelBased = false; + + // channel based constraints + foreach ($rule->channels as $channel) { + if ($channel->channel_id == core()->getCurrentChannel()->id) { + $channelBased = true; + } + } + + $customerGroupBased = false; + + // customer groups based constraints if (auth()->guard('customer')->check()) { - $nonCouponAbleRules = $this->cartRule->findWhere([ - 'use_coupon' => 0, - 'status' => 1 - ]); - } else { - $nonCouponAbleRules = $this->cartRule->findWhere([ - 'use_coupon' => 0, - 'is_guest' => 1, - 'status' => 1 - ]); - } - - $canBeApplied = array(); - - // time based filter - foreach($nonCouponAbleRules as $rule) { - $report = $this->checkApplicability($rule); - $report['rule'] = $rule; - - $passed = 0; - if ($rule->starts_from != null && $rule->ends_till == null) { - if (Carbon::parse($rule->starts_from) < now()) { - $passed = 1; + foreach ($rule->customer_groups as $customer_group) { + if ($customer_group->customer_group_id == auth()->guard('customer')->user()->group->id) { + $customerGroupBased = true; } - } else if ($rule->starts_from == null && $rule->ends_till != null) { - if (Carbon::parse($rule->ends_till) > now()) { - $passed = 1; - } - } else if ($rule->starts_from != null && $rule->ends_till != null) { - if (Carbon::parse($rule->starts_from) < now() && now() < Carbon::parse($rule->ends_till)) { - $passed = 1; - } - } else { - $passed = 1; - } - - if ($passed) { - $report['used_coupon'] = false; - array_push($canBeApplied, $report); - } - } - - //min priority - $minPriority = collect($canBeApplied)->min('priority'); - - $canBeApplied = collect($canBeApplied)->where('priority', $minPriority); - - if (count($canBeApplied) > 1) { - $maxDiscount = collect($canBeApplied)->max('discount'); - - $canBeApplied = collect($canBeApplied)->where('discount', $maxDiscount); - - $leastId = 999999999999; - if (count($canBeApplied) > 1) { - foreach($canBeApplied as $rule) { - if ($rule['rule']->id < $leastId) { - $leastId = $rule['rule']->id; - } - } - - // fighting the edge case for non couponable discount rule with least id or older rule - foreach($canBeApplied as $rule) { - if ($rule['rule']->id == $leastId) { - $rule['used_coupon'] = false; - - foreach (\Cart::getCart()->items as $item) { - if ($item->id == $itemId['item_id']) { - $item->update([ - 'discount_amount' => array_first($canBeApplied)['discount'], - 'base_discount_amount' => array_first($canBeApplied)['discount'] - ]); - - break; - } - } - - // check customer_groups - if (auth()->guard('customer')->check()) { - foreach($rule['rule']->customer_groups as $customer_group) { - if (auth()->guard('customer')->user()->group->id == $customer_group->id) { - $this->save($rule['rule']); - - return $rule; - } - } - } else if ($rule['rule']->is_guest) { - $this->save($rule['rule']); - - return $rule; - } else { - return 'false'; - } - } - } - } - } - - if ($canBeApplied->count()) { - $itemId = array_first($canBeApplied); - - if (auth()->guard('customer')->check()) { - foreach(array_first($canBeApplied)['rule']->customer_groups as $customer_group) { - if (auth()->guard('customer')->user()->group->id == $customer_group->id) { - $this->save($rule['rule']); - - foreach (\Cart::getCart()->items as $item) { - if ($item->id == $itemId['item_id']) { - $item->update([ - 'discount_amount' => array_first($canBeApplied)['discount'], - 'base_discount_amount' => array_first($canBeApplied)['discount'] - ]); - - break; - } - } - - $this->save(array_first($canBeApplied)['rule']); - - return array_first($canBeApplied); - } - } - } else if (array_first($canBeApplied)['rule']->is_guest) { - foreach (\Cart::getCart()->items as $item) { - if ($item->id == $itemId['item_id']) { - $item->update([ - 'discount_amount' => array_first($canBeApplied)['discount'], - 'base_discount_amount' => array_first($canBeApplied)['discount'] - ]); - - break; - } - } - - $this->save(array_first($canBeApplied)['rule']); - - return array_first($canBeApplied); } } else { - return 'false'; - } - } - - /** - * Applies the couponable rule on the current cart - * - * @return mixed - */ - public function applyCouponAbleRule($code) - { - $cart = \Cart::getCart(); - - if (auth()->guard('customer')->check()) { - $couponAbleRules = $this->cartRule->findWhere([ - 'use_coupon' => 1, - 'status' => 1 - ]); - } else { - $couponAbleRules = $this->cartRule->findWhere([ - 'use_coupon' => 1, - 'is_guest' => 1, - 'status' => 1 - ]); - } - - foreach ($couponAbleRules as $couponAbleRule) { - if ($couponAbleRule->coupons->code == $code) { - $rule = $couponAbleRule; + if ($rule->is_guest) { + $customerGroupBased = true; } } - $useCouponable = false; - if (isset($rule)) { - $canBeApplied = array(); - - // time based filter - $report = $this->checkApplicability($rule); - $report['rule'] = $rule; - - $passed = 0; - if ($rule->starts_from != null && $rule->ends_till == null) { - if (Carbon::parse($rule->starts_from) < now()) { - $passed = 1; - } - } else if ($rule->starts_from == null && $rule->ends_till != null) { - if (Carbon::parse($rule->ends_till) > now()) { - $passed = 1; - } - } else if ($rule->starts_from != null && $rule->ends_till != null) { - if (Carbon::parse($rule->starts_from) < now() && now() < Carbon::parse($rule->ends_till)) { - $passed = 1; - } - } else { - $passed = 1; - } - - if ($passed) { - array_push($canBeApplied, $report); - - $useCouponable = true; - $alreadyAppliedRule = $this->cartRuleCart->findWhere([ - 'cart_id' => \Cart::getCart()->id - ]); - - if ($alreadyAppliedRule->count() && ! ($alreadyAppliedRule->first()->cart_rule->priority < $canBeApplied[0]['rule']->priority)) { - unset($report); - - $alreadyAppliedRule = $alreadyAppliedRule->first()->cart_rule; - - // analyze impact - if ($alreadyAppliedRule->id != $rule->id) { - $report = $this->checkApplicability($alreadyAppliedRule); - $report['rule'] = $alreadyAppliedRule; - - array_push($canBeApplied, $report); - - //min priority - $minPriority = collect($canBeApplied)->min('priority'); - //min priority rule - $canBeApplied = collect($canBeApplied)->where('priority', $minPriority); - - if (count($canBeApplied) > 1) { - $maxDiscount = collect($canBeApplied)->max('discount'); - - $canBeApplied = collect($canBeApplied)->where('discount', $maxDiscount); - - $leastId = 999999999999; - if (count($canBeApplied) > 1) { - foreach($canBeApplied as $rule) { - if ($rule['rule']->id < $leastId) { - $leastId = $rule['rule']->id; - } - } - - // fighting the edge case for couponable discount rule - foreach($canBeApplied as $rule) { - if ($rule['rule']->id == $leastId) { - if($rule['rule']->use_coupon) { - $useCouponable = true; - } - } - } - } else { - if (array_first($canBeApplied)['rule']->use_coupon) { - $useCouponable = true; - } - } - } else if (count($canBeApplied)) { - if (array_first($canBeApplied)['rule']->use_coupon) { - $useCouponable = true; - } - } - - if ($alreadyAppliedRule->end_other_rules) { - $useCouponable = false; - } - } else { - $report = $this->checkApplicability($rule); - - array_push($canBeApplied, $report); - } - } - } - } - - if ($useCouponable) { - $report = $this->checkApplicability($rule); - $report['rule'] = $rule; - $report['used_coupon'] = $useCouponable; - - $itemId = array_first($canBeApplied); - - foreach ($cart->items as $item) { - if ($item->id == $itemId['item_id']) { - $item->update([ - 'discount_amount' => array_first($canBeApplied)['discount'], - 'base_discount_amount' => array_first($canBeApplied)['discount'], - 'coupon_code' => $rule->coupons->code - ]); - - $cart->update([ - 'coupon_code' => $rule->coupons->code - ]); - - \Cart::collectTotals(); - - $report['grand_total'] = core()->currency(\Cart::getCart()->grand_total); - - break; - } - } - - // saves the rule in cart rule cart - $this->save($rule); - - return $report; - } else { - return null; - } - } - - /** - * This function checks whether the rule is getting applied on the current cart or not - * - * @return mixed - */ - public function checkApplicability($rule = null) - { - $cart = \Cart::getCart(); - $report = array(); - $result = 0; + $conditionsBased = true; //check conditions if ($rule->conditions != null) { $conditions = json_decode(json_decode($rule->conditions)); - $test_mode = config('pricerules.test_mode.0'); - if ($test_mode == config('pricerules.test_mode.0')) { - $result = $this->testIfAllConditionAreTrue($conditions, $cart); - } else if ($test_mode == config('pricerules.test_mode.1')) { - $result = $this->testIfAllConditionAreFalse($conditions, $cart); - } else if ($test_mode == config('pricerules.test_mode.2')) { - $result = $this->testIfAnyConditionIsTrue($conditions, $cart); - } else if ($test_mode == config('pricerules.test_mode.3')) { - $result = $this->testIfAnyConditionIsFalse($conditions, $cart); + $test_mode = array_last($conditions); + + if ($test_mode->criteria == 'any_is_true') { + $conditionsBased = $this->testIfAnyConditionIsTrue($conditions, $cart); + } + + if ($test_mode->criteria == 'all_are_true') { + $conditionsBased = $this->testIfAllConditionAreTrue($conditions, $cart); } } - if ($result) { - $report['conditions'] = true; + if ($timeBased && $channelBased && $customerGroupBased && $conditionsBased) { + return true; } else { - if ($rule->conditions == null) - $report['conditions'] = true; - else - $report['conditions'] = false; + return false; } - - //check endrule - if ($rule->end_other_rules) { - $report['end_other_rules'] = true; - } else { - $report['end_other_rules'] = false; - } - - //calculate discount amount - $action_type = $rule->action_type; // action type used - $disc_threshold = $rule->disc_threshold; // atleast quantity by default 1 --> may be omitted in near future - $disc_amount = $rule->disc_amount; // value of discount - $disc_quantity = $rule->disc_quantity; //max quantity allowed to be discounted - - $amountDiscounted = 0; - $leastWorthItem = \Cart::leastWorthItem(); - $realQty = $leastWorthItem['quantity']; - - if ($cart->items_qty >= $disc_threshold) { - if ($action_type == config('pricerules.cart.validation.0')) { - $amountDiscounted = $leastWorthItem['price'] * ($disc_amount / 100); - - if ($realQty > $disc_quantity) { - $amountDiscounted = $amountDiscounted * $disc_quantity; - } - - if ($amountDiscounted > $leastWorthItem['price']) { - $amountDiscounted = $leastWorthItem['price']; - } - } else if ($action_type == config('pricerules.cart.validation.1')) { - $amountDiscounted = $disc_amount; - - if ($realQty > $disc_quantity) { - $amountDiscounted = $amountDiscounted * $disc_quantity; - } - - if ($amountDiscounted > $leastWorthItem['price']) { - $amountDiscounted = $leastWorthItem['price']; - } - } else if ($action_type == config('pricerules.cart.validation.2')) { - $amountDiscounted = $disc_amount; - - if ($realQty > $disc_quantity) { - $amountDiscounted = $amountDiscounted * $disc_quantity; - } - - if ($amountDiscounted > $leastWorthItem['price']) { - $amountDiscounted = $leastWorthItem['price']; - } - } - } - - $report['item_id'] = $leastWorthItem['id']; - $report['item_price'] = $leastWorthItem['total']; - $report['discount'] = $amountDiscounted; - $report['action'] = $action_type; - $report['formatted_discount'] = core()->formatPrice($amountDiscounted, $cart->cart_currency_code); - $report['grand_total'] = $cart->grand_total - $amountDiscounted; - $report['formatted_grand_total'] = core()->formatPrice($cart->grand_total - $amountDiscounted, $cart->cart_currency_code); - $report['priority'] = $rule->priority; - - return $report; } /** - * Save the rule in the cart rule cart + * Save the rule in the CartRule for current cart instance * * @return boolean */ @@ -480,6 +144,8 @@ class Discount 'cart_rule_id' => $rule->id ]); + $this->updateCartItemAndCart($rule); + return true; } } else { @@ -488,6 +154,8 @@ class Discount 'cart_rule_id' => $rule->id ]); + $this->updateCartItemAndCart($rule); + return true; } @@ -495,48 +163,142 @@ class Discount } /** - * Removes the couponable rule from the current cart and cart rule cart + * Removes any cart rule from the current cart instance + * + * @return void + */ + public function clearDiscount() + { + $cart = Cart::getCart(); + + $cartItems = $cart->items; + + foreach($cartItems as $item) { + $item->update([ + 'coupon_code' => NULL, + 'discount_percent' => 0, + 'discount_amount' => 0, + 'base_discount_amount' => 0 + ]); + } + + $cart->update([ + 'coupon_code' => NULL, + 'discount_amount' => 0, + 'base_discount_amount' => 0 + ]); + + return true; + } + + /** + * To find the least worth item in current cart instance + * + * @return array + */ + public function leastWorthItem() + { + $cart = Cart::getCart(); + + $leastValue = 999999999999; + $leastWorthItem = []; + + foreach ($cart->items as $item) { + if ($item->price < $leastValue) { + $leastValue = $item->price; + $leastWorthItem = [ + 'id' => $item->id, + 'price' => $item->price, + 'base_price' => $item->base_price, + 'quantity' => $item->quantity + ]; + } + } + + return $leastWorthItem; + } + + /** + * Update discount for least worth item + */ + public function updateCartItemAndCart($rule) + { + $cart = Cart::getCart(); + + $leastWorthItem = $this->leastWorthItem(); + + $actionInstance = new $this->rules[$rule->action_type]; + + $impact = $actionInstance->calculate($rule, $leastWorthItem, $cart); + + foreach ($cart->items as $item) { + if ($item->id == $leastWorthItem['id']) { + if ($rule->action_type == 'percent_of_product') { + $item->update([ + 'discount_percent' => $rule->discount_amount, + 'discount_amount' => $impact['discount'], + 'base_discount_amount' => $impact['discount'] + ]); + } else { + $item->update([ + 'discount_amount' => $impact['discount'], + 'base_discount_amount' => $impact['discount'] + ]); + } + + // save coupon if rule has it + if ($rule->use_coupon) { + $coupon = $rule->coupons->code; + + $item->update([ + 'coupon_code' => $coupon + ]); + + $cart->update([ + 'coupon_code' => $coupon + ]); + } + + break; + } + } + + return true; + } + + /** + * To find the max worth item in current cart instance + * + * @return Array + */ + public function maxWorthItem() + { + $cart = Cart::getCart(); + + $maxValue = 0; + $maxWorthItem = []; + + foreach ($cart->items as $item) { + if ($item->base_total > $maxValue) { + $maxValue = $item->total; + $maxWorthItem = [ + 'id' => $item->id, + 'price' => $item->price, + 'base_price' => $item->base_price, + 'quantity' => $item->quantity + ]; + } + } + + return $maxWorthItem; + } + + /** + * Checks the rule against the current cart instance whether rule conditions are applicable + * or not * * @return boolean */ - public function removeCoupon() - { - $cart = \Cart::getCart(); - - $existingRule = $this->cartRuleCart->findWhere([ - 'cart_id' => $cart->id - ]); - - if ($existingRule->count()) { - if ($existingRule->first()->delete()) { - foreach ($cart->items as $item) { - if ($item->discount_amount > 0) { - $item->update([ - 'discount_amount' => 0, - 'base_discount_amount' => 0, - 'discount_percent' => 0, - 'coupon_code' => NULL - ]); - } - } - - $cart->update([ - 'coupon_code' => NULL, - 'discount_amount' => 0, - 'base_discount_amount' => 0 - ]); - - \Cart::collectTotals(); - - return true; - } else { - return false; - } - } else { - return false; - } - } - protected function testIfAllConditionAreTrue($conditions, $cart) { array_pop($conditions); @@ -616,4 +378,88 @@ class Discount return $result; } + + /** + * Checks the rule against the current cart instance whether rule conditions are applicable + * or not + * + * @return boolean + */ + protected function testIfAnyConditionIsTrue($conditions, $cart) { + array_pop($conditions); + + $result = true; + + $shipping_address = $cart->getShippingAddressAttribute() ?? ''; + + $shipping_method = $cart->shipping_method ?? ''; + $shipping_country = $shipping_address->country ?? ''; + $shipping_state = $shipping_address->state ?? ''; + $shipping_postcode = $shipping_address->postcode ?? ''; + $shipping_city = $shipping_address->city ?? ''; + + $payment_method = $cart->payment->method ?? ''; + $sub_total = $cart->base_sub_total; + + $total_items = $cart->items_qty; + $total_weight = 0; + + foreach($cart->items as $item) { + $total_weight = $total_weight + $item->base_total_weight; + } + + foreach ($conditions as $condition) { + $actual_value = ${$condition->attribute}; + $test_value = $condition->value; + $test_condition = $condition->condition; + + if ($condition->type == 'numeric' || $condition->type == 'string' || $condition->type == 'text') { + if ($test_condition == '=') { + if ($actual_value != $test_value) { + $result = false; + + break; + } + } else if ($test_condition == '>=') { + if (! ($actual_value >= $test_value)) { + $result = false; + + break; + } + } else if ($test_condition == '<=') { + if (! ($actual_value <= $test_value)) { + $result = false; + + break; + } + } else if ($test_condition == '>') { + if (! ($actual_value > $test_value)) { + $result = false; + + break; + } + } else if ($test_condition == '<') { + if (! ($actual_value < $test_value)) { + $result = false; + + break; + } + } else if ($test_condition == '{}') { + if (! str_contains($actual_value, $test_value)) { + $result = false; + + break; + } + } else if ($test_condition == '!{}') { + if (str_contains($actual_value, $test_value)) { + $result = false; + + break; + } + } + } + } + + return $result; + } } \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Helpers/NonCouponAbleRule.php b/packages/Webkul/Discount/src/Helpers/NonCouponAbleRule.php new file mode 100644 index 000000000..f5a929d44 --- /dev/null +++ b/packages/Webkul/Discount/src/Helpers/NonCouponAbleRule.php @@ -0,0 +1,172 @@ +guard('customer')->check()) { + $rules = $this->cartRule->findWhere([ + 'use_coupon' => 0, + 'status' => 1 + ]); + } else { + $rules = $this->cartRule->findWhere([ + 'use_coupon' => 0, + 'is_guest' => 1, + 'status' => 1 + ]); + } + + // time based filter + foreach($rules as $rule) { + $applicability = $this->checkApplicability($rule); + + if ($applicability) { + $item = $this->leastWorthItem(); + + $actionInstance = new $this->rules[$rule->action_type]; + + $impact = $actionInstance->calculate($rule, $item, $cart); + + array_push($applicableRules, [ + 'rule' => $rule, + 'impact' => $impact + ]); + } + } + + if (count($applicableRules) > 1) { + // priority criteria + $prioritySorted = array(); + $leastPriority = 999999999999; + + foreach ($applicableRules as $applicableRule) { + if ($applicableRule['rule']->priority <= $leastPriority) { + $leastPriority = $applicableRule['rule']->priority; + array_push($prioritySorted, $applicableRule); + } + } + + // end rule criteria with end rule + $endRules = array(); + + if (count($prioritySorted) > 1) { + foreach ($prioritySorted as $prioritySortedRule) { + if ($prioritySortedRule['rule']->end_other_rules) { + array_push($endRules, $prioritySortedRule); + } + } + } else { + $this->save(array_first($prioritySorted)['rule']); + + return $prioritySorted; + } + + // max impact criteria with end rule + $maxImpacts = array(); + + if (count($endRules)) { + $this->endRuleActive = true; + + if (count($endRules) == 1) { + $this->save(array_first($endRules)['rule']); + + return $endRules; + } + + $maxImpact = 0; + + foreach ($endRules as $endRule) { + if ($endRule['impact']['discount'] >= $maxImpact) { + $maxImpact = $endRule['impact']['discount']; + + array_push($maxImpacts, $endRule); + } + } + + // oldest and max impact criteria + $leastId = 999999999999; + $leastIdImpactIndex = 0; + + if (count($maxImpacts) > 1) { + foreach ($maxImpacts as $index => $maxImpactRule) { + if ($maxImpactRule['rule']->id < $leastId) { + $leastId = $maxImpactRule['rule']->id; + + $leastIdImpactIndex = $index; + } + } + + $this->save($maxImpacts[$leastIdImpactIndex]['rule']); + + return $maxImpacts[$leastIdImpactIndex]; + } else { + $this->save(array_first($maxImpacts)['rule']); + + return $maxImpacts; + } + } + + if (count($prioritySorted) > 1) { + $maxImpact = 0; + + foreach ($prioritySorted as $prioritySortedRule) { + if ($prioritySortedRule['impact']['discount'] >= $maxImpact) { + $maxImpact = $prioritySortedRule['impact']['discount']; + + array_push($maxImpacts, $prioritySortedRule); + } + } + + // oldest and max impact criteria + $leastId = 999999999999; + $leastIdImpactIndex = 0; + + if (count($maxImpacts) > 1) { + foreach ($maxImpacts as $index => $maxImpactRule) { + if ($maxImpactRule['rule']->id < $leastId) { + $leastId = $maxImpactRule['rule']->id; + + $leastIdImpactIndex = $index; + } + } + + $this->save($maxImpacts[$leastIdImpactIndex]['rule']); + + return $maxImpacts[$leastIdImpactIndex]; + } else { + $this->save(array_first($maxImpacts)['rule']); + + return $maxImpacts; + } + } else { + $this->save(array_first($prioritySorted)['rule']); + + return $prioritySorted; + } + } else if (count($applicableRules) == 1) { + $rule = array_first($applicableRules)['rule']; + + $this->save($applicableRules); + + return array_first($applicableRules)['impact']; + } else { + return null; + } + } +} \ No newline at end of file diff --git a/packages/Webkul/Discount/src/Http/Controllers/CartRuleController.php b/packages/Webkul/Discount/src/Http/Controllers/CartRuleController.php index eca1b1c27..269efc1f8 100644 --- a/packages/Webkul/Discount/src/Http/Controllers/CartRuleController.php +++ b/packages/Webkul/Discount/src/Http/Controllers/CartRuleController.php @@ -312,9 +312,6 @@ class CartRuleController extends Controller // unset request token from $data unset($data['_token']); - // condition validation rules from config - $types = config('price_rules.cart.validations'); - // set rule uasge limit $data['usage_limit'] = 0; @@ -374,25 +371,15 @@ class CartRuleController extends Controller // encode php array to json for actions $data['actions'] = json_encode($data['actions']); - // prepare conditions from data for json conditions + // Prepares conditions from all conditions if (! isset($data['all_conditions']) || $data['all_conditions'] == "[]" || $data['all_conditions'] == "") { $data['conditions'] = null; } else { - if (count(json_decode($data['all_conditions']))) { - $conditions = json_decode($data['all_conditions']); - - foreach($conditions as $condition) { - if (isset($condition->criteria)) { - $condition->criteria = $data['match_criteria']; - } - } - - $data['conditions'] = json_encode($conditions); - - unset($data['match_criteria']); - } + $data['conditions'] = json_encode($data['all_conditions']); } + unset($data['match_criteria']); + // unset all_conditions from conditions unset($data['all_conditions']); diff --git a/packages/Webkul/Discount/src/Providers/DiscountServiceProvider.php b/packages/Webkul/Discount/src/Providers/DiscountServiceProvider.php index a51ba75b1..9c4e5fd5c 100644 --- a/packages/Webkul/Discount/src/Providers/DiscountServiceProvider.php +++ b/packages/Webkul/Discount/src/Providers/DiscountServiceProvider.php @@ -35,5 +35,9 @@ class DiscountServiceProvider extends ServiceProvider $this->mergeConfigFrom( dirname(__DIR__) . '/Config/rule-conditions.php', 'pricerules' ); + + $this->mergeConfigFrom( + dirname(__DIR__) . '/Config/discount-rules.php', 'discount-rules' + ); } } \ No newline at end of file diff --git a/packages/Webkul/Shop/src/Http/Controllers/CartController.php b/packages/Webkul/Shop/src/Http/Controllers/CartController.php index 48ffb82bf..6577e11bf 100755 --- a/packages/Webkul/Shop/src/Http/Controllers/CartController.php +++ b/packages/Webkul/Shop/src/Http/Controllers/CartController.php @@ -9,7 +9,6 @@ use Webkul\Checkout\Repositories\CartItemRepository; use Webkul\Product\Repositories\ProductRepository; use Webkul\Customer\Repositories\CustomerRepository; use Webkul\Customer\Repositories\WishlistRepository; -use Webkul\Discount\Repositories\CartRuleCartRepository as CartRuleCart; use Illuminate\Support\Facades\Event; use Cart; @@ -34,11 +33,15 @@ class CartController extends Controller * @param $productView */ protected $_config; + protected $cart; + protected $cartItem; + protected $customer; + protected $product; - protected $cartRuleCart; + protected $suppressFlash = false; /** @@ -53,8 +56,7 @@ class CartController extends Controller CartItemRepository $cartItem, CustomerRepository $customer, ProductRepository $product, - WishlistRepository $wishlist, - CartRuleCart $cartRuleCart + WishlistRepository $wishlist ) { @@ -70,8 +72,6 @@ class CartController extends Controller $this->wishlist = $wishlist; - $this->cartRuleCart = $cartRuleCart; - $this->_config = request('_config'); } @@ -252,70 +252,4 @@ class CartController extends Controller return redirect()->back(); } } - - /** - * To apply coupon rules - */ - public function applyCoupon() - { - $this->validate(request(), [ - 'code' => 'string|required' - ]); - - $code = request()->input('code'); - - $result = Cart::applyCoupon($code); - - if ($result != null) { - return response()->json([ - 'success' => true, - 'message' => trans('shop::app.checkout.onepage.total.coupon-applied'), - 'result' => $result - ]); - } else { - return response()->json([ - 'success' => false, - 'message' => trans('shop::app.checkout.onepage.total.cannot-apply-coupon'), - 'result' => null - ]); - } - - return $result; - } - - /** - * Fetch the non couponable rule - */ - public function getNonCouponAbleRule() - { - $cart = Cart::getCart(); - $nonCouponAbleRules = Cart::applyNonCoupon(); - - return $nonCouponAbleRules; - } - - /** - * To remove the currently active - * couponable rule - */ - public function removeCoupon() - { - $result = Cart::removeCoupon(); - - if ($result) { - return response()->json([ - 'success' => true, - 'message' => trans('admin::app.promotion.status.coupon-removed'), - 'data' => [ - 'grand_total' => core()->currency(Cart::getCart()->grand_total) - ] - ]); - } else { - return response()->json([ - 'success' => false, - 'message' => trans('admin::app.promotion.status.coupon-remove-failed'), - 'data' => null - ]); - } - } } \ No newline at end of file diff --git a/packages/Webkul/Shop/src/Http/Controllers/OnepageController.php b/packages/Webkul/Shop/src/Http/Controllers/OnepageController.php index 587bd800b..44114bc60 100755 --- a/packages/Webkul/Shop/src/Http/Controllers/OnepageController.php +++ b/packages/Webkul/Shop/src/Http/Controllers/OnepageController.php @@ -3,20 +3,22 @@ namespace Webkul\Shop\Http\Controllers; use Webkul\Shop\Http\Controllers\Controller; -use Illuminate\Http\Request; -use Illuminate\Http\Response; -use Auth; use Webkul\Checkout\Facades\Cart; use Webkul\Shipping\Facades\Shipping; use Webkul\Payment\Facades\Payment; -use Webkul\Discount\Repositories\CartRuleCartRepository as CartRuleCart; use Webkul\Checkout\Http\Requests\CustomerAddressForm; use Webkul\Sales\Repositories\OrderRepository; +use Webkul\Discount\Helpers\CouponAbleRule as Coupon; +use Webkul\Discount\Helpers\NonCouponAbleRule as NonCoupon; +use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Auth; /** * Chekout controller for the customer and guest for placing order * - * @author Jitendra Singh + * @author Jitendra Singh @jitendra-webkul + * @author Prashant Singh @prashant-webkul * @copyright 2018 Webkul Software Pvt Ltd (http://www.webkul.com) */ class OnepageController extends Controller @@ -28,13 +30,6 @@ class OnepageController extends Controller */ protected $orderRepository; - /** - * CartRuleCartRepository object - * - * @var array - */ - protected $cartRuleCart; - /** * Contains route related configuration * @@ -42,17 +37,31 @@ class OnepageController extends Controller */ protected $_config; + /** + * + * CouponAbleRule instance + */ + protected $coupon; + + /** + * + * NoncouponAbleRule instance + */ + protected $nonCoupon; + /** * Create a new controller instance. * * @param \Webkul\Attribute\Repositories\OrderRepository $orderRepository * @return void */ - public function __construct(OrderRepository $orderRepository, CartRuleCart $cartRuleCart) + public function __construct(OrderRepository $orderRepository, Coupon $coupon, NonCoupon $nonCoupon) { - $this->orderRepository = $orderRepository; + $this->coupon = $coupon; - $this->cartRuleCart = $cartRuleCart; + $this->nonCoupon = $nonCoupon; + + $this->orderRepository = $orderRepository; $this->_config = request('_config'); } @@ -67,7 +76,9 @@ class OnepageController extends Controller if (Cart::hasError()) return redirect()->route('shop.checkout.cart.index'); - Cart::applyNonCoupon(); + $this->nonCoupon->apply(); + + Cart::collectTotals(); return view($this->_config['view'])->with('cart', Cart::getCart()); } @@ -99,7 +110,7 @@ class OnepageController extends Controller $data['billing']['address1'] = implode(PHP_EOL, array_filter($data['billing']['address1'])); $data['shipping']['address1'] = implode(PHP_EOL, array_filter($data['shipping']['address1'])); - Cart::applyNonCoupon(); + $this->nonCoupon->apply(); Cart::collectTotals(); @@ -118,7 +129,7 @@ class OnepageController extends Controller { $shippingMethod = request()->get('shipping_method'); - Cart::applyNonCoupon(); + $this->nonCoupon->apply(); Cart::collectTotals(); @@ -137,7 +148,7 @@ class OnepageController extends Controller { $payment = request()->get('payment'); - Cart::applyNonCoupon(); + $this->nonCoupon->apply(); Cart::collectTotals(); @@ -208,6 +219,9 @@ class OnepageController extends Controller { $cart = Cart::getCart(); + // extra validation check if some the conditions is changed for the coupons but not using it now + // $this->nonCoupon->apply(); + if (! $cart->shipping_address) { throw new \Exception(trans('Please check shipping address.')); } @@ -224,4 +238,75 @@ class OnepageController extends Controller throw new \Exception(trans('Please specify payment method.')); } } + + /** + * To apply couponable rule requested + * + * @return JSON + */ + public function applyCoupon() + { + $this->validate(request(), [ + 'code' => 'string|required' + ]); + + $code = request()->input('code'); + + $result = $this->coupon->apply($code); + + if ($result != null) { + return response()->json([ + 'success' => true, + 'message' => trans('shop::app.checkout.onepage.total.coupon-applied'), + 'result' => $result + ]); + } else { + return response()->json([ + 'success' => false, + 'message' => trans('shop::app.checkout.onepage.total.cannot-apply-coupon'), + 'result' => null + ]); + } + + return $result; + } + + /** + * Applies non couponable rule if present + * + * @return Void + */ + public function applyNonCouponAbleRule() + { + $cart = Cart::getCart(); + $nonCouponAbleRules = Cart::applyNonCoupon(); + + return $nonCouponAbleRules; + } + + /** + * Initiates the removal of couponable cart rule + * + * @return Void + */ + public function removeCoupon() + { + $result = $this->coupon->remove(); + + if ($result) { + return response()->json([ + 'success' => true, + 'message' => trans('admin::app.promotion.status.coupon-removed'), + 'data' => [ + 'grand_total' => core()->currency(Cart::getCart()->grand_total) + ] + ], 200); + } else { + return response()->json([ + 'success' => false, + 'message' => trans('admin::app.promotion.status.coupon-remove-failed'), + 'data' => null + ], 422); + } + } } \ No newline at end of file diff --git a/packages/Webkul/Shop/src/Http/routes.php b/packages/Webkul/Shop/src/Http/routes.php index ac044d289..ebd7e5e31 100755 --- a/packages/Webkul/Shop/src/Http/routes.php +++ b/packages/Webkul/Shop/src/Http/routes.php @@ -39,9 +39,9 @@ Route::group(['middleware' => ['web', 'locale', 'theme', 'currency']], function 'view' => 'shop::checkout.cart.index' ])->name('shop.checkout.cart.index'); - Route::post('checkout/check/coupons', 'Webkul\Shop\Http\Controllers\CartController@applyCoupon')->name('shop.checkout.check.coupons'); + Route::post('checkout/check/coupons', 'Webkul\Shop\Http\Controllers\OnepageController@applyCoupon')->name('shop.checkout.check.coupons'); - Route::post('checkout/remove/coupon', 'Webkul\Shop\Http\Controllers\CartController@removeCoupon')->name('shop.checkout.remove.coupon'); + Route::post('checkout/remove/coupon', 'Webkul\Shop\Http\Controllers\OnepageController@removeCoupon')->name('shop.checkout.remove.coupon'); //Cart Items Add Route::post('checkout/cart/add/{id}', 'Webkul\Shop\Http\Controllers\CartController@add')->defaults('_config', [ diff --git a/packages/Webkul/Ui/src/DataGrid/DataGrid.php b/packages/Webkul/Ui/src/DataGrid/DataGrid.php index b72968c19..d1ed77667 100644 --- a/packages/Webkul/Ui/src/DataGrid/DataGrid.php +++ b/packages/Webkul/Ui/src/DataGrid/DataGrid.php @@ -241,7 +241,7 @@ abstract class DataGrid $this->operators[$condition], '%'.$filter_value.'%' ); - } else if($this->enableFilterMap && !isset($this->filterMap[$columnName])) { + } else if ($this->enableFilterMap && ! isset($this->filterMap[$columnName])) { $collection->where( $columnName, $this->operators[$condition], @@ -264,7 +264,7 @@ abstract class DataGrid $this->operators[$condition], $filter_value ); - } else if($this->enableFilterMap && !isset($this->filterMap[$columnName])) { + } else if ($this->enableFilterMap && ! isset($this->filterMap[$columnName])) { $collection->whereDate( $columnName, $this->operators[$condition],