Newer
Older
TillQliro / Model / Management / CheckoutStatus.php
@Jonas Jonsson Jonas Jonsson on 2 Apr 2024 9 KB Initial
<?php
/**
 * Copyright © Qliro AB. All rights reserved.
 * See LICENSE.txt for license details.
 */

namespace Qliro\QliroOne\Model\Management;

use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Sales\Api\OrderRepositoryInterface;
use Qliro\QliroOne\Api\Client\MerchantInterface;
use Qliro\QliroOne\Api\Data\CheckoutStatusInterface as CheckoutStatusInterfaceAlias;
use Qliro\QliroOne\Api\Data\CheckoutStatusInterface;
use Qliro\QliroOne\Api\Data\CheckoutStatusResponseInterface;
use Qliro\QliroOne\Api\Data\CheckoutStatusResponseInterfaceFactory;
use Qliro\QliroOne\Api\LinkRepositoryInterface;
use Qliro\QliroOne\Model\Logger\Manager as LogManager;
use Qliro\QliroOne\Model\ResourceModel\Lock;
use Qliro\QliroOne\Model\Exception\TerminalException;
use Qliro\QliroOne\Model\Exception\FailToLockException;

/**
 * QliroOne management class
 */
class CheckoutStatus extends AbstractManagement
{
    /**
     * @var \Qliro\QliroOne\Api\Client\MerchantInterface
     */
    private $merchantApi;

    /**
     * @var \Qliro\QliroOne\Api\LinkRepositoryInterface
     */
    private $linkRepository;

    /**
     * @var \Qliro\QliroOne\Model\Logger\Manager
     */
    private $logManager;

    /**
     * @var \Qliro\QliroOne\Model\ResourceModel\Lock
     */
    private $lock;

    /**
     * @var \Magento\Sales\Api\OrderRepositoryInterface
     */
    private $orderRepository;

    /**
     * @var \Qliro\QliroOne\Api\Data\CheckoutStatusResponseInterfaceFactory
     */
    private $checkoutStatusResponseFactory;

    /**
     * @var PlaceOrder
     */
    private $placeOrder;
    /**
     * @var QliroOrder
     */
    private $qliroOrder;

    /**
     * Inject dependencies
     *
     * @param MerchantInterface $merchantApi
     * @param CheckoutStatusResponseInterfaceFactory $checkoutStatusResponseFactory
     * @param LinkRepositoryInterface $linkRepository
     * @param OrderRepositoryInterface $orderRepository
     * @param LogManager $logManager
     * @param Lock $lock
     * @param PlaceOrder $placeOrder
     * @param QliroOrder $qliroOrder
     */
    public function __construct(
        MerchantInterface $merchantApi,
        CheckoutStatusResponseInterfaceFactory $checkoutStatusResponseFactory,
        LinkRepositoryInterface $linkRepository,
        OrderRepositoryInterface $orderRepository,
        LogManager $logManager,
        Lock $lock,
        PlaceOrder $placeOrder,
        QliroOrder $qliroOrder
    ) {
        $this->merchantApi = $merchantApi;
        $this->linkRepository = $linkRepository;
        $this->logManager = $logManager;
        $this->lock = $lock;
        $this->orderRepository = $orderRepository;
        $this->checkoutStatusResponseFactory = $checkoutStatusResponseFactory;
        $this->placeOrder = $placeOrder;
        $this->qliroOrder = $qliroOrder;
    }

    /**
     * @param CheckoutStatusInterfaceAlias $checkoutStatus
     * @return \Qliro\QliroOne\Api\Data\CheckoutStatusResponseInterface
     */
    public function update(CheckoutStatusInterface $checkoutStatus)
    {
        $qliroOrderId = $checkoutStatus->getOrderId();
        $logContext = [
            'extra' => [
                'qliro_order_id' => $qliroOrderId,
            ],
        ];

        try {
            if (!$this->lock->lock($qliroOrderId)) {
                throw new FailToLockException(__('Failed to aquire lock when placing order'));
            }

            try {
                $link = $this->linkRepository->getByQliroOrderId($qliroOrderId);
            } catch (NoSuchEntityException $exception) {
                $this->handleOrderCancelationIfRequired($checkoutStatus);
                throw $exception;
            }

            $this->logManager->setMerchantReference($link->getReference());

            $link->setQliroOrderStatus($checkoutStatus->getStatus());
            $this->linkRepository->save($link);

            $orderId = $link->getOrderId();

            if (empty($orderId)) {
                /*
                 * First major scenario:
                 * There is not yet any Magento order. Attempt to create the order, placeOrder()
                 * will process the created order based on the QliroOne order status as found in the link.
                 */

                try {
                    // TODO: the quote is still active, so the shopper might be adding more items
                    // TODO: without knowing that there is no order yet

                    $curTimeStamp = time();
                    $tooEarly = false;
                    $placedTimeStamp = strtotime($link->getPlacedAt());
                    $updTimeStamp = strtotime($link->getUpdatedAt());
                    if ($placedTimeStamp && $curTimeStamp < $placedTimeStamp + self::QLIRO_POLL_VS_CHECKOUT_STATUS_TIMEOUT) {
                        $tooEarly = true;
                    }
                    if ($curTimeStamp < $updTimeStamp + self::QLIRO_POLL_VS_CHECKOUT_STATUS_TIMEOUT_FINAL) {
                        $tooEarly = true;
                    }

                    if (!$tooEarly) {
                        $responseContainer = $this->merchantApi->getOrder($qliroOrderId);
                        $this->placeOrder->execute($responseContainer);

                        $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_RECEIVED);
                    } else {
                        $this->logManager->notice(
                            'checkoutStatus received to early, responding with order not found',
                            $logContext
                        );

                        $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND);
                    }

                } catch (FailToLockException $exception) {
                    /*
                     * As the lock was removed from placeOrder, this can no longer trigger, keeping it anyway
                     * Someone else is creating the order at the moment. Let Qliro try again in a few minutes.
                     */
                    $this->logManager->critical($exception, $logContext);

                    $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND);
                } catch (\Exception $exception) {
                    $this->logManager->critical($exception, $logContext);

                    $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND);
                }
            } else {
                /*
                 * Second major scenario:
                 * The order already exists; just update the order with the new QliroOne order status
                 */
                if ($this->placeOrder->applyQliroOrderStatus($this->orderRepository->get($orderId))) {
                    $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_RECEIVED);
                } else {
                    $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND);
                }
            }
            $this->lock->unlock($qliroOrderId);

        } catch (NoSuchEntityException $exception) {
            /* no more qliro pushes should be sent */
            $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_RECEIVED);

        } catch (FailToLockException $exception) {
            /*
             * Someone else is creating the order at the moment. Let Qliro try again in a few minutes.
             */
            $this->logManager->critical($exception, $logContext);
            $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND);

        } catch (\Exception $exception) {
            $this->logManager->critical($exception, $logContext);
            $response = $this->checkoutStatusRespond(CheckoutStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND);

        }

        return $response;
    }

    /**
     * Special case is processed here:
     * When the QliroOne order is not found, among active links, but push notification updates
     * status to "Completed", we want to find an inactive link and cancel such QliroOne order,
     * because Magento has previously failed creating corresponding order for it.
     *
     * @param CheckoutStatusInterfaceAlias $checkoutStatus
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     * @throws \Magento\Framework\Exception\AlreadyExistsException
     */
    private function handleOrderCancelationIfRequired(CheckoutStatusInterface $checkoutStatus)
    {
        $qliroOrderId = $checkoutStatus->getOrderId();

        if ($checkoutStatus->getStatus() === CheckoutStatusInterface::STATUS_COMPLETED) {
            $link = $this->linkRepository->getByQliroOrderId($qliroOrderId, false);

            try {
                $this->logManager->setMerchantReference($link->getReference());
                $link->setQliroOrderStatus($checkoutStatus->getStatus());
                $this->qliroOrder->cancel($link->getQliroOrderId());
                $link->setMessage(sprintf('Requested to cancel QliroOne order #%s', $link->getQliroOrderId()));
            } catch (TerminalException $exception) {
                $link->setMessage(sprintf('Failed to cancel QliroOne order #%s', $link->getQliroOrderId()));
            }

            $this->linkRepository->save($link);
        }
    }

    /**
     * @param string $result
     * @return mixed
     */
    private function checkoutStatusRespond($result)
    {
        return $this->checkoutStatusResponseFactory->create()->setCallbackResponse($result);
    }
}