<?php declare(strict_types=1);
namespace App\Order\Service;
use App\Core\Service\ParameterService;
use App\Customer\Entity\BillingAddress;
use App\Customer\Entity\Customer;
use App\Customer\Entity\StreetAddress;
use App\Delivery\Entity\DeliverySchedule;
use App\Delivery\Service\DeliveryScheduleService;
use App\Delivery\Service\DeliveryZoneService;
use App\Order\Entity\Cart;
use App\Order\Entity\CartItem;
use App\Order\Entity\Order;
use App\Order\Exception\InvalidAddressException;
use App\Order\Repository\CartRepository;
use App\Product\Entity\Product;
use App\Stock\Service\StockService;
use Doctrine\ORM\EntityNotFoundException;
use Exception;
class CartService
{
private CartRepository $repository;
private ParameterService $parameterService;
private DeliveryZoneService $deliveryZoneService;
private StockService $stockService;
private DeliveryScheduleService $deliveryScheduleService;
public function __construct(
CartRepository $repository,
ParameterService $parameterService,
DeliveryZoneService $deliveryZoneService,
DeliveryScheduleService $deliveryScheduleService,
StockService $stockService
) {
$this->repository = $repository;
$this->parameterService = $parameterService;
$this->deliveryZoneService = $deliveryZoneService;
$this->deliveryScheduleService = $deliveryScheduleService;
$this->stockService = $stockService;
}
public function getCartById(int $id): Cart
{
$cart = $this->repository->findOneBy(['id' => $id]);
if ($cart === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(Cart::class, [$id]);
}
return $cart;
}
public function getCartsByCustomer(Customer $customer): array
{
return $this->repository->findBy([
'customer' => $customer
]);
}
public function createCart(): Cart
{
$cart = new Cart();
$this->repository->add($cart, true);
return $cart;
}
public function updateCartCustomer(Cart $cart, Customer $customer): void
{
$cart->setCustomer($customer);
$cart->setShippingAddress(null);
$this->repository->flush();
}
public function updateCartShippingAddress(Cart $cart, StreetAddress $address): void
{
$customer = $cart->getCustomer();
if ($customer === null) {
throw new InvalidAddressException();
}
if ($customer !== $address->getCustomer()) {
throw new InvalidAddressException();
}
$cart->setShippingAddress($address);
// reset schedule
$cart->setDeliveryDate(null);
$cart->setDeliveryStartTime(null);
$cart->setDeliveryEndTime(null);
// update the shipping cost
$this->updateShippingCost($cart);
$this->repository->flush();
}
public function updateCartBillingAddress(Cart $cart, BillingAddress $address): void
{
$customer = $cart->getCustomer();
if ($customer === null) {
throw new InvalidAddressException();
}
if ($customer !== $address->getCustomer()) {
throw new InvalidAddressException();
}
$cart->setBillingAddress($address);
$this->repository->flush();
}
public function updateCartDeliverySchedule(Cart $cart, DeliverySchedule $deliverySchedule): void
{
// TODO: MAke sure this schedule is valid for the current address ??
$cart->setDeliverySchedule($deliverySchedule);
$cart->setDeliveryStartTime($deliverySchedule->getStart());
$cart->setDeliveryEndTime($deliverySchedule->getEnd());
// calculate the actual delivery date
$deliveryDate = $this->deliveryScheduleService->getActualDateForSchedule($deliverySchedule);
$cart->setDeliveryDate($deliveryDate);
$this->repository->flush();
}
public function updateCartDeliveryCustomSchedule(Cart $cart, array $schedule): void
{
$cart->setDeliverySchedule(null);
$cart->setDeliveryDate($schedule['date']);
$cart->setDeliveryStartTime($schedule['start']);
$cart->setDeliveryEndTime($schedule['end']);
$this->repository->flush();
}
public function updateCartOrderType(Cart $cart, string $orderType): void
{
$cart->setOrderType($orderType);
$this->repository->flush();
}
/**
* Add product to cart.
*/
public function add(Cart $cart, Product $product, int $quantity): void
{
// add the item
$this->addItemInternal($cart, $product, $quantity);
// update shipping cost if required
$this->updateShippingCost($cart);
// Persist and flush related entities
$this->repository->flush();
}
/**
* Remove product from Cart.
*/
public function remove(Cart $cart, Product $product, int $quantity): void
{
$this->removeItemInternal($cart, $product, $quantity);
$this->updateShippingCost($cart);
// Persist and flush related entities
$this->repository->flush();
}
/**
* @throws Exception
*
* @return array<int, array<string, mixed>>
*/
public function getDiscountsForOrder(Cart $cart): array
{
$discounts = [];
if ($cart->getOrderType() === Order::OrderTypeSupport) {
$discounts[] = [
'name' => 'Descuento reposición',
'amount' => $cart->getSubtotal(),
];
}
return $discounts;
}
/**
* @return string[]
*/
public function getValidationErrors(Cart $cart): array
{
$errors = [];
// support orders have no associated costs
if ($cart->getOrderType() === Order::OrderTypeSupport) {
return $errors;
}
$maxNoBilling = $this->parameterService->getParameterValue('order.max_no_billing');
$minSale = $this->parameterService->getParameterValue('order.min_sale');
$subtotal = $cart->getSubtotal();
$discounts = $this->getDiscountsForOrder($cart);
$discountsTotal = 0;
foreach ($discounts as $discount) {
$discountsTotal += $discount['amount'];
}
$total = $subtotal + $discountsTotal;
if ($total >= $maxNoBilling && (!$cart->getBillingAddress() || !$cart->getBillingAddress()->getCuit())) {
$errors[] = 'error.order.billing_required';
}
if ($subtotal < $minSale) {
$errors[] = 'error.order.total_below_minimum_sale';
}
// check that products in the cart are in stock
foreach ($cart->getItems() as $item) {
$publicSale = $item->getProduct()->getPublicSale();
$stock = $this->stockService->getStockByProduct($item->getProduct());
if ($stock && (!$publicSale || $item->getQuantity() > $stock->getCurrentStock())) {
$errors[] = 'error.order.out_of_stock';
break;
}
}
return $errors;
}
public function deleteCart(Cart $cart): void
{
$this->repository->remove($cart);
$this->repository->flush();
}
private function addItemInternal(Cart $cart, Product $product, int $quantity): CartItem
{
// Find an existing cart item
$cartItem = $cart->getItem($product);
// If it wasn't found, create a new one
if ($cartItem === null) {
$cartItem = new CartItem();
$cartItem->setProduct($product);
$cartItem->setQuantity($quantity);
// Add it to the cart
$cart->addItem($cartItem);
} else {
$cartItem->setQuantity($quantity + $cartItem->getQuantity());
}
return $cartItem;
}
private function removeItemInternal(Cart $cart, Product $product, int $quantity): void
{
if ($quantity < -1) {
return;
}
// Find an existing cart item
$cartItem = $cart->getItem($product);
if ($cartItem === null) {
return;
}
$newQuantity = $quantity === -1 ? 0 : $cartItem->getQuantity() - $quantity;
// Either update or remove
if ($newQuantity > 0) {
// Change the quantity on the item itself
$cartItem->setQuantity($newQuantity);
} else {
$cart->removeItem($cartItem);
}
}
private function updateShippingCost(Cart $cart): void
{
if ($cart->getShippingAddress() == null) {
$cart->setShippingCartProduct(null);
return;
}
$freeShippingMinimum = $this->parameterService->getParameterValue('order.free_shipping');
$zone = $this->deliveryZoneService->getZoneForAddress($cart->getShippingAddress());
if ($zone == null) {
$cart->setShippingCartProduct(null);
return;
}
$zoneShippingProduct = $zone->getShippingCostProduct();
$cartShippingProduct = $cart->getShippingCartProduct();
// if products subtotal is over the free shipping threshold, remove the automatic shipping cost product
if ($cart->getProductsSubtotal() > $freeShippingMinimum) {
if ($cartShippingProduct != null) {
$this->removeItemInternal($cart, $cartShippingProduct, 1);
$cart->setShippingCartProduct(null);
}
} else {
// not reached the free threshold
if ($cartShippingProduct !== $zoneShippingProduct) {
if ($cartShippingProduct != null) {
$this->removeItemInternal($cart, $cartShippingProduct, 1);
$cart->setShippingCartProduct(null);
}
if ($zoneShippingProduct != null) {
$this->addItemInternal($cart, $zoneShippingProduct, 1);
$cart->setShippingCartProduct($zoneShippingProduct);
}
}
}
}
}