<?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\Quote\Api\CartRepositoryInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use Magento\Sales\Model\Order;
use Qliro\QliroOne\Api\Client\MerchantInterface;
use Qliro\QliroOne\Api\Client\OrderManagementInterface;
use Qliro\QliroOne\Api\Data\AdminUpdateMerchantReferenceRequestInterface;
use Qliro\QliroOne\Api\Data\QliroOrderInterface;
use Qliro\QliroOne\Api\Data\CheckoutStatusInterface;
use Qliro\QliroOne\Api\LinkRepositoryInterface;
use Qliro\QliroOne\Model\Config;
use Qliro\QliroOne\Model\ContainerMapper;
use Qliro\QliroOne\Model\Exception\OrderPlacementPendingException;
use Qliro\QliroOne\Model\Logger\Manager as LogManager;
use Qliro\QliroOne\Model\Order\OrderPlacer;
use Qliro\QliroOne\Model\QliroOrder\Converter\QuoteFromOrderConverter;
use Qliro\QliroOne\Model\ResourceModel\Lock;
use Qliro\QliroOne\Model\Exception\TerminalException;
use Qliro\QliroOne\Model\Exception\FailToLockException;
use Magento\Sales\Model\Order\Email\Sender\OrderSender;
/**
* QliroOne management class
*/
class PlaceOrder extends AbstractManagement
{
/**
* @var \Qliro\QliroOne\Model\Config
*/
private $qliroConfig;
/**
* @var \Qliro\QliroOne\Api\Client\MerchantInterface
*/
private $merchantApi;
/**
* @var \Qliro\QliroOne\Api\Client\OrderManagementInterface
*/
private $orderManagementApi;
/**
* @var \Qliro\QliroOne\Api\LinkRepositoryInterface
*/
private $linkRepository;
/**
* @var \Magento\Quote\Api\CartRepositoryInterface
*/
private $quoteRepository;
/**
* @var \Qliro\QliroOne\Model\ContainerMapper
*/
private $containerMapper;
/**
* @var \Qliro\QliroOne\Model\Logger\Manager
*/
private $logManager;
/**
* @var \Qliro\QliroOne\Model\QliroOrder\Converter\QuoteFromOrderConverter
*/
private $quoteFromOrderConverter;
/**
* @var \Qliro\QliroOne\Model\Order\OrderPlacer
*/
private $orderPlacer;
/**
* @var \Qliro\QliroOne\Model\ResourceModel\Lock
*/
private $lock;
/**
* @var \Magento\Sales\Api\OrderRepositoryInterface
*/
private $orderRepository;
/**
* @var \Magento\Sales\Model\Order\Email\Sender\OrderSender
*/
private $orderSender;
/**
* @var Quote
*/
private $quoteManagement;
/**
* @var Payment
*/
private $paymentManagement;
/**
* Inject dependencies
*
* @param Config $qliroConfig
* @param MerchantInterface $merchantApi
* @param OrderManagementInterface $orderManagementApi
* @param QuoteFromOrderConverter $quoteFromOrderConverter
* @param LinkRepositoryInterface $linkRepository
* @param CartRepositoryInterface $quoteRepository
* @param OrderRepositoryInterface $orderRepository
* @param ContainerMapper $containerMapper
* @param LogManager $logManager
* @param OrderPlacer $orderPlacer
* @param Lock $lock
* @param OrderSender $orderSender
* @param Quote $quoteManagement
* @param Payment $paymentManagement
*/
public function __construct(
Config $qliroConfig,
MerchantInterface $merchantApi,
OrderManagementInterface $orderManagementApi,
QuoteFromOrderConverter $quoteFromOrderConverter,
LinkRepositoryInterface $linkRepository,
CartRepositoryInterface $quoteRepository,
OrderRepositoryInterface $orderRepository,
ContainerMapper $containerMapper,
LogManager $logManager,
OrderPlacer $orderPlacer,
Lock $lock,
OrderSender $orderSender,
Quote $quoteManagement,
Payment $paymentManagement
) {
$this->qliroConfig = $qliroConfig;
$this->merchantApi = $merchantApi;
$this->orderManagementApi = $orderManagementApi;
$this->linkRepository = $linkRepository;
$this->quoteRepository = $quoteRepository;
$this->containerMapper = $containerMapper;
$this->logManager = $logManager;
$this->quoteFromOrderConverter = $quoteFromOrderConverter;
$this->orderPlacer = $orderPlacer;
$this->lock = $lock;
$this->orderRepository = $orderRepository;
$this->orderSender = $orderSender;
$this->quoteManagement = $quoteManagement;
$this->paymentManagement = $paymentManagement;
}
/**
* Poll for Magento order placement and return order increment ID if successful
*
* @return \Magento\Sales\Model\Order
* @throws TerminalException
*/
public function poll()
{
$quoteId = $this->getQuote()->getId();
try {
$link = $this->linkRepository->getByQuoteId($quoteId);
$orderId = $link->getOrderId();
$qliroOrderId = $link->getQliroOrderId();
$this->logManager->setMerchantReference($link->getReference());
if (empty($orderId)) {
try {
$responseContainer = $this->merchantApi->getOrder($qliroOrderId);
if ($responseContainer->getCustomerCheckoutStatus() == CheckoutStatusInterface::STATUS_IN_PROCESS) {
throw new OrderPlacementPendingException(
__('QliroOne order status is "InProcess" and order cannot be placed.')
);
}
if (!$this->lock->lock($qliroOrderId)) {
throw new FailToLockException(__('Failed to aquire lock when placing order'));
}
$order = $this->execute($responseContainer);
$this->lock->unlock($qliroOrderId);
} catch (FailToLockException $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'quote_id' => $quoteId,
'qliro_order_id' => $qliroOrderId,
],
]
);
throw $exception;
} catch (OrderPlacementPendingException $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'quote_id' => $quoteId,
'qliro_order_id' => $qliroOrderId,
],
]
);
$this->lock->unlock($qliroOrderId);
throw $exception;
} catch (\Exception $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'quote_id' => $quoteId,
'qliro_order_id' => $qliroOrderId,
],
]
);
$this->lock->unlock($qliroOrderId);
throw new TerminalException('Order placement failed', null, $exception);
}
} else {
$order = $this->orderRepository->get($orderId);
}
} catch (NoSuchEntityException $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'quote_id' => $quoteId,
'order_id' => $orderId ?? null,
'qliro_order_id' => $qliroOrderId ?? null,
],
]
);
throw new TerminalException('Failed to link current session with Qliro One order', null, $exception);
} catch (\Exception $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'quote_id' => $quoteId,
'order_id' => $orderId ?? null,
'qliro_order_id' => $qliroOrderId ?? null,
],
]
);
throw new TerminalException('Something went wrong during order placement polling', null, $exception);
}
return $order;
}
/**
* Get a QliroOne order, update the quote, then place Magento order
* If placeOrder is successful, it returns the Magento Order
* If an error occurs it returns null
* If it's not possible to aquire lock, it returns false
*
* @param \Qliro\QliroOne\Api\Data\QliroOrderInterface $qliroOrder
* @param string $state
* @return \Magento\Sales\Model\Order
* @throws TerminalException
* @todo May require doing something upon $this->applyQliroOrderStatus($orderId) returning false
*/
public function execute(QliroOrderInterface $qliroOrder, $state = Order::STATE_PENDING_PAYMENT)
{
$qliroOrderId = $qliroOrder->getOrderId();
$this->logManager->setMark('PLACE ORDER');
$order = null; // Placeholder, this method may never return null as an order
try {
$link = $this->linkRepository->getByQliroOrderId($qliroOrderId);
try {
if ($orderId = $link->getOrderId()) {
$this->logManager->debug(
'Order is already created, skipping',
[
'extra' => [
'qliro_order' => $qliroOrderId,
'quote_id' => $this->getQuote()->getId(),
'order_id' => $orderId,
],
]
);
$order = $this->orderRepository->get($orderId);
} else {
$this->setQuote($this->quoteRepository->get($link->getQuoteId()));
$this->logManager->debug(
'Placing order',
[
'extra' => [
'qliro_order' => $qliroOrderId,
'quote_id' => $this->getQuote()->getId(),
],
]
);
$this->quoteFromOrderConverter->convert($qliroOrder, $this->getQuote());
$this->addAdditionalInfoToQuote($link, $qliroOrder->getPaymentMethod());
$this->quoteManagement->setQuote($this->getQuote())->recalculateAndSaveQuote();
$order = $this->orderPlacer->place($this->getQuote());
$orderId = $order->getId();
$link->setOrderId($orderId);
$this->linkRepository->save($link);
$this->paymentManagement->createPaymentTransaction($order, $qliroOrder, $state);
$this->logManager->debug(
'Order placed successfully',
[
'extra' => [
'qliro_order' => $qliroOrderId,
'quote_id' => $this->getQuote()->getId(),
'order_id' => $orderId,
],
]
);
$link->setMessage(sprintf('Created order %s', $order->getIncrementId()));
$this->linkRepository->save($link);
}
$this->applyQliroOrderStatus($order);
} catch (\Exception $exception) {
$link->setIsActive(false);
$link->setMessage($exception->getMessage());
$this->linkRepository->save($link);
$this->logManager->critical(
$exception,
[
'extra' => [
'qliro_order_id' => $qliroOrderId,
'quote_id' => $link->getQuoteId(),
],
]
);
throw $exception;
}
} catch (\Exception $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'qliro_order_id' => $qliroOrderId,
],
]
);
throw new TerminalException($exception->getMessage(), $exception->getCode(), $exception);
} finally {
$this->logManager->setMark(null);
}
return $order;
}
/**
* Act on the order based on the qliro order status
* It can be one of:
* - Completed - the order can be shipped
* - OnHold - review of buyer require more time
* - Refused - deny the purchase
*
* @param Order $order
* @return bool
*/
public function applyQliroOrderStatus($order)
{
$orderId = $order->getId();
try {
$link = $this->linkRepository->getByOrderId($orderId);
switch ($link->getQliroOrderStatus()) {
case CheckoutStatusInterface::STATUS_COMPLETED:
$this->applyOrderState($order, Order::STATE_NEW);
if ($order->getCanSendNewEmailFlag() && !$order->getEmailSent()) {
try {
$this->orderSender->send($order);
} catch (\Exception $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'order_id' => $orderId,
],
]
);
}
}
/*
* If Magento order has already been placed and QliroOne order status is completed,
* the order merchant reference must be replaced with Magento order increment ID
*/
/** @var \Qliro\QliroOne\Api\Data\AdminUpdateMerchantReferenceRequestInterface $request */
$request = $this->containerMapper->fromArray(
[
'OrderId' => $link->getQliroOrderId(),
'NewMerchantReference' => $order->getIncrementId(),
],
AdminUpdateMerchantReferenceRequestInterface::class
);
$response = $this->orderManagementApi->updateMerchantReference($request, $order->getStoreId());
$transactionId = 'unknown';
if ($response && $response->getPaymentTransactionId()) {
$transactionId = $response->getPaymentTransactionId();
}
$this->logManager->debug('New merchant reference was assigned to the Qliro One order', [
'payment_transaction_id' => $transactionId,
'qliro_order_id' => $link->getQliroOrderId(),
'order_id' => $order->getId(),
'new_merchant_reference' => $order->getIncrementId(),
]);
break;
case CheckoutStatusInterface::STATUS_ONHOLD:
$this->applyOrderState($order, Order::STATE_PAYMENT_REVIEW);
break;
case CheckoutStatusInterface::STATUS_REFUSED:
// Deactivate link regardless of if the upcoming order cancellation successful or not
$link->setIsActive(false);
$link->setMessage(sprintf('Order #%s marked as canceled', $order->getIncrementId()));
$this->linkRepository->save($link);
$this->applyOrderState($order, Order::STATE_NEW);
if ($order->canCancel()) {
$order->cancel();
$this->orderRepository->save($order);
}
break;
case CheckoutStatusInterface::STATUS_IN_PROCESS:
default:
return false;
}
return true;
} catch (\Exception $exception) {
$this->logManager->critical(
$exception,
[
'extra' => [
'order_id' => $orderId,
],
]
);
return false;
}
}
/**
* Add information regarding this purchase to Quote, which will transfer to Order
*
* @param \Qliro\QliroOne\Api\Data\LinkInterface $link
* @param \Qliro\QliroOne\Api\Data\QliroOrderPaymentMethodInterface $paymentMethod
* @throws \Magento\Framework\Exception\LocalizedException
*/
private function addAdditionalInfoToQuote($link, $paymentMethod)
{
$payment = $this->getQuote()->getPayment();
$payment->setAdditionalInformation(Config::QLIROONE_ADDITIONAL_INFO_QLIRO_ORDER_ID, $link->getQliroOrderId());
$payment->setAdditionalInformation(Config::QLIROONE_ADDITIONAL_INFO_REFERENCE, $link->getReference());
if ($paymentMethod) {
$payment->setAdditionalInformation(
Config::QLIROONE_ADDITIONAL_INFO_PAYMENT_METHOD_CODE,
$paymentMethod->getPaymentTypeCode()
);
$payment->setAdditionalInformation(
Config::QLIROONE_ADDITIONAL_INFO_PAYMENT_METHOD_NAME,
$paymentMethod->getPaymentMethodName()
);
}
}
/**
* Apply a proper state with its default status to the order
*
* @param \Magento\Sales\Model\Order $order
* @param string $state
*/
private function applyOrderState(Order $order, $state)
{
$status = Order::STATE_NEW === $state
? $this->qliroConfig->getOrderStatus()
: $order->getConfig()->getStateDefaultStatus($state);
$order->setState($state);
$order->setStatus($status);
$this->orderRepository->save($order);
}
}