HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/farmfun/reserveren.farmfun.be/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();
    }
}