first upload all files

This commit is contained in:
NW
2023-06-11 13:14:03 +01:00
parent f14dbc52b5
commit c08b36d1b6
1705 changed files with 106852 additions and 0 deletions

432
Modules/Cart/Cart.php Normal file
View File

@@ -0,0 +1,432 @@
<?php
namespace Modules\Cart;
use JsonSerializable;
use Modules\Support\Money;
use Modules\Tax\Entities\TaxRate;
use Illuminate\Support\Collection;
use Modules\Coupon\Entities\Coupon;
use Modules\Product\Entities\Product;
use Modules\Shipping\Facades\ShippingMethod;
use Darryldecode\Cart\Cart as DarryldecodeCart;
use Modules\Product\Services\ChosenProductOptions;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
class Cart extends DarryldecodeCart implements JsonSerializable
{
/**
* Get the current instance.
*
* @return $this
*/
public function instance()
{
return $this;
}
/**
* Clear the cart.
*
* @return void
*/
public function clear()
{
parent::clear();
$this->clearCartConditions();
}
/**
* Store a new item to the cart.
*
* @param int $productId
* @param int $qty
* @param array $options
*
* @return void
*/
public function store($productId, $qty, $options = [])
{
$options = array_filter($options);
$product = Product::with('files', 'categories', 'taxClass')->findOrFail($productId);
$chosenOptions = new ChosenProductOptions($product, $options);
$this->add([
'id' => md5("product_id.{$product->id}:options." . serialize($options)),
'name' => $product->name,
'price' => $product->selling_price->amount(),
'quantity' => $qty,
'attributes' => [
'product' => $product,
'options' => $chosenOptions->getEntities(),
'created_at' => time(),
],
]);
}
public function updateQuantity($id, $qty)
{
$this->update($id, [
'quantity' => [
'relative' => false,
'value' => $qty,
],
]);
}
public function items()
{
return $this->getContent()
->sortBy('attributes.created_at')
->map(function ($item) {
return new CartItem($item);
});
}
public function addedQty($productId)
{
return $this->findByProductId($productId)->sum('qty');
}
public function findByProductId($productId)
{
return $this->items()->filter(function ($cartItem) use ($productId) {
return $cartItem->product->id == $productId;
});
}
public function crossSellProducts()
{
return $this->getAllProducts()
->load([
'crossSellProducts' => function ($query) {
$query->forCard();
},
])
->pluck('crossSellProducts')
->flatten();
}
public function getAllProducts()
{
return $this->items()
->map(function ($cartItem) {
return $cartItem->product;
})
->flatten()
->pipe(function ($products) {
return new EloquentCollection($products);
});
}
public function reduceStock()
{
$this->manageStock(function ($cartItem) {
$cartItem->product->decrement('qty', $cartItem->qty);
if ($cartItem->refreshStock()->product->qty === 0) {
$cartItem->product->markAsOutOfStock();
}
});
}
public function restoreStock()
{
$this->manageStock(function ($cartItem) {
$cartItem->product->increment('qty', $cartItem->qty);
if ($cartItem->product->qty > 0) {
$cartItem->product->markAsInStock();
}
});
}
private function manageStock($callback)
{
$this->items()
->filter(function ($cartItem) {
return $cartItem->product->manage_stock;
})
->each($callback);
}
public function quantity()
{
return $this->getTotalQuantity();
}
public function hasAvailableShippingMethod()
{
return $this->availableShippingMethods()->isNotEmpty();
}
public function availableShippingMethods()
{
if ($this->allItemsAreVirtual()) {
return collect();
}
return ShippingMethod::available();
}
public function allItemsAreVirtual()
{
return $this->items()->every(function (CartItem $cartItem) {
return $cartItem->product->is_virtual;
});
}
public function hasShippingMethod()
{
return $this->getConditionsByType('shipping_method')->isNotEmpty();
}
public function shippingMethod()
{
if (!$this->hasShippingMethod()) {
return new NullCartShippingMethod();
}
return new CartShippingMethod($this, $this->getConditionsByType('shipping_method')->first());
}
public function shippingCost()
{
return $this->shippingMethod()->cost();
}
public function addShippingMethod($shippingMethod)
{
$this->removeShippingMethod();
$this->condition(
new CartCondition([
'name' => $shippingMethod->label,
'type' => 'shipping_method',
'target' => 'total',
'value' => "+{$shippingMethod->cost->amount()}",
'order' => 1,
'attributes' => [
'shipping_method' => $shippingMethod,
],
]),
);
$this->refreshFreeShippingCoupon();
return $this->shippingMethod();
}
public function removeShippingMethod()
{
$this->removeConditionsByType('shipping_method');
}
private function refreshFreeShippingCoupon()
{
if ($this->coupon()->isFreeShipping()) {
$this->applyCoupon($this->coupon()->entity());
}
}
public function hasCoupon()
{
if ($this->getConditionsByType('coupon')->isEmpty()) {
return false;
}
$couponId = $this->getConditionsByType('coupon')
->first()
->getAttribute('coupon_id');
return Coupon::where('id', $couponId)->exists();
}
public function couponAlreadyApplied(Coupon $coupon)
{
return $this->coupon()->code() === $coupon->code;
}
public function coupon()
{
if (!$this->hasCoupon()) {
return new NullCartCoupon();
}
$couponCondition = $this->getConditionsByType('coupon')->first();
$coupon = Coupon::with('products', 'categories')->find($couponCondition->getAttribute('coupon_id'));
return new CartCoupon($this, $coupon, $couponCondition);
}
public function discount()
{
return $this->coupon()->value();
}
public function applyCoupon(Coupon $coupon)
{
$this->removeCoupon();
$this->condition(
new CartCondition([
'name' => $coupon->code,
'type' => 'coupon',
'target' => 'total',
'value' => $this->getCouponValue($coupon),
'order' => 2,
'attributes' => [
'coupon_id' => $coupon->id,
],
]),
);
}
private function getCouponValue($coupon)
{
if ($coupon->free_shipping) {
return "-{$this->shippingMethod()->cost()->amount()}";
}
if ($coupon->is_percent) {
return "-{$coupon->value}%";
}
return "-{$coupon->value->amount()}";
}
public function removeCoupon()
{
$this->removeConditionsByType('coupon');
}
public function hasTax()
{
return $this->getConditionsByType('tax')->isNotEmpty();
}
public function taxes()
{
if (!$this->hasTax()) {
return new Collection();
}
$taxConditions = $this->getConditionsByType('tax');
$taxRates = TaxRate::whereIn('id', $this->getTaxRateIds($taxConditions))->get();
return $taxConditions->map(function ($taxCondition) use ($taxRates) {
$taxRate = $taxRates->where('id', $taxCondition->getAttribute('tax_rate_id'))->first();
return new CartTax($this, $taxRate, $taxCondition);
});
}
private function getTaxRateIds($taxConditions)
{
return $taxConditions->map(function ($taxCondition) {
return $taxCondition->getAttribute('tax_rate_id');
});
}
public function tax()
{
return Money::inDefaultCurrency($this->calculateTax());
}
private function calculateTax()
{
return $this->taxes()->sum(function ($cartTax) {
return $cartTax->amount()->amount();
});
}
public function addTaxes($request)
{
$this->removeTaxes();
$this->findTaxes($request)->each(function ($taxRate) {
$this->condition(
new CartCondition([
'name' => $taxRate->id,
'type' => 'tax',
'target' => 'total',
'value' => "+{$taxRate->rate}%",
'order' => 3,
'attributes' => [
'tax_rate_id' => $taxRate->id,
],
]),
);
});
}
public function removeTaxes()
{
$this->removeConditionsByType('tax');
}
private function findTaxes($request)
{
return $this->items()
->groupBy('tax_class_id')
->flatten()
->map(function (CartItem $cartItem) use ($request) {
return $cartItem->findTax($request->only(['billing', 'shipping']));
})
->filter();
}
public function subTotal()
{
return Money::inDefaultCurrency($this->getSubTotal())->add($this->optionsPrice());
}
private function optionsPrice()
{
return Money::inDefaultCurrency($this->calculateOptionsPrice());
}
private function calculateOptionsPrice()
{
return $this->items()->sum(function ($cartItem) {
return $cartItem
->optionsPrice()
->multiply($cartItem->qty)
->amount();
});
}
public function total()
{
return $this->subTotal()
->add($this->shippingMethod()->cost())
->subtract($this->coupon()->value())
->add($this->tax());
}
public function toArray()
{
return [
'items' => $this->items(),
'quantity' => $this->quantity(),
'availableShippingMethods' => $this->availableShippingMethods(),
'subTotal' => $this->subTotal(),
'shippingMethodName' => $this->shippingMethod()->name(),
'shippingCost' => $this->shippingCost(),
'coupon' => $this->coupon(),
'taxes' => $this->taxes(),
'total' => $this->total(),
];
}
public function jsonSerialize()
{
return $this->toArray();
}
public function __toString()
{
return json_encode($this->jsonSerialize());
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Modules\Cart;
use Darryldecode\Cart\CartCondition as DarryldecodeCartCondition;
class CartCondition extends DarryldecodeCartCondition
{
public function getAttribute($key, $default = null)
{
return array_get($this->getAttributes(), $key, $default);
}
}

160
Modules/Cart/CartCoupon.php Normal file
View File

@@ -0,0 +1,160 @@
<?php
namespace Modules\Cart;
use JsonSerializable;
use Modules\Support\Money;
class CartCoupon implements JsonSerializable
{
private $cart;
private $coupon;
private $couponCondition;
public function __construct($cart, $coupon, $couponCondition)
{
$this->cart = $cart;
$this->coupon = $coupon;
$this->couponCondition = $couponCondition;
}
public function entity()
{
return $this->coupon;
}
public function id()
{
return $this->coupon->id;
}
public function code()
{
return $this->coupon->code;
}
public function isFreeShipping()
{
return $this->coupon->free_shipping;
}
public function usageLimitReached($customerEmail)
{
return $this->fresh()->coupon->usageLimitReached($customerEmail);
}
public function didNotSpendTheRequiredAmount()
{
return $this->fresh()->coupon->didNotSpendTheRequiredAmount();
}
public function spentMoreThanMaximumAmount()
{
return $this->fresh()->coupon->spentMoreThanMaximumAmount();
}
public function fresh()
{
$this->coupon = $this->coupon->refresh();
return $this;
}
public function usedOnce()
{
$this->coupon->increment('used');
}
public function value()
{
if ($this->coupon->free_shipping) {
return $this->cart->shippingMethod()->cost();
}
return Money::inDefaultCurrency($this->calculate());
}
private function calculate()
{
return $this->couponCondition->getCalculatedValue($this->productsTotalPrice());
}
private function productsTotalPrice()
{
return $this->couponApplicableProducts()->sum(function ($cartItem) {
return $cartItem->total()->amount();
});
}
private function couponApplicableProducts()
{
return $this->cart->items()->filter(function ($cartItem) {
return $this->inApplicableProducts($cartItem);
})->reject(function ($cartItem) {
return $this->inExcludedProducts($cartItem);
})->filter(function ($cartItem) {
return $this->inApplicableCategories($cartItem);
})->reject(function ($cartItem) {
return $this->inExcludedCategories($cartItem);
});
}
private function inApplicableProducts($cartItem)
{
if ($this->coupon->products->isEmpty()) {
return true;
}
return $this->coupon->products->contains($cartItem->product);
}
private function inExcludedProducts($cartItem)
{
if ($this->coupon->excludeProducts->isEmpty()) {
return false;
}
return $this->coupon->excludeProducts->contains($cartItem->product);
}
private function inApplicableCategories($cartItem)
{
if ($this->coupon->categories->isEmpty()) {
return true;
}
return $this->coupon->categories->intersect($cartItem->product->categories)->isNotEmpty();
}
private function inExcludedCategories($cartItem)
{
if ($this->coupon->excludeCategories->isEmpty()) {
return false;
}
return $this->coupon->excludeCategories->intersect($cartItem->product->categories)->isNotEmpty();
}
public function toArray()
{
return [
'code' => $this->code(),
'value' => $this->value(),
];
}
public function jsonSerialize()
{
return $this->toArray();
}
public function __toString()
{
return json_encode($this->jsonSerialize());
}
public function __get($attribute)
{
return $this->coupon->{$attribute};
}
}

91
Modules/Cart/CartItem.php Normal file
View File

@@ -0,0 +1,91 @@
<?php
namespace Modules\Cart;
use JsonSerializable;
use Modules\Support\Money;
use Modules\Product\Entities\Product;
class CartItem implements JsonSerializable
{
public $id;
public $qty;
public $product;
public $options;
public function __construct($item)
{
$this->id = $item->id;
$this->qty = $item->quantity;
$this->product = $item->attributes['product'];
$this->options = $item->attributes['options'];
}
public function unitPrice()
{
return $this->product->selling_price->add($this->optionsPrice());
}
public function total()
{
return $this->unitPrice()->multiply($this->qty);
}
public function optionsPrice()
{
return Money::inDefaultCurrency($this->calculateOptionsPrice());
}
public function calculateOptionsPrice()
{
return $this->options->sum(function ($option) {
return $this->valuesSum($option->values);
});
}
private function valuesSum($values)
{
return $values->sum(function ($value) {
if ($value->price_type === 'fixed') {
return $value->price->amount();
}
return ($value->price / 100) * $this->product->selling_price->amount();
});
}
public function refreshStock()
{
$product = Product::select(['manage_stock', 'in_stock', 'qty'])->find($this->product->id);
$this->product->fill([
'manage_stock' => $product->manage_stock,
'in_stock' => $product->in_stock,
'qty' => $product->qty,
]);
return $this;
}
public function findTax(array $addresses)
{
return $this->product->taxClass->findTaxRate($addresses);
}
public function jsonSerialize()
{
return [
'id' => $this->id,
'qty' => $this->qty,
'product' => $this->product->clean(),
'options' => $this->options->keyBy('position'),
'unitPrice' => $this->unitPrice(),
'total' => $this->total(),
];
}
public function __toString()
{
return json_encode($this->jsonSerialize());
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Modules\Cart;
use Modules\Support\Money;
class CartShippingMethod
{
private $cart;
private $shippingMethodCondition;
public function __construct($cart, $shippingMethodCondition)
{
$this->cart = $cart;
$this->shippingMethodCondition = $shippingMethodCondition;
}
public function name()
{
return $this->shippingMethodCondition->getAttribute('shipping_method')->name;
}
public function label()
{
return $this->shippingMethodCondition->getAttribute('shipping_method')->label;
}
public function cost()
{
return Money::inDefaultCurrency($this->calculate());
}
private function calculate()
{
return $this->shippingMethodCondition->getCalculatedValue($this->cart->subTotal()->amount());
}
}

77
Modules/Cart/CartTax.php Normal file
View File

@@ -0,0 +1,77 @@
<?php
namespace Modules\Cart;
use JsonSerializable;
use Modules\Support\Money;
class CartTax implements JsonSerializable
{
private $cart;
private $taxRate;
private $taxCondition;
public function __construct($cart, $taxRate, $taxCondition)
{
$this->cart = $cart;
$this->taxRate = $taxRate;
$this->taxCondition = $taxCondition;
}
public function id()
{
return $this->taxRate->id;
}
public function name()
{
return $this->taxRate->name;
}
public function amount()
{
return Money::inDefaultCurrency($this->calculate());
}
private function calculate()
{
return $this->taxCondition->getCalculatedValue($this->productsTotalPrice());
}
private function productsTotalPrice()
{
return $this->taxApplicableProducts()->sum(function ($cartItem) {
return $cartItem->total()->amount();
});
}
private function taxApplicableProducts()
{
return $this->cart->items()->filter(function ($cartItem) {
return $this->hasMatchingTaxClass($cartItem);
});
}
private function hasMatchingTaxClass($cartItem)
{
return $cartItem->product->tax_class_id === $this->taxRate->tax_class_id;
}
public function toArray()
{
return [
'name' => $this->name(),
'amount' => $this->amount(),
];
}
public function jsonSerialize()
{
return $this->toArray();
}
public function __toString()
{
return json_encode($this->jsonSerialize());
}
}

View File

@@ -0,0 +1,36 @@
<?php
return [
/*
* ---------------------------------------------------------------
* formatting
* ---------------------------------------------------------------
*
* the formatting of shopping cart values
*/
'format_numbers' => env('SHOPPING_FORMAT_VALUES', false),
'decimals' => env('SHOPPING_DECIMALS', 0),
'dec_point' => env('SHOPPING_DEC_POINT', '.'),
'thousands_sep' => env('SHOPPING_THOUSANDS_SEP', ','),
/*
* ---------------------------------------------------------------
* persistence
* ---------------------------------------------------------------
*
* the configuration for persisting cart
*/
'storage' => null,
/*
* ---------------------------------------------------------------
* events
* ---------------------------------------------------------------
*
* the configuration for cart events
*/
'events' => null,
];

View File

@@ -0,0 +1,56 @@
<?php
namespace Modules\Cart\Facades;
use Illuminate\Support\Facades\Facade;
/**
* @method static \Modules\Cart\Cart instance()
* @method static void clear()
* @method static void store(int $productId, int $qty, array $options = [])
* @method static void updateQuantity(string $id, int $qty)
* @method static \Darryldecode\Cart\CartCollection items()
* @method static int addedQty(int $productId)
* @method static int findByProductId(int $productId)
* @method static \Illuminate\Database\Eloquent\Collection crossSellProducts()
* @method static \Illuminate\Database\Eloquent\Collection getAllProducts()
* @method static void reduceStock()
* @method static void restoreStock()
* @method static int quantity()
* @method static bool hasAvailableShippingMethod()
* @method static \Illuminate\Support\Collection availableShippingMethods()
* @method static bool hasShippingMethod()
* @method static \Modules\Cart\CartShippingMethod shippingMethod()
* @method static \Modules\Support\Money shippingCost()
* @method static \Modules\Support\Money addShippingMethod(\Modules\Shipping\Method $shippingMethod)
* @method static void removeShippingMethod()
* @method static bool hasCoupon()
* @method static bool couponAlreadyApplied()
* @method static \Modules\Cart\CartCoupon coupon()
* @method static \Modules\Support\Money discount()
* @method static void applyCoupon(\Modules\Coupon\Entities\Coupon $coupon)
* @method static void removeCoupon()
* @method static void hasTax()
* @method static \Darryldecode\Cart\CartConditionCollection taxes()
* @method static \Modules\Support\Money tax()
* @method static void addTaxes(\Illuminate\Http\Request $request)
* @method static void removeTaxes()
* @method static \Modules\Support\Money subTotal()
* @method static \Modules\Support\Money total()
* @method static array toArray()
* @method static array jsonSerialize()
*
* @see \Modules\Cart\Cart
*/
class Cart extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return \Modules\Cart\Cart::class;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Modules\Cart\Http\Controllers;
use Modules\Cart\Facades\Cart;
class CartClearController
{
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
*/
public function store()
{
Cart::clear();
return Cart::instance();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Modules\Cart\Http\Controllers;
class CartController
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('public.cart.index');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Modules\Cart\Http\Controllers;
use Modules\Cart\Facades\Cart;
class CartCrossSellProductsController
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return Cart::crossSellProducts();
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Modules\Cart\Http\Controllers;
use Modules\Cart\Facades\Cart;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Routing\Controller;
use Modules\Coupon\Checkers\MaximumSpend;
use Modules\Coupon\Checkers\MinimumSpend;
use Modules\Cart\Http\Requests\StoreCartItemRequest;
use Modules\Coupon\Exceptions\MaximumSpendException;
use Modules\Coupon\Exceptions\MinimumSpendException;
use Modules\Cart\Http\Middleware\CheckProductIsInStock;
class CartItemController extends Controller
{
private $checkers = [
MinimumSpend::class,
MaximumSpend::class,
];
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware(CheckProductIsInStock::class)->only(['store', 'update']);
}
/**
* Store a newly created resource in storage.
*
* @param \Modules\Cart\Http\Requests\StoreCartItemRequest $request
* @return \Illuminate\Http\Response
*/
public function store(StoreCartItemRequest $request)
{
Cart::store($request->product_id, $request->qty, $request->options ?? []);
return Cart::instance();
}
/**
* Update the specified resource in storage.
*
* @param mixed $cartItemId
* @return \Illuminate\Http\Response
*/
public function update($cartItemId)
{
Cart::updateQuantity($cartItemId, request('qty'));
try {
resolve(Pipeline::class)
->send(Cart::coupon())
->through($this->checkers)
->thenReturn();
} catch (MinimumSpendException | MaximumSpendException $e) {
Cart::removeCoupon();
}
return Cart::instance();
}
/**
* Remove the specified resource from storage.
*
* @param string $cartItemId
* @return \Illuminhtate\Http\Response
*/
public function destroy($cartItemId)
{
Cart::remove($cartItemId);
return Cart::instance();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Modules\Cart\Http\Controllers;
use Modules\Cart\Facades\Cart;
use Modules\Shipping\Facades\ShippingMethod;
class CartShippingMethodController
{
/**
* Store a newly created resource in storage.
*
* @return \Illuminate\Http\Response
*/
public function store()
{
$shippingMethod = ShippingMethod::get(request('shipping_method'));
Cart::addShippingMethod($shippingMethod);
return Cart::instance();
}
}

View File

@@ -0,0 +1,73 @@
<?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;
class CheckCartStock
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
try {
Cart::items()->each(function (CartItem $cartItem) {
$cartItem->refreshStock();
$this->checkQuantity($cartItem);
});
} catch (Exception $e) {
return redirect()->route('cart.index')->with('error', $e->getMessage());
}
return $next($request);
}
private function checkQuantity(CartItem $cartItem)
{
if ($cartItem->product->isOutOfStock()) {
throw new Exception(trans('cart::messages.one_or_more_product_is_out_of_stock'));
}
if (! $this->hasFlashSaleStock($cartItem)) {
throw new Exception(trans('cart::messages.one_or_more_product_doesn\'t_have_enough_stock'));
}
if (! $this->hasStock($cartItem)) {
throw new Exception(trans('cart::messages.one_or_more_product_doesn\'t_have_enough_stock'));
}
}
private function hasFlashSaleStock(CartItem $cartItem)
{
if (! FlashSale::contains($cartItem->product)) {
return true;
}
$remainingQty = FlashSale::remainingQty($cartItem->product);
$addedCartQty = Cart::addedQty($cartItem->product->id);
return ($remainingQty - $addedCartQty) >= 0;
}
private function hasStock(CartItem $cartItem)
{
if (! $cartItem->product->manage_stock) {
return true;
}
$addedCartQty = Cart::addedQty($cartItem->product->id);
return ($cartItem->product->qty - $addedCartQty) >= 0;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Modules\Cart\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Modules\Cart\Facades\Cart;
use Modules\Coupon\Exceptions\CouponUsageLimitReachedException;
class CheckCouponUsageLimit
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (Cart::coupon()->usageLimitReached($request->customer_email)) {
throw new CouponUsageLimitReachedException;
}
return $next($request);
}
}

View File

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

View File

@@ -0,0 +1,26 @@
<?php
namespace Modules\Cart\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Modules\Cart\Facades\Cart;
class RedirectIfCartIsEmpty
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
if (Cart::isEmpty()) {
return redirect()->route('cart.index');
}
return $next($request);
}
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Modules\Cart\Http\Requests;
use Illuminate\Validation\Rule;
use Modules\Product\Entities\Product;
use Modules\Core\Http\Requests\Request;
class StoreCartItemRequest extends Request
{
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
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));
}
/**
* 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.
*
* @return array
*/
public function validationData()
{
return array_merge(
$this->all(),
[
'options' => array_filter($this->options ?? []),
]
);
}
/**
* Get custom messages for validator errors.
*
* @return array
*/
public function messages()
{
return array_merge([
'options.*.required' => trans('cart::validation.this_field_is_required'),
'options.*.in' => trans('cart::validation.the_selected_option_is_invalid'),
], parent::messages());
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Modules\Cart\Listeners;
use Modules\Cart\Facades\Cart;
class ClearCart
{
/**
* Handle the event.
*
* @return void
*/
public function handle()
{
Cart::clear();
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Modules\Cart;
use Modules\Support\Money;
class NullCartCoupon
{
public function id()
{
//
}
public function code()
{
//
}
public function isFreeShipping()
{
return false;
}
public function usageLimitReached()
{
return false;
}
public function didNotSpendTheRequiredAmount()
{
return false;
}
public function spentMoreThanMaximumAmount()
{
return false;
}
public function usedOnce()
{
//
}
public function value()
{
return Money::inDefaultCurrency(0);
}
public function __get($attribute)
{
//
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Modules\Cart;
use Modules\Support\Money;
class NullCartShippingMethod
{
public function name()
{
//
}
public function title()
{
//
}
public function cost()
{
return Money::inDefaultCurrency(0);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Modules\Cart\Providers;
use Modules\Cart\Cart;
use Illuminate\Support\ServiceProvider;
class CartServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->singleton(Cart::class, function ($app) {
return new Cart(
$app['session'],
$app['events'],
'cart',
session()->getId(),
config('fleetcart.modules.cart.config')
);
});
$this->app->alias(Cart::class, 'cart');
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Modules\Cart\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
\Modules\Checkout\Events\OrderPlaced::class => [
\Modules\Cart\Listeners\ClearCart::class,
],
];
}

View File

@@ -0,0 +1,8 @@
<?php
return [
'out_of_stock' => 'Sorry, the product is out of stock.',
'not_have_enough_quantity_in_stock' => 'Sorry, we only have :stock in stock.',
'one_or_more_product_is_out_of_stock' => 'Sorry, one or more product is out of stock.',
'one_or_more_product_doesn\'t_have_enough_stock' => 'Sorry, one or more product doesn\'t have enough stock.',
];

View File

@@ -0,0 +1,6 @@
<?php
return [
'this_field_is_required' => 'This field is required.',
'the_selected_option_is_invalid' => 'The selected option is invalid.',
];

View File

@@ -0,0 +1,15 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('cart', 'CartController@index')->name('cart.index');
Route::post('cart/items', 'CartItemController@store')->name('cart.items.store');
Route::put('cart/items/{cartItemId}', 'CartItemController@update')->name('cart.items.update');
Route::delete('cart/items/{cartItemId}', 'CartItemController@destroy')->name('cart.items.destroy');
Route::post('cart/clear', 'CartClearController@store')->name('cart.clear.store');
Route::post('cart/shipping-method', 'CartShippingMethodController@store')->name('cart.shipping_method.store');
Route::get('cart/cross-sell-products', 'CartCrossSellProductsController@index')->name('cart.cross_sell_products.index');

View File

@@ -0,0 +1,28 @@
{
"name": "fleetcart/cart",
"description": "The FleetCart Cart Module.",
"authors": [
{
"name": "Envay Soft",
"email": "envaysoft@gmail.com"
}
],
"require": {
"php": "^8.0.2",
"darryldecode/cart": "^4.2"
},
"autoload": {
"psr-4": {
"Modules\\Cart\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}

10
Modules/Cart/module.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "Cart",
"alias": "cart",
"description": "The FleetCart Cart Module.",
"priority": 100,
"providers": [
"Modules\\Cart\\Providers\\CartServiceProvider",
"Modules\\Cart\\Providers\\EventServiceProvider"
]
}