inklabs/kommerce-core

View on GitHub
src/Service/OrderService.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
namespace inklabs\kommerce\Service;

use inklabs\kommerce\Entity\Attachment;
use inklabs\kommerce\Entity\Product;
use inklabs\kommerce\Entity\User;
use inklabs\kommerce\EntityDTO\OrderItemQtyDTO;
use inklabs\kommerce\Entity\Cart;
use inklabs\kommerce\Entity\CreditCard;
use inklabs\kommerce\Entity\CreditPayment;
use inklabs\kommerce\Entity\Order;
use inklabs\kommerce\Entity\OrderAddress;
use inklabs\kommerce\Entity\OrderItem;
use inklabs\kommerce\Entity\OrderStatusType;
use inklabs\kommerce\Entity\Shipment;
use inklabs\kommerce\Entity\ShipmentCarrierType;
use inklabs\kommerce\Entity\ShipmentComment;
use inklabs\kommerce\Entity\ShipmentItem;
use inklabs\kommerce\Entity\ShipmentTracker;
use inklabs\kommerce\EntityRepository\OrderItemRepositoryInterface;
use inklabs\kommerce\EntityRepository\OrderRepositoryInterface;
use inklabs\kommerce\EntityRepository\ProductRepositoryInterface;
use inklabs\kommerce\Event\OrderCreatedFromCartEvent;
use inklabs\kommerce\Lib\CartCalculatorInterface;
use inklabs\kommerce\Lib\Event\EventDispatcherInterface;
use inklabs\kommerce\Lib\PaymentGateway\ChargeRequest;
use inklabs\kommerce\Lib\PaymentGateway\PaymentGatewayInterface;
use inklabs\kommerce\Lib\ReferenceNumber\ReferenceNumberGeneratorInterface;
use inklabs\kommerce\Lib\ShipmentGateway\ShipmentGatewayInterface;
use inklabs\kommerce\Lib\Uuid;
use inklabs\kommerce\Lib\UuidInterface;

class OrderService implements OrderServiceInterface
{
    use EntityValidationTrait;

    /** @var EventDispatcherInterface */
    private $eventDispatcher;

    /** @var InventoryServiceInterface */
    private $inventoryService;

    /** @var OrderRepositoryInterface */
    private $orderRepository;

    /** @var ProductRepositoryInterface */
    private $productRepository;

    /** @var ShipmentGatewayInterface */
    private $shipmentGateway;

    /** @var OrderItemRepositoryInterface */
    private $orderItemRepository;

    /** @var PaymentGatewayInterface */
    protected $paymentGateway;

    /** @var ReferenceNumberGeneratorInterface */
    private $referenceNumberGenerator;

    public function __construct(
        EventDispatcherInterface $eventDispatcher,
        InventoryServiceInterface $inventoryService,
        OrderRepositoryInterface $orderRepository,
        OrderItemRepositoryInterface $orderItemRepository,
        PaymentGatewayInterface $paymentGateway,
        ProductRepositoryInterface $productRepository,
        ShipmentGatewayInterface $shipmentGateway,
        ReferenceNumberGeneratorInterface $referenceNumberGenerator
    ) {
        $this->eventDispatcher = $eventDispatcher;
        $this->inventoryService = $inventoryService;
        $this->orderRepository = $orderRepository;
        $this->orderItemRepository = $orderItemRepository;
        $this->productRepository = $productRepository;
        $this->paymentGateway = $paymentGateway;
        $this->shipmentGateway = $shipmentGateway;
        $this->referenceNumberGenerator = $referenceNumberGenerator;
    }

    public function update(Order & $order): void
    {
        $this->orderRepository->update($order);
        $this->eventDispatcher->dispatchEvents($order->releaseEvents());
    }

    public function buyShipmentLabel(
        UuidInterface $orderId,
        OrderItemQtyDTO $orderItemQtyDTO,
        string $comment,
        string $rateExternalId,
        string $shipmentExternalId
    ): void {
        $order = $this->orderRepository->findOneById($orderId);

        $shipmentTracker = $this->shipmentGateway->buy(
            $shipmentExternalId,
            $rateExternalId
        );

        $this->addShipment($comment, $orderItemQtyDTO, $shipmentTracker, $order);
    }

    public function addShipmentTrackingCode(
        UuidInterface $orderId,
        OrderItemQtyDTO $orderItemQtyDTO,
        string $comment,
        int $shipmentCarrierTypeId,
        string $trackingCode
    ): void {
        $order = $this->orderRepository->findOneById($orderId);

        $shipmentCarrierType = ShipmentCarrierType::createById($shipmentCarrierTypeId);
        $shipmentTracker = new ShipmentTracker($shipmentCarrierType, $trackingCode);

        $this->addShipment($comment, $orderItemQtyDTO, $shipmentTracker, $order);
    }

    private function addShipment(
        string $comment,
        OrderItemQtyDTO $orderItemQtyDTO,
        ShipmentTracker $shipmentTracker,
        Order $order
    ): void {
        $shipment = new Shipment;

        $shipmentTracker->setShipment($shipment);

        if ($comment !== '') {
            new ShipmentComment($shipment, $comment);
        }

        $this->addShipmentItemsFromOrderItems($orderItemQtyDTO, $shipment);
        $this->shipProductsFromInventory($order, $orderItemQtyDTO);

        $order->addShipment($shipment);
        $this->update($order);
    }

    private function addShipmentItemsFromOrderItems(OrderItemQtyDTO $orderItemQtyDTO, Shipment $shipment): void
    {
        foreach ($orderItemQtyDTO->getItems() as $orderItemId => $quantity) {
            $orderItem = $this->orderItemRepository->findOneById(Uuid::fromString($orderItemId));

            new ShipmentItem($shipment, $orderItem, $quantity);
        }
    }

    public function setOrderStatus(UuidInterface $orderId, OrderStatusType $orderStatusType): void
    {
        $order = $this->orderRepository->findOneById($orderId);
        $order->setStatus($orderStatusType);
        $this->update($order);

        if ($orderStatusType->isFinished()) {
            $this->lockOrderAttachments($order);
        }
    }

    private function lockOrderAttachments(Order $order): void
    {
        foreach ($order->getOrderItems() as $orderItem) {
            $this->lockOrderItemAttachments($orderItem);
        }
    }

    private function lockOrderItemAttachments(OrderItem $orderItem): void
    {
        foreach ($orderItem->getAttachments() as $attachment) {
            $this->lockAttachment($attachment);
        }
    }

    private function lockAttachment(Attachment $attachment): void
    {
        if (! $attachment->isLocked()) {
            $attachment->setLocked();
            $this->updateAttachment($attachment);
        }
    }

    private function updateAttachment(Attachment & $attachment): void
    {
        // TODO: Move this responsibility to the Attachment Service without a cyclic dependency
        $this->orderRepository->update($attachment);
    }

    public function createOrderFromCart(
        UuidInterface $orderId,
        User $user,
        Cart $cart,
        CartCalculatorInterface $cartCalculator,
        string $ip4,
        OrderAddress $shippingAddress,
        OrderAddress $billingAddress,
        CreditCard $creditCard
    ): Order {
        $this->throwValidationErrors($creditCard);

        $order = Order::fromCart($orderId, $user, $cart, $cartCalculator, $ip4);
        $order->setShippingAddress($shippingAddress);
        $order->setBillingAddress($billingAddress);

        $this->throwValidationErrors($order);

        $order->setReferenceNumber(
            $this->referenceNumberGenerator->generate()
        );

        $this->reserveProductsFromInventory($order);
        $this->addCreditCardPayment($order, $creditCard, $order->getTotal()->total);

        $this->orderRepository->create($order);

        $this->eventDispatcher->dispatchEvent(
            new OrderCreatedFromCartEvent($order->getId())
        );

        return $order;
    }

    public function addCreditCardPayment(Order $order, CreditCard $creditCard, int $amount): void
    {
        $chargeRequest = new ChargeRequest;
        $chargeRequest->setCreditCard($creditCard);
        $chargeRequest->setAmount($amount);
        $chargeRequest->setCurrency('usd');
        $chargeRequest->setDescription($order->getShippingAddress()->getEmail());

        $chargeResponse = $this->paymentGateway->getCharge($chargeRequest);

        $payment = new CreditPayment($chargeResponse);

        $this->throwValidationErrors($payment);
        $order->addPayment($payment);
    }

    private function reserveProductsFromInventory(Order $order): void
    {
        foreach ($order->getOrderItems() as $orderItem) {
            $this->inventoryService->reserveProductForOrder(
                $order,
                $orderItem->getProduct(),
                $orderItem->getQuantity()
            );

            $this->reduceProductInventory(
                $orderItem->getProduct(),
                $orderItem->getQuantity()
            );

            foreach ($orderItem->getOrderItemOptionProducts() as $orderItemOptionProduct) {
                $this->inventoryService->reserveProductForOrder(
                    $order,
                    $orderItemOptionProduct->getOptionProduct()->getProduct(),
                    $orderItem->getQuantity()
                );

                $this->reduceProductInventory(
                    $orderItemOptionProduct->getOptionProduct()->getProduct(),
                    $orderItem->getQuantity()
                );
            }
        }
    }

    private function shipProductsFromInventory(Order $order, OrderItemQtyDTO $orderItemQtyDTO): void
    {
        foreach ($order->getOrderItems() as $orderItem) {
            $quantity = $orderItemQtyDTO->getItemQuantity($orderItem->getId());
            if ($quantity !== null) {
                $this->inventoryService->shipProductForOrderItem(
                    $orderItem,
                    $orderItem->getProduct(),
                    $quantity
                );

                $this->reduceProductInventory(
                    $orderItem->getProduct(),
                    $quantity
                );

                foreach ($orderItem->getOrderItemOptionProducts() as $orderItemOptionProduct) {
                    $this->inventoryService->shipProductForOrderItem(
                        $orderItem,
                        $orderItemOptionProduct->getOptionProduct()->getProduct(),
                        $quantity
                    );

                    $this->reduceProductInventory(
                        $orderItemOptionProduct->getOptionProduct()->getProduct(),
                        $quantity
                    );
                }
            }
        }
    }

    private function reduceProductInventory(Product $product, int $quantity): void
    {
        $product->reduceQuantity($quantity);
        $this->productRepository->update($product);
    }
}