File: D:/HostingSpaces/SBogers10/farmfun.komma.pro/app/Komma/ShoppingCart/ShoppingCart.php
<?php
namespace App\Komma\ShoppingCart;
use App\Komma\Availability\AvailabilityService;
use App\Komma\Availability\Types\Availability;
use App\Komma\Availability\Types\TimeSlot;
use App\Komma\Locations\Models\Location;
use App\Komma\Locations\Types\DayInfo;
use App\Komma\Products\Models\Product;
use App\Komma\Session\HasSessionTrait;
use App\Komma\ShoppingCart\Interfaces\ShoppingCartInterface;
use Carbon\Carbon;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use JsonSerializable;
/**
* Represents a shopping cart
*
* Class ShoppingCartService
*/
class ShoppingCart implements ShoppingCartInterface, JsonSerializable, Arrayable
{
use HasSessionTrait;
/** @var array The names of variables in this class to store and restore. */
protected $variablesForSession = [
'items',
'date',
'location',
];
/**
* @var array
*/
private $items;
/** @var Carbon */
private $date;
/** @var Location */
private $location;
/** @var array */
public $errorMessages = [];
/** @var bool */
private $useSession;
public function __construct($useSession = true)
{
$this->items = [];
$this->useSession = $useSession;
if($this->useSession) {
$this->restoreFromSession();
}
}
/**
* Add an item to the shopping cart
*
* @param Availability $availability
* @return void
*/
public function addItem(Availability $availability)
{
// If there are not items in the shopping cart
// We define the date and location out of the availability
if(empty($this->items)) {
$this->date = $availability->getDate()->startOfDay();
$this->location = $availability->getLocation();
} elseif(! $this->checkIfAdditionalItemsMatchedCurrentShoppingCart($availability)) {
abort(422, __('site/cart.errors.add_item.default'));
}
// Load capacity of the availability
$availability->loadCapacity();
// Make the first selected
// $firstTimeSlot = $availability->timeSlots->where('locked', '=', false)->first();
// $availability->setSelectedTimeSlot($firstTimeSlot->getTimeSlotValue());
$this->items[] = $availability;
if($this->useSession) {
$this->saveSession();
}
}
/**
* Add an item to the shopping cart for generation
*
* @param Availability $availability
* @param bool $shouldRedirect
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function addGeneratedItem(Availability $availability, bool $shouldRedirect = true)
{
// If there are not items in the shopping cart
// We define the date and location out of the availability
if(empty($this->items)) {
$this->date = $availability->getDate()->startOfDay();
$this->location = $availability->getLocation();
} elseif(! $this->checkIfAdditionalItemsMatchedCurrentShoppingCart($availability)) {
if(empty($this->errorMessages)) {
$this->errorMessages[] = __('site/cart.errors.add_item.default');
}
return redirect()
->back()
->with(['modalData' => [
'productId' => $availability->getProduct()->id,
'locationId' => $availability->getLocation()->id,
]])
->withInput()
->withErrors(['modalErrors' => $this->errorMessages]);
}
// Load capacity of the availability
$availability->loadCapacity();
// Make the time slots for the shopping cart view
$availability->timeSlots = $this->makeTimeSlots($availability);
// Make the first selected
// $firstTimeSlot = $availability->timeSlots->where('locked', '=', false)->first();
// $availability->setSelectedTimeSlot($firstTimeSlot->getTimeSlotValue());
$this->items[] = $availability;
$this->saveSession();
if($shouldRedirect) {
return redirect(localized_route('shoppingCart'));
}
}
/**
* Validates the additional item against the current shopping cart
*
* @param Availability $availability
* @return bool
*/
private function checkIfAdditionalItemsMatchedCurrentShoppingCart(Availability $availability): bool
{
$valid = true;
// Validate that new item is is on the same location
if(! $availability->getLocation()->is($this->location)) {
Log::info(self::class.': Location does not match with the already defined location in the shopping cart');
$this->errorMessages[] = __('site/cart.errors.add_item.location');
$valid = false;
}
// validate that it's the same day
if($availability->getDate()->startOfDay() != $this->date) {
Log::info(self::class.': Date does not match with the already defined date in the shopping cart');
$this->errorMessages[] = __('site/cart.errors.add_item.date');
$valid = false;
}
// Validate that the product isn't already in the shopping cart
foreach ($this->items as $item) {
if($availability->getProduct()->is($item->getProduct())) {
Log::info(self::class.': Product is already in the shopping cart.');
$this->errorMessages[] = __('site/cart.errors.add_item.already_added');
$valid = false;
}
}
return $valid;
}
/**
* Update an item from the cart
*
* @param int $productId
* @param $quantity
* @param $timeSlot
* @param $notification
*/
public function updateItem(int $productId, $quantity, $timeSlot, $notification)
{
$itemHasBeenUpdated = false;
/**
* @var Availability $item
*/
foreach($this->items as $index => $item) {
if($item->getProduct()->id == $productId) {
$item->setAmountOfPersons($quantity);
if(isset($timeSlot)) {
$item->setSelectedTimeSlot($timeSlot);
}
$item->setNotification($notification ?? '');
$itemHasBeenUpdated = true;
break;
}
}
if($itemHasBeenUpdated && $this->useSession) {
if(count($this->items) == 0) {
$this->clear();
} else {
$this->saveSession();
}
return;
}
throw new \LogicException(self::class.": Activity with product id: '".$productId."' and location id: '".$locationId."' not found within the shopping cart.", 422);
}
/**
* Deletes an item from the shopping cart
*
* @param int $productId
*/
public function deleteItem(int $productId)
{
$itemHasBeenRemoved = false;
foreach($this->items as $index => $item) {
if($item->getProduct()->id == $productId) {
array_splice($this->items, $index, 1);
$itemHasBeenRemoved = true;
break;
}
}
if($itemHasBeenRemoved && $this->useSession) {
if(count($this->items) == 0) {
$this->clear();
} else {
$this->saveSession();
}
return;
}
throw new \LogicException(self::class.": Activity with product id: '".$productId."' not found within the shopping cart.", 422);
}
public function deleteAll()
{
unset($this->items);
}
/**
* Change the date of the shopping cart and also the date on the set availabilities
*
* @param Carbon $date
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function setDate(Carbon $date)
{
$this->date = $date;
foreach ($this->items as $item) {
// Store the selected timeslot
$selectedTimeSlot = $item->getSelectedTimeSlot();
$item->setDate($date->format('d-m-Y'));
// Generate new time slots for the shopping cart view
$item->timeSlots = $this->makeTimeSlots($item);
// If we didn't had a timeslot we can continue here
if(! isset($selectedTimeSlot)) {
continue;
}
// Else try to set the previous selected timeslot as active
$selectedSet = false;
foreach ($item->timeSlots as $timeSlot) {
if($timeSlot->getTimeSlotValue() == $selectedTimeSlot->getTimeSlotValue()) {
if($timeSlot->locked) {
continue;
}
$timeSlot->selected = true;
$selectedSet = true;
continue;
}
$timeSlot->selected = false;
}
// if(!$selectedSet) {
//
// debug('Previous selected time slot no found within the new time slots');
// $firstAvailableTimeSlot = $item->timeSlots->where('locked', false)->first();
// if(isset($firstAvailableTimeSlot)) {
// debug('First available option has been selected.');
// $item->setSelectedTimeSlot($firstAvailableTimeSlot->getTimeSlotValue());
// }
// else {
// debug('No availalbe options left, therefor the first option has been selected.');
// $item->setSelectedTimeSlot($item->timeSlots->first()->getTimeSlotValue());
// }
// }
}
if(! $this->useSession) {
return;
}
// session()->put(AvailabilityService::SEARCH_DATE_KEY, $date->format('d-m-Y'));
$this->saveSession();
}
/**
* Clears session variables values.
*/
public function clear(): void
{
// session()->remove(AvailabilityService::SEARCH_LOCATION_KEY);
// session()->remove(AvailabilityService::SEARCH_DATE_KEY);
$this->clearSession();
}
/**
* Returns the sum of all base prices of products.
* Remember the base price is in cents. And may be a float (e.g. value in cents with decimals)
*
* @param bool $formatted
* @return float|int
*/
public function getTotal(bool $formatted = true)
{
$price = 0;
if(count($this->items) > 0) {
foreach ($this->items as $index => $item) {
$price += $item->getTotal(true, false);
}
}
// Return formatted or not
if($formatted) {
return euro_pricing_format($price);
}
return $price;
}
/**
* Returns the sum of all base prices of products.
* Remember the base price is in cents. And may be a float (e.g. value in cents with decimals)
*
* @param bool $formatted
* @return float|int
*/
public function getTotalWithoutVat(bool $formatted = true)
{
$price = 0;
if(count($this->items) > 0) {
foreach ($this->items as $index => $item) {
$price += $item->getTotal(false, false);
}
}
// Return formatted or not
if($formatted) {
return euro_pricing_format($price);
}
return $price;
}
/**
* Returns the sum of the vat of all products.
* Remember this is in cents. And may be a float (e.g. value in cents with decimals).
*
* NOTE: We must first calculated the full price including vat then calculate the vat (the system excluding vat are rounded)
*
* @param bool $formatted
* @return float|int
*/
public function getVat(bool $formatted = true)
{
$total = $this->getTotal(false);
$totalWithoutVat = calculate_price_excluding_vat($total, config('site.vat_percentage'));
// Return formatted or not
if($formatted) {
return euro_pricing_format($total - $totalWithoutVat);
}
return $total - $totalWithoutVat;
}
/**
* Returns the deposit of the cart
* Remember this is in cents. And may be a float (e.g. value in cents with decimals).
*
* NOTE: We must first calculated the full price including vat then calculate the deposit through the set ratio.
*
* @param bool $formatted
* @return float|int
*/
public function getDeposit(bool $formatted = true)
{
$total = $this->getTotal(false);
$deposit = (int) round($total / 100 * config('site.deposit_percentage'));
// Return formatted or not
if($formatted) {
return euro_pricing_format($deposit);
}
return $deposit;
}
/**
* Get the amount of items in the shopping cart
*
* @return int
*/
public function getItemsCount(): int
{
return count($this->items);
}
/**
* Get all shopping cart items
*
* @return array
*/
public function getItems(): array
{
if(empty($this->items)) {
return [];
}
return $this->items;
}
/**
* Get the products of the cart items
*
* @return Collection
*/
public function getProducts()
{
$products = collect();
foreach ($this->getItems() as $item) {
$products->push($item->getProduct());
}
return $products;
}
/**
* determine if product is in cart
*
* @param int $productId
* @return bool
*/
public function hasProduct(int $productId): bool
{
foreach ($this->getItems() as $item) {
$itemProduct = $item->getProduct();
if($itemProduct->id == $productId) {
return true;
}
}
return false;
}
/**
* @return Carbon|null
*/
public function getDate(): ?Carbon
{
return $this->date;
}
/**
* @return Location|null
*/
public function getLocation(): ?Location
{
return $this->location;
}
/**
* Create a route parameter for products
* Will only return a filled string if date and location isset.
*
* @return string
*/
// public function getProductsRouteParameters(): string
// {
// if($this->getDate() !== null && $this->getLocation() !== null) {
// return '?' . __('site/availability.keys.' . AvailabilityService::SEARCH_LOCATION_KEY) . '=' . $this->getLocation()->translation->slug . '&' . __('site/availability.keys.' . AvailabilityService::SEARCH_DATE_KEY) . '=' . $this->getDate()->format('d-m-Y');
// }
// return '';
// }
/**
* Makes the time slots for a given duration
*
* @param Availability $availability
* @return Collection
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
private function makeTimeSlots(Availability $availability)
{
// Make sure that the location and the date is defined before calling the method
if(! isset($this->location) || ! isset($this->date)) {
throw new \UnderflowException(self::class.': The location and date should be defined before calling this method.', 500);
}
// Load the relation if needed
if(! $this->location->relationLoaded('availability')) {
$this->location->load('availability');
}
/** @var DayInfo $dayInfo */
$dayInfo = $this->location->availability->infoForDay($this->date);
// Make sure that the day is open
if(! $dayInfo->open) {
throw new \LogicException(self::class.": The chosen day isn't possible for the given location. This day option shouldn't even be possible to select.", 500);
}
/** @var AvailabilityService $availabilityService */
$availabilityService = app()->make(AvailabilityService::class);
$timeSlots = $availabilityService->makeTimeSlots($dayInfo, $availability->getProduct()->system_duration, $availability->getProduct()->duration, $availability);
return $timeSlots;
}
public function updateTimeSlots()
{
// Check that the items aren't zero
if(count($this->getItems()) == 0) {
return;
}
/** @var AvailabilityService $availabilityService */
$availabilityService = app()->make(AvailabilityService::class);
// Loop through the items
/** @var Availability $item */
foreach ($this->items as $item) {
// Loop through the time slots
/** @var TimeSlot $timeSlot */
foreach ($item->timeSlots as $timeSlot) {
$timeSlot->locked = ! $availabilityService->isTimeSlotAvailable($timeSlot, $item);
}
}
}
/**
* Check if the products marked as food exceed the activity products.
*
* @return bool
*/
public function isActivityPricingCorrectForFood(): bool
{
$foodPrice = 0;
$activityPrice = 0;
foreach ($this->items as $item) {
$product = $item->getProduct();
if(Product::$types[$product->product_type] == 'food') {
$foodPrice += $item->getTotal(true, false);
} else {
$activityPrice += $item->getTotal(true, false);
}
}
if($activityPrice >= 39000) {
return true;
}
if($foodPrice > $activityPrice) {
return false;
} else {
return true;
}
}
public function sortChronologically(): void
{
$this->items = collect($this->items)
->sortBy(function (Availability $availability) {
if (! $availability->getSelectedTimeSlot()) {
return 0;
}
return $availability->getSelectedTimeSlot()->start->getTimestamp();
})
->flatten()
->all();
}
// /**
// * It is possible the change the session variable of the date (and location) after setting it first.
// * For example when putting something into the cart then go back to searching for an other date and then return to the cart.
// * Therefor we valid that session values are up-to-date
// */
// public function updateSessionIfNeeded()
// {
// // Check if the date is still correct
// if($this->getDate()->format('d-m-Y') !== session()->get(AvailabilityService::SEARCH_DATE_KEY)) {
// session()->put(AvailabilityService::SEARCH_DATE_KEY, $this->getDate()->format('d-m-Y'));
// }
//
// // Check that the location is still correct
// if($this->getLocation()->id !== session()->get(AvailabilityService::SEARCH_LOCATION_KEY)) {
// session()->put(AvailabilityService::SEARCH_LOCATION_KEY, $this->getLocation()->id);
// }
// }
/**
* generate a link for the planning mail to generate a new cart
*
* @return string
*/
public function generateLink()
{
if(! isset($this->items) || count($this->items) === 0) {
return localized_route('shoppingCart');
}
$products = [];
// dd($this);
foreach($this->items as $orderLine) :
$object = new \stdClass();
$object->id = $orderLine->getProduct()->id;
if ($orderLine->getSelectedTimeSlot() !== null) {
$object->timeSlot = $orderLine->getSelectedTimeSlot()->getTimeSlotValue();
}
$object->persons = $orderLine->amountOfPersons;
$products[] = $object;
endforeach;
return route('shoppingCart.link', [
'date' => $this->date->format('d-m-Y'),
'location' => $this->location->id,
'products' => json_encode($products),
]);
}
public function toArray(): array
{
if(count($this->items) === 0) {
return [];
}
return [
'date' => isset($this->date) ? $this->date->toArray() : null,
'items' => array_map(function (Availability $availability) {
return $availability->toArray();
}, $this->items),
'location' => [
'id' => $this->location->id,
'name' => $this->location->translation->name,
],
'isActivityPricingCorrectForFood' => $this->isActivityPricingCorrectForFood(),
'total' => $this->getTotal(),
];
}
public function jsonSerialize()
{
return $this->toArray();
}
}