¨4.0.1¨

This commit is contained in:
¨NW¨
2023-12-03 14:07:47 +00:00
parent c08b36d1b6
commit f35052522d
1112 changed files with 43019 additions and 24987 deletions

View File

@@ -9,9 +9,9 @@ class CartClearController
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
* @return \Modules\Cart\Cart
*/
public function store()
public function store(): \Modules\Cart\Cart
{
Cart::clear();

View File

@@ -2,14 +2,18 @@
namespace Modules\Cart\Http\Controllers;
use Illuminate\Contracts\View\View;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\Foundation\Application;
class CartController
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return Application|Factory|View
*/
public function index()
public function index(): Application|Factory|View
{
return view('public.cart.index');
}

View File

@@ -3,15 +3,16 @@
namespace Modules\Cart\Http\Controllers;
use Modules\Cart\Facades\Cart;
use Illuminate\Support\Collection;
class CartCrossSellProductsController
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
* @return Collection
*/
public function index()
public function index(): Collection
{
return Cart::crossSellProducts();
}

View File

@@ -2,23 +2,43 @@
namespace Modules\Cart\Http\Controllers;
use Exception;
use Modules\Cart\Facades\Cart;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Routing\Controller;
use Modules\Coupon\Entities\Coupon;
use Modules\Coupon\Checkers\ValidCoupon;
use Modules\Coupon\Checkers\MaximumSpend;
use Modules\Coupon\Checkers\MinimumSpend;
use Modules\Coupon\Checkers\CouponExists;
use Modules\Coupon\Checkers\AlreadyApplied;
use Modules\Coupon\Checkers\ExcludedProducts;
use Modules\Coupon\Checkers\ApplicableProducts;
use Modules\Coupon\Checkers\ExcludedCategories;
use Modules\Coupon\Checkers\UsageLimitPerCoupon;
use Modules\Cart\Http\Middleware\CheckItemStock;
use Modules\Coupon\Checkers\ApplicableCategories;
use Modules\Coupon\Checkers\UsageLimitPerCustomer;
use Modules\Cart\Http\Requests\StoreCartItemRequest;
use Modules\Coupon\Exceptions\MaximumSpendException;
use Modules\Coupon\Exceptions\MinimumSpendException;
use Modules\Cart\Http\Middleware\CheckProductIsInStock;
use Modules\Cart\Http\Requests\UpdateCartItemRequest;
class CartItemController extends Controller
{
private $checkers = [
private array $checkers = [
CouponExists::class,
AlreadyApplied::class,
ValidCoupon::class,
MinimumSpend::class,
MaximumSpend::class,
ApplicableProducts::class,
ExcludedProducts::class,
ApplicableCategories::class,
ExcludedCategories::class,
UsageLimitPerCoupon::class,
UsageLimitPerCustomer::class,
];
/**
* Create a new controller instance.
*
@@ -26,54 +46,93 @@ class CartItemController extends Controller
*/
public function __construct()
{
$this->middleware(CheckProductIsInStock::class)->only(['store', 'update']);
$this->middleware(CheckItemStock::class)->only(['store', 'update']);
}
/**
* Store a newly created resource in storage.
*
* @param \Modules\Cart\Http\Requests\StoreCartItemRequest $request
* @return \Illuminate\Http\Response
* @param StoreCartItemRequest $request
*
* @return \Modules\Cart\Cart
*/
public function store(StoreCartItemRequest $request)
{
Cart::store($request->product_id, $request->qty, $request->options ?? []);
Cart::store(
$request->product_id,
$request->variant_id,
$request->qty,
$request->options ?? []
);
return Cart::instance();
}
/**
* Update the specified resource in storage.
*
* @param mixed $cartItemId
* @return \Illuminate\Http\Response
* @param UpdateCartItemRequest $request
* @param string $cartItemId
*
* @return \Modules\Cart\Cart
*/
public function update($cartItemId)
public function update(UpdateCartItemRequest $request, string $cartItemId)
{
Cart::updateQuantity($cartItemId, request('qty'));
try {
resolve(Pipeline::class)
->send(Cart::coupon())
->through($this->checkers)
->thenReturn();
} catch (MinimumSpendException | MaximumSpendException $e) {
Cart::removeCoupon();
$cartWithCoupon = null;
$couponCode = request()->query('coupon_code');
if ($couponCode) {
$coupon = Coupon::findByCode($couponCode);
try {
resolve(Pipeline::class)
->send($coupon)
->through($this->checkers)
->then(function ($coupon) use (&$cartWithCoupon) {
Cart::applyCoupon($coupon);
$cartWithCoupon = json_encode(Cart::instance());
Cart::removeCoupon();
});
} catch (Exception) {
//Just suppressing the exception
}
}
return Cart::instance();
return $cartWithCoupon ?? Cart::instance();
}
/**
* Remove the specified resource from storage.
*
* @param string $cartItemId
* @return \Illuminhtate\Http\Response
*
* @return \Modules\Cart\Cart
*/
public function destroy($cartItemId)
public function destroy(string $cartItemId)
{
Cart::remove($cartItemId);
return Cart::instance();
$cartWithCoupon = null;
$couponCode = request()->query('coupon_code');
if ($couponCode) {
$coupon = Coupon::findByCode($couponCode);
try {
resolve(Pipeline::class)
->send($coupon)
->through($this->checkers)
->then(function ($coupon) use (&$cartWithCoupon) {
Cart::applyCoupon($coupon);
$cartWithCoupon = json_encode(Cart::instance());
Cart::removeCoupon();
});
} catch (Exception) {
//Just suppressing the exception
}
}
return $cartWithCoupon ?? Cart::instance();
}
}

View File

@@ -2,15 +2,44 @@
namespace Modules\Cart\Http\Controllers;
use Exception;
use Modules\Cart\Facades\Cart;
use Illuminate\Pipeline\Pipeline;
use Modules\Coupon\Entities\Coupon;
use Modules\Coupon\Checkers\ValidCoupon;
use Modules\Coupon\Checkers\CouponExists;
use Modules\Coupon\Checkers\MinimumSpend;
use Modules\Coupon\Checkers\MaximumSpend;
use Modules\Coupon\Checkers\AlreadyApplied;
use Modules\Shipping\Facades\ShippingMethod;
use Modules\Coupon\Checkers\ExcludedProducts;
use Modules\Coupon\Checkers\ApplicableProducts;
use Modules\Coupon\Checkers\ExcludedCategories;
use Modules\Coupon\Checkers\UsageLimitPerCoupon;
use Modules\Coupon\Checkers\ApplicableCategories;
use Modules\Coupon\Checkers\UsageLimitPerCustomer;
class CartShippingMethodController
{
private array $checkers = [
CouponExists::class,
AlreadyApplied::class,
ValidCoupon::class,
MinimumSpend::class,
MaximumSpend::class,
ApplicableProducts::class,
ExcludedProducts::class,
ApplicableCategories::class,
ExcludedCategories::class,
UsageLimitPerCoupon::class,
UsageLimitPerCustomer::class,
];
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
* @return \Modules\Cart\Cart
*/
public function store()
{
@@ -18,6 +47,28 @@ class CartShippingMethodController
Cart::addShippingMethod($shippingMethod);
return Cart::instance();
$cartWithCoupon = null;
$couponCode = request()->query('coupon_code');
if ($couponCode) {
$coupon = Coupon::findByCode($couponCode);
try {
resolve(Pipeline::class)
->send($coupon)
->through($this->checkers)
->then(function ($coupon) use (&$cartWithCoupon) {
Cart::applyCoupon($coupon);
$cartWithCoupon = json_encode(Cart::instance());
Cart::removeCoupon();
});
} catch (Exception) {
//Just suppressing the exception
}
}
return $cartWithCoupon ?? Cart::instance();
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Modules\Cart\Http\Middleware;
use Closure;
use Exception;
use Modules\Cart\CartItem;
use Illuminate\Http\Request;
use Modules\Cart\Facades\Cart;
use Modules\FlashSale\Entities\FlashSale;
use Modules\Cart\Exceptions\CartItemsStockException;
class CheckCartItemsStock
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
try {
Cart::items()->each(function (CartItem $cartItem) {
$cartItem->refreshStock();
$this->checkStock($cartItem);
});
} catch (CartItemsStockException $e) {
if (request()->wantsJson()) {
return response()->json(
[
'message' => $e->getMessage(),
],
400
);
}
return redirect()->back()->with('error', $e->getMessage());
} catch (Exception $e) {
if (app()->hasDebugModeEnabled()) {
$message = $e->getMessage();
} else {
$message = trans('core::something_went_wrong');
}
if (request()->ajax()) {
return response()->json(
[
'message' => $message,
],
400
);
}
return redirect()->back()->with('error', $message);
}
return $next($request);
}
public function isInStock($cartItem)
{
return $cartItem->item->isInStock();
}
/**
* @throws CartItemsStockException
*/
private function checkStock(CartItem $cartItem): void
{
if (!$this->isInStock($cartItem)) {
throw new CartItemsStockException(trans('cart::messages.one_or_more_product_is_out_of_stock'));
}
if (!$this->hasFlashSaleStock($cartItem)) {
throw new CartItemsStockException(trans('cart::messages.one_or_more_product_doesn\'t_have_enough_stock'));
}
if (!$this->hasStock($cartItem)) {
throw new CartItemsStockException(trans('cart::messages.one_or_more_product_doesn\'t_have_enough_stock'));
}
}
private function hasFlashSaleStock(CartItem $cartItem): bool
{
if ($cartItem->variant || !FlashSale::contains($cartItem->product)) {
return true;
}
$remainingQty = FlashSale::remainingQty($cartItem->product);
$addedCartQty = Cart::addedQty($cartItem);
return ($remainingQty - $addedCartQty) >= 0;
}
private function hasStock(CartItem $cartItem): bool
{
if (!$cartItem->item->manage_stock) {
return true;
}
$addedCartQty = Cart::addedQty($cartItem);
return ($cartItem->item->qty - $addedCartQty) >= 0;
}
}

View File

@@ -12,11 +12,13 @@ class CheckCouponUsageLimit
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param Request $request
* @param Closure $next
*
* @return mixed
* @throws CouponUsageLimitReachedException
*/
public function handle(Request $request, Closure $next)
public function handle(Request $request, Closure $next): mixed
{
if (Cart::coupon()->usageLimitReached($request->customer_email)) {
throw new CouponUsageLimitReachedException;

View File

@@ -0,0 +1,130 @@
<?php
namespace Modules\Cart\Http\Middleware;
use Closure;
use Modules\Cart\CartItem;
use Illuminate\Http\Request;
use Modules\Cart\Facades\Cart;
use Modules\Product\Entities\Product;
use Modules\FlashSale\Entities\FlashSale;
use Modules\Product\Entities\ProductVariant;
class CheckItemStock
{
private CartItem|null $cartItem;
private Product $product;
private ProductVariant|null $variant;
private Product|ProductVariant $item;
public function __construct()
{
if (request()->routeIs('cart.items.update')) {
$this->cartItem = Cart::items()->get(request('cartItemId'));
$this->product = $this->cartItem->product;
$this->variant = $this->cartItem->variant;
}
if (request()->routeIs('cart.items.store')) {
$this->product = request('product_id') ? $this->getProduct(request('product_id')) : null;
$this->variant = request('variant_id') ? $this->getVariant(request('variant_id')) : null;
}
$this->item = $this->variant ?? $this->product;
}
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
if ($this->item->isOutOfStock()) {
abort(400, trans('cart::messages.out_of_stock'));
}
if (!$this->hasFlashSaleStock()) {
abort(400, trans('cart::messages.not_have_enough_quantity_in_stock', [
'stock' => FlashSale::remainingQty($this->product),
]));
}
if (!$this->hasStock()) {
abort(400, trans('cart::messages.not_have_enough_quantity_in_stock', [
'stock' => $this->item->qty,
]));
}
return $next($request);
}
private function getProduct($id)
{
return Product::withName()
->addSelect('id', 'in_stock', 'manage_stock', 'qty')
->where('id', $id)
->firstOrFail();
}
private function getVariant($id)
{
return ProductVariant::addSelect('id', 'in_stock', 'manage_stock', 'qty')
->where('id', $id)
->firstOrFail();
}
private function hasFlashSaleStock(): bool
{
if (!FlashSale::contains($this->product)) {
return true;
}
$remainingQty = FlashSale::remainingQty($this->product);
$cartItem = Cart::items()->get(request('cartItemId'));
if ($cartItem) {
$addedCartQty = Cart::addedQty($cartItem);
// Exclude current cart item quantity from the total added cart quantity
// So, that current quantity is not added with the updated quantity.
$addedCartQty -= $cartItem->qty;
return ($remainingQty - $addedCartQty) >= request('qty');
}
return $remainingQty >= request('qty');
}
private function hasStock(): bool
{
if (!$this->item->manage_stock) {
return true;
}
$cartItem = Cart::items()->get(request('cartItemId'));
if ($cartItem) {
$addedCartQty = Cart::addedQty($cartItem);
// Exclude current cart item quantity from the total added cart quantity
// So, that current quantity is not added with the updated quantity.
$addedCartQty -= $cartItem->qty;
return ($cartItem->item->qty - $addedCartQty) >= request('qty');
}
return $this->item->qty >= request('qty');
}
}

View File

@@ -11,8 +11,9 @@ class RedirectIfCartIsEmpty
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)

View File

@@ -3,11 +3,41 @@
namespace Modules\Cart\Http\Requests;
use Illuminate\Validation\Rule;
use Modules\Option\Entities\Option;
use Modules\Product\Entities\Product;
use Modules\Core\Http\Requests\Request;
use Modules\Product\Entities\ProductVariant;
use Illuminate\Database\Eloquent\Collection;
class StoreCartItemRequest extends Request
{
protected Product|null $product;
protected ProductVariant|null $variant;
protected Product|ProductVariant $item;
public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null)
{
parent::__construct($query, $request, $attributes, $cookies, $files, $server, $content);
$this->product = $this->getProduct();
$this->variant = $this->getVariant();
$this->item = $this->variant ?? $this->product;
}
public function authorize()
{
if ($this->item->is_active) {
return true;
}
return false;
}
/**
* Get the validation rules that apply to the request.
*
@@ -15,61 +45,11 @@ class StoreCartItemRequest extends Request
*/
public function rules()
{
$product = Product::with('options')
->select('id', 'manage_stock', 'qty')
->findOrFail($this->product_id);
return array_merge([
'qty' => ['required', 'numeric', $this->maxQty($product)],
], $this->getOptionsRules($product->options));
'qty' => ['required', 'numeric', $this->maxQty($this->item)],
], $this->getOptionsRules($this->product->options));
}
/**
* Get the max qty rule for the given product.
*
* @param \Modules\Product\Entities\Product $product
* @return string|null
*/
private function maxQty($product)
{
if ($product->manage_stock) {
return "max:{$product->qty}";
}
}
/**
* Get rules for the given options.
*
* @param \Illuminate\Database\Eloquent\Collection $options
* @return array
*/
private function getOptionsRules($options)
{
return $options->flatMap(function ($option) {
return ["options.{$option->id}" => $this->getOptionRules($option)];
})->all();
}
/**
* Get rules for the given option.
*
* @param \Modules\Option\Entities\Option $option
* @return array
*/
private function getOptionRules($option)
{
$rules = [];
if ($option->is_required) {
$rules[] = 'required';
}
if (in_array($option->type, ['dropdown', 'radio'])) {
$rules[] = Rule::in($option->values->map->id->all());
}
return $rules;
}
/**
* Get data to be validated from the request.
@@ -86,6 +66,7 @@ class StoreCartItemRequest extends Request
);
}
/**
* Get custom messages for validator errors.
*
@@ -98,4 +79,91 @@ class StoreCartItemRequest extends Request
'options.*.in' => trans('cart::validation.the_selected_option_is_invalid'),
], parent::messages());
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'qty' => (int)$this->qty,
]);
}
private function getProduct()
{
$product_id = request()->input('product_id');
return Product::with('options')
->select('id', 'manage_stock', 'qty', 'is_active')
->find($product_id);
}
private function getVariant()
{
$variant_id = request()->input('variant_id');
if ($variant_id) {
return ProductVariant::select('id', 'manage_stock', 'qty', 'is_active')
->find($variant_id);
}
}
/**
* Get the max qty rule for the given product.
*
* @param Product|ProductVariant $item
*
* @return string|null
*/
private function maxQty(Product|ProductVariant $item)
{
if ($item->manage_stock) {
return "max:{$item->qty}";
}
}
/**
* Get rules for the given options.
*
* @param Collection $options
*
* @return array
*/
private function getOptionsRules($options)
{
return $options->flatMap(function ($option) {
return ["options.{$option->id}" => $this->getOptionRules($option)];
})->all();
}
/**
* Get rules for the given option.
*
* @param Option $option
*
* @return array
*/
private function getOptionRules($option)
{
$rules = [];
if ($option->is_required) {
$rules[] = 'required';
}
if (in_array($option->type, ['dropdown', 'radio'])) {
$rules[] = Rule::in($option->values->map->id->all());
}
return $rules;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Modules\Cart\Http\Requests;
use Modules\Cart\CartItem;
use Modules\Cart\Facades\Cart;
use Modules\Product\Entities\Product;
use Modules\Core\Http\Requests\Request;
use Modules\Product\Entities\ProductVariant;
class UpdateCartItemRequest extends Request
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$item = $this->getCartItem()->item;
return [
'qty' => ['required', 'numeric', $this->maxQty($item)],
];
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'qty' => (int)$this->qty,
]);
}
/**
* Get the item from the cart
*
* @return CartItem
*/
private function getCartItem(): CartItem
{
return Cart::items()->get($this->cartItemId)->refreshStock();
}
/**
* Get the max qty rule for the given product or variant.
*
* @param Product|ProductVariant $item
*
* @return string|null
*/
private function maxQty(Product|ProductVariant $item)
{
if ($item->manage_stock) {
return "max:{$item->qty}";
}
}
}