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/SBogers10/shop.komma.nl/app/Checkout/CheckoutService.php
<?php
namespace App\Checkout;


use App\Addresses\AddressService;
use App\Addresses\Models\Address;
use App\Cart\ShoppingCartInterface;
use App\Cart\ShoppingCartItem;
use App\Discounts\DiscountService;
use App\Finance\RoundingService;
use App\Orders\Product\OrderedProduct;
use App\Payment\PaymentService;
use App\Session\HasSessionTrait;
use App\Cart\ShoppingCart;
use App\Orders\InvoiceNumberSequence;
use App\Orders\Kms\OrderService;
use App\Orders\Models\Order;
use App\Orders\OrderNumberSequence;
use App\Orders\OrderStatus;
use App\Products\Product\Product;
use App\Products\Product\ProductModelService;
use App\Products\ProductComposite\ProductCompositeModelService;
use App\Products\ProductGroup\ProductGroupModelService;
use App\ShippingCosts\ShippingCostsService;
use App\Users\SiteUserInterface;
use App\Users\SiteUserRole;
use App\Vat\Models\VatScenario;
use App\Vat\VatRateTotal;
use App\Vat\VatScenarioEnum;
use App\Vat\VatService;
use Illuminate\Database\Eloquent\Collection as DatabaseCollection;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash;
use Komma\KMS\Globalization\Language;
use Komma\KMS\Globalization\RegionInfo;

/**
 * Class CheckoutService
 *
 * The checkout service sits between Order related, product related and payment related code.
 * It directs those domains to work together.
 *
 * @package App\Checkout
 */
class CheckoutService
{
    use HasSessionTrait;

    private OrderService $orderService;
    private ProductCompositeModelService $productCompositeService;
    private PaymentService $paymentService;
    private AddressService $addressService;
    private OrderNumberSequence $orderNumberGenerator;
    private ProductGroupModelService $productGroupService;
    private ProductModelService $productService;
    private VatService $vatService;
    private ShippingCostsService $shippingCostService;
    private InvoiceNumberSequence $invoiceNumberGenerator;
    private ?Address $shippingAddress;
    private ?Address $invoiceAddress;
    private ?SiteUserInterface $user;

    //The names of variables in this class to store and restore using a php session.
    protected array $variablesForSession = [
        'shippingAddress',
        'invoiceAddress',
        'user'
    ];

    private string $siteUserClass;
    private DiscountService $discountService;

    public function __construct()
    {
        $this->discountService = new DiscountService();
        $this->orderService = app(OrderService::class);
        $this->productService = new ProductModelService();
        $this->productGroupService = new ProductGroupModelService();
        $this->productCompositeService = new ProductCompositeModelService();
        $this->paymentService = new PaymentService();
        $this->addressService = new AddressService();
        $this->orderNumberGenerator = app(OrderNumberSequence::class);
        $this->invoiceNumberGenerator = app(InvoiceNumberSequence::class);
        $this->vatService = new VatService();
        $this->shippingCostService = new ShippingCostsService();
        $this->invoiceAddress = null;
        $this->shippingAddress = null;
        $this->user = null;
        $this->siteUserClass = get_class(app(SiteUserInterface::class));
        $this->restoreFromSession();
    }

    /**
     * Initializes a payment request for the user that owns the order.
     *
     * @param Order $order
     * @return RedirectResponse
     */
    public function startPaymentForOrder(Order $order): RedirectResponse
    {
        $pspAdapter = $this->paymentService->getAdapter();
        $transaction = $pspAdapter->createTransaction($order);
        return $pspAdapter->redirectForPayment($transaction);
    }

    /**
     * @param ShoppingCartInterface $shoppingCart
     * @param SiteUserInterface     $customer
     * @param Address               $addressForShipping
     * @param Address               $addressForInvoice
     *
     * @return Order|null
     */
    public function createOrder(ShoppingCartInterface $shoppingCart, SiteUserInterface $customer, Address $addressForShipping, Address $addressForInvoice): ?Order {
        $order = null;
        \DB::transaction(function () use ($shoppingCart, $customer, $addressForShipping, $addressForInvoice, &$order) {
            //Create the order, link customer and addresses to it and save it. We need to save it because the ordered productables need a order id
            /** @var ShippingCostsService $shippingCostsService */
            $shippingCostsService = app(ShippingCostsService::class);

            /** @var Order $order */
            $order = $this->orderService->newModel();
            $order->status = OrderStatus::NEW;
            $order->order_number = (string) $this->orderNumberGenerator->next();
            $order->invoice_number = (string) $this->invoiceNumberGenerator->next();
            $order->shipping_costs = 0;
            $order->remarks = '';

            //Attach the customer to the order and fill the order with address and customer details.
            $order->customer()->associate($customer);
            $this->fillOrderWithUserDetails($order, $customer);
            $this->fillOrderWithInvoiceAddressFromAddress($order, $addressForInvoice);
            $this->fillOrderWithShippingAddressFromAddress($order, $addressForShipping);
            $order->save();

            //Take the shipping costs into account.
            $shippingCosts = $shippingCostsService->get($shoppingCart, $addressForShipping);
            $this->vatService->calculateVatForModelWithVatScenarioEnum($shippingCosts);

            $this->discountService->applyDiscountsTo($shoppingCart);

            //Re-calculate the total inc and ex vat price of the shopping cart
            $subtotalInc = 0;
            $subtotalEx = 0;
            foreach($shoppingCart->getItems() as $item) {
                $this->discountService->applyDiscountsTo($item);
                $this->vatService->calculateVatForModelWithVatScenarioEnum($item);
                $subtotalInc += $item->getPriceInc();
                $subtotalEx += $item->getPriceEx();
            }

            $cartPriceModifications = $shoppingCart->getDiscountPriceMutations();
            $subtotalWithDiscountsEx = $subtotalEx;
            $subtotalWithDiscountsInc = $subtotalInc;
            foreach($cartPriceModifications as $cartPriceModification) {
                $subtotalWithDiscountsEx += $cartPriceModification->getPriceEx();
                $subtotalWithDiscountsInc += $cartPriceModification->getPriceInc();
            }

            //Create ordered productables and attach them to the order.
            $this->createOrderedProductablesForOrder($order, $shoppingCart);

            //Fill order model with financial data
            $order->subtotal = RoundingService::Round($subtotalWithDiscountsInc); //Inc vat, inc discounts
            $order->shipping_costs = RoundingService::Round($shippingCosts->getPriceInc());
            $order->total = RoundingService::Round($order->subtotal + $order->shipping_costs); //including vat

            //And finally save it
            $order->save();
        });

        return $order;
    }

    /**
     * @param Order                 $order
     * @param ShoppingCartInterface $cart
     */
    private function createOrderedProductablesForOrder(Order $order, ShoppingCartInterface $cart): void
    {
        //Loop over all shopping cart items and create ordered products for them. And link them to the order.
        foreach ($cart->getItems() as $shoppingCartItem) {
            /** @var ShoppingCartItem $shoppingCartItem */
            $productable = $shoppingCartItem->getProductable();

            switch (get_class($productable)) {
                case Product::class:
                    /** @var Product $productable */
                    $this->productService->createOrderedProductFromProduct($productable, $order, $shoppingCartItem->getQuantity());
                    break;
//                    case ProductGroup::class:
//                        /** @var ProductGroup $productable */
//                        $this->productGroupService->createOrderedProductGroupFromProductGroup($productable, $order, $shoppingCartItem->getQuantity());
//                        break;
//                    case ProductComposite::class:
//                        /** @var ProductComposite $productable */
//                        $this->productCompositeService->createOrderedProductCompositeProductComposite($productable, $order, $shoppingCartItem->getQuantity());
//                        break;
                default:
                    return;
            }
        };
    }

    /**
     * @param Order $order
     * @param Address $address
     */
    private function fillOrderWithShippingAddressFromAddress(Order $order, Address $address): void
    {
        foreach($order->shippingAttributeNames as $index => $attributeName) {
            $addressAttributeName = str_replace('shipping_', '', $attributeName);
            $order[$attributeName] = $address->$addressAttributeName;
        }
    }

    /**
     * @param Order $order
     * @param Address $address
     */
    private function fillOrderWithInvoiceAddressFromAddress(Order $order, Address $address): void
    {
        foreach($order->invoiceAttributeNames as $index => $attributeName) {
            $addressAttributeName = str_replace('invoice_', '', $attributeName);
            $order[$attributeName] = $address->$addressAttributeName;
        }
    }

    /**
     * @param Order    $order
     * @param SiteUserInterface $customer
     */
    private function fillOrderWithUserDetails(Order $order, SiteUserInterface $customer): void
    {
        foreach($order->userDetailAttributeNames as $attributeName) {
            $order[$attributeName] = $customer->$attributeName;
        }
    }

    /**
     * A previously used user is a SiteUserInterface that was being used in an earlier session.
     * That user will be returned if the given users e-mail matches and.
     *
     * @param SiteUserInterface      $user
     * @return SiteUserInterface|null
     */
    public function previouslyUsedUser(SiteUserInterface $user): ? SiteUserInterface
    {
        /** @var SiteUserInterface|null $previouslyUsedUser */
        if($previouslyUsedUser = $this->siteUserClass::where('email', '=', $user->email)->first()) {
            return $previouslyUsedUser;
        }
        return null;
    }

    /**
     * @param SiteUserInterface $prevUser
     * @param SiteUserInterface $user
     *
     * @return SiteUserInterface|null
     */
    public function updatePreviouslyUsedUserIfGuest(SiteUserInterface $prevUser, SiteUserInterface $user)
    {
        if(!$prevUser || !$prevUser->exists) throw new \RuntimeException('The user does not match a previously used guest user');

        if($prevUser->is_guest) {
            $prevUser->fill($user->getAttributes());
            $prevUser->save();
        }

        return $prevUser;
    }

    /**
     * Returns the last used shipping address for user or null if that not exists.
     *
     * @param SiteUserInterface $user
     * @return Address
     */
    public function getLastUsedShippingAddressForUser(SiteUserInterface $user): ? Address
    {
        $addressQuery = $user->addresses();

        /** @var Order $latestOrder */
        $latestOrder = $user->orders()->latest()->first();
        if(!$latestOrder) return null;

        $attributes = $latestOrder->getShippingAddressAttributes();
        foreach ($attributes as $attribute => $value) $addressQuery->where(str_replace('shipping_', '', $attribute), '=', $value);
        return $addressQuery->first();
    }

    /**
     * Returns the last used invoice address for the user or null if that not exists
     *
     * @param SiteUserInterface $user
     * @return Address
     * @see Address
     */
    public function getLastUsedInvoiceAddressForUser(SiteUserInterface $user): ? Address
    {
        $addressQuery = $user->addresses();

        /** @var Order $latestOrder */
        $latestOrder = $user->orders()->latest()->first();
        if(!$latestOrder) return null;

        $attributes = $latestOrder->getInvoiceAddressAttributes();
        foreach ($attributes as $attribute => $value) $addressQuery->where(str_replace('invoice_', '', $attribute), '=', $value);
        return $addressQuery->first();
    }


    /**
     * Clears session variables values.
     */
    public function clearCheckoutData(): void
    {
        $this->clearSession();
    }

    /**
     * @return Address
     */
    public function getShippingAddress(): ? Address
    {
        return $this->shippingAddress;
    }

    /**
     * @param Address|null $shippingAddress
     * @return CheckoutService
     */
    public function setShippingAddress(Address $shippingAddress = null): CheckoutService
    {
        $this->shippingAddress = $shippingAddress;
        $this->saveSession();
        return $this;
    }

    /**
     * @param Address|null $invoiceAddress
     * @return CheckoutService
     */
    public function setInvoiceAddress(Address $invoiceAddress = null): CheckoutService
    {
        $this->invoiceAddress = $invoiceAddress;
        $this->saveSession();
        return $this;
    }

    /**
     * @return Address
     */
    public function getInvoiceAddress(): ? Address
    {
        return $this->invoiceAddress;
    }

    /**
     * Gets the user that checks out.
     * @return SiteUserInterface|null
     */
    public function getUser(): ? SiteUserInterface
    {
        return $this->user;
    }

    /**
     * Get the user from the input.
     *
     * @return SiteUserInterface|null
     */
    public function getUserFromInput(): ?SiteUserInterface
    {
        /** @var null|RegionInfo $usersRegionInfo */
        $usersRegionInfo = RegionInfo::getWhere('threeLetterISORegionName', '=', request()->get('invoice_country'))->first();
        /** @var Language $language */
        $language = $usersRegionInfo->getLanguages()->first();
        $culture = '';
        if($language) $culture = mb_strtolower($language->getTwoLetterISOLanguageName()).'-';
        $culture .= mb_strtoupper($usersRegionInfo->getName());

        //Determine guest attributes
        $attributes = [
            'role'              => SiteUserRole::Customer,
            'email'             => request('email'),
            'password'          => Hash::make(''),
            'first_name'        => request('first_name'),
            'last_name_prefix'   => request('last_name_prefix'),
            'last_name'         => request('last_name'),
            'culture'           => $culture,
            'telephone'         => request('phone'),
            'is_guest'          => 1,
        ];

        return new $this->siteUserClass($attributes);
    }
    /**
     * Use the form input to get a user that previously ordered if possible. Else we return null
     *
     * @return SiteUserInterface|null
     */
    public function getPreviouslyUsedUser(): ? SiteUserInterface
    {
        //If we know his e-mail address we try to get his previously used guest user. If it does not exist, use the existing guest user.
        if(request()->has('email') && (!$this->user || !$this->user->exists)) {
            return $this->siteUserClass::where('email', '=', request()->get('email'))->first();
            if($previouslyUsedUser) return $previouslyUsedUser;
        }
        return null;
    }

    /**
     * Returns a collection of VatRateTotal models which hold information about vat totals by scenario
     *
     * @param ShoppingCartInterface $shoppingCart
     *
     * @return Collection|VatRateTotal[]
     */
    public function getVatRateTotalsFromShoppingCart(ShoppingCartInterface $shoppingCart): Collection
    {
        //Group the shopping cart items by their vat scenario first. Throw an error when they don't have a vat_scenario_enum.
        $vatRateTotals = collect($shoppingCart->getItems())->groupBy(function(ShoppingCartItem $item) {
            if(is_null($item->getProductable()->vat_scenario_enum))
                throw new \RuntimeException('Encountered a shopping cart item with productable of class "'.get_class($item->getProductable()).'" without a vat scenario');

            return $item->getProductable()->vat_scenario_enum;
        })
        //Calculate their vat totals.
        ->map(function(Collection $shoppingCartItems, $vatEnumValue) {
            $vatTotal = $shoppingCartItems->sum(function (ShoppingCartItem $shoppingCartItem) {
                return $shoppingCartItem->getVatAmount();
            });

            //Then return a vatRateTotal model which carries the data
            return (new VatRateTotal())
                ->setName(__('shop/vatrates.scenarios.'.$vatEnumValue.'.rate_name'))
                ->setVatScenarioEnum($vatEnumValue)
                ->setVatTotal($vatTotal);
        });

        return $vatRateTotals;
    }

    /**
     * @param SiteUserInterface $user
     * @return CheckoutService
     */
    public function setUser(SiteUserInterface $user): CheckoutService
    {
        $this->user = $user;
        $this->saveSession();
        return $this;
    }

    /**
     * @param Collection $vatRateTotals
     * @return VatRateTotal|null
     */
    public function getVatRateTotalHavingTheHighestVatPercentage(Collection $vatRateTotals):? VatRateTotal
    {
        //Get the vat amount from the shipping costs and add them to the highest vat amount
        /** @var VatRateTotal|null $highestVatRateTotal */
        $highestVatRateTotal = null;
        $highestVatRatePercentage = 0;
        $vatRateTotals->each(function (VatRateTotal $vatRateTotal) use(&$highestVatRateTotal, &$highestVatRatePercentage) {
            $percentage = config('shop.vat.scenarios')[$vatRateTotal->getVatScenarioEnum()]['percentage'];
            if($percentage > $highestVatRatePercentage) {
                $highestVatRateTotal = $vatRateTotal;
                $highestVatRatePercentage = $percentage;
            }
        });

        return $highestVatRateTotal;
    }

    /**
     * Returns a collection of VatRateTotal models which hold information about vat totals by scenario
     *
     * @param Order $order
     *
     * @return DatabaseCollection|VatRateTotal[]
     */
    public function getVatRateTotalsFromOrder(Order $order): Collection
    {
        //Eager load stuff
        $order->load('orderedProducts');

        $vatRateTotals = $this->getVatRateTotalsFromOrderedProducts($order);
        //TODO. Consider if we are going to use productGroups and productComposites too. If we do, create methods for them too.
        //These vat rate totals then all may be merged and returned as one collection.

        //Get the productable with highest vat percentage from the ordered productables. Remember that that ordered productable does has a vat_scenario_enum.
        //We are going to need that in the step after this one.
        $orderedProduct = $this->orderService->getOrderedProductWithHighestVatPercentage($order);
        //TODO. Same remark as above. But we need to determine which of the ordered productables has the highest vat percentage.

        //Get the vatRateTotal that belongs to the vat_scenario_enum that has the highest vat percentage in the ordered productables.
        $highestVatRateTotal = $vatRateTotals->get($orderedProduct->vat_scenario_enum);

        //Add the vat amount of the shipping costs to the vat amount of the VatRateTotal that has the highest vat percentage.
        if($highestVatRateTotal) {
            /** @var VatRateTotal $highestVatRateTotal */
            $highestVatRateTotal->setVatTotal(RoundingService::RoundVat($highestVatRateTotal->getVatTotal() + $this->vatService->calculateVatRateAmountFromIncAmount($order->shipping_costs)));
        }

        return $vatRateTotals;
    }

    /**
     * Uses the ordered products vat_scenario_enums to build vatRateTotals.
     *
     * First it uses the vat_scenario_enum of orderedProducts to get the appropriate vatScenario.
     * Then it overrides the percentage of the vatScenario with the one of an ordered product.
     *
     * Then it creates vatRateTotals, keyed / indexed by vat_scenario_enum. That get filled with the vatAmount
     * for each orderedProduct using its vatScenario that was determined earlier.
     *
     * The collection you'll get is keyed / indexed by vatScenerioEnum. Each value will be a vatRateTotal
     *
     * @param Order $order
     * @return Collection
     */
    private function getVatRateTotalsFromOrderedProducts(Order $order): Collection
    {
        $vatService = $this->vatService;

        //Group the shopping cart items by their vat scenario first. Throw an error when they don't have a vat_scenario_enum.
        $orderedProducts = $order->orderedProducts->groupBy('vat_scenario_enum')
        //Calculate their vat totals.
        ->mapWithKeys(function( $orderedProducts, $vatScenarioEnum) use($vatService, $order) {
            //Sum the vat amounts for each ordered product.
            $highestVatPercentage = null;

            //Get the vatScenario, based on the vat scenario enum. And override the vat percentage with the one from the ordered.
            /** @var VatScenario $vatScenario */
            $vatScenario = $vatService->getAllVatScenarios()->where('enumValue', $vatScenarioEnum)->first();
            if(!$vatScenario) throw new \RuntimeException('The vat_scenario_enum value of ordered products from order with id' . $order->id.'" was not a valid one. Make sure it is one of the '.VatScenarioEnum::class.' values');

            $vatRateTotal = $orderedProducts->sum(function (OrderedProduct $orderedProduct) use($vatService, &$allEncounteredVatScenarios, $vatScenario) {
                //Use the vatScenario to fill the orderedProduct with vat information. But use the vat_percentage from the ordered product.
                $vatScenario->percentage = $orderedProduct->vat_percentage;
                $vatService->calculateVatForModelUsingVatScenario($orderedProduct, $vatScenario);

                //Return the vatAmount in cents
                return $orderedProduct->getVatAmount();
            });

            //Then return a vatRateTotal model which carries the data
            $vatRateTotal = (new VatRateTotal())
                ->setName(__('shop/vatrates.scenarios.'.$vatScenarioEnum.'.rate_name'))
                ->setVatScenarioEnum($vatScenarioEnum)
                ->setVatTotal($vatRateTotal);

            return [$vatScenarioEnum => $vatRateTotal];
        });

        return $orderedProducts;
    }
}