diff --git a/Api/Admin/Builder/OrderItemHandlerInterface.php b/Api/Admin/Builder/OrderItemHandlerInterface.php
new file mode 100644
index 0000000..88b5f6d
--- /dev/null
+++ b/Api/Admin/Builder/OrderItemHandlerInterface.php
@@ -0,0 +1,22 @@
+=0) | Positive (Y>=0) | Product | X | Y |
+ * | null | Negative (X<0) | Negative (Y<0) | Discount | X | Y |
+ * | Product/Fee/Shipping | X | Y | Product/Fee/Shipping | Abs(X) | Abs(Y) |
+ * | Discount | X | Y | Discount | -Abs(X) | -Abs(Y) |
+ * +----------------------+-----------------+-----------------+----------------------+-------------------------+------------------------+
+ *
+ * @api
+ */
+interface QliroOrderItemInterface extends ContainerInterface
+{
+ const TYPE_PRODUCT = 'Product';
+ const TYPE_DISCOUNT = 'Discount';
+ const TYPE_FEE = 'Fee';
+ const TYPE_SHIPPING = 'Shipping';
+
+ /**
+ * @return string
+ */
+ public function getMerchantReference();
+
+ /**
+ * Get item type.
+ * Can be 'Product', 'Discount', 'Fee' or 'Shipping'
+ *
+ * @return string
+ */
+ public function getType();
+
+ /**
+ * @return int
+ */
+ public function getQuantity();
+
+ /**
+ * @return float
+ */
+ public function getPricePerItemIncVat();
+
+ /**
+ * @return float
+ */
+ public function getPricePerItemExVat();
+
+ /**
+ * @return string
+ */
+ public function getDescription();
+
+ /**
+ * @return array
+ */
+ public function getMetaData();
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setMerchantReference($value);
+
+ /**
+ * Set item type.
+ * Can be 'Product', 'Discount', 'Fee' or 'Shipping'
+ *
+ * @param string $value
+ * @return $this
+ */
+ public function setType($value);
+
+ /**
+ * @param int $value
+ * @return $this
+ */
+ public function setQuantity($value);
+
+ /**
+ * @param float $value
+ * @return $this
+ */
+ public function setPricePerItemIncVat($value);
+
+ /**
+ * @param float $value
+ * @return $this
+ */
+ public function setPricePerItemExVat($value);
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setDescription($value);
+
+ /**
+ * Additional metadata.
+ *
+ * In OrderManagement API can be used to have two possible elements
+ * - HeaderLines (array) Array of strings that will be diplayed above the item on the invoice.
+ * Maximum number of strings is 5 and maximum length of each string is 115 characters.
+ * - FooterLines (array) Array of strings that will be diplayed below the item on the invoice.
+ * Maximum number of strings is 5 and maximum length of each string is 115 characters.
+ *
+ * @param array $value
+ * @return $this
+ */
+ public function setMetaData($value);
+}
diff --git a/Api/Data/QliroOrderManagementStatusInterface.php b/Api/Data/QliroOrderManagementStatusInterface.php
new file mode 100644
index 0000000..82692c3
--- /dev/null
+++ b/Api/Data/QliroOrderManagementStatusInterface.php
@@ -0,0 +1,170 @@
+ element
+ *
+ * @param string $value
+ * @return $this
+ */
+ public function setInputName($value)
+ {
+ return $this->setName($value);
+ }
+
+ /**
+ * Set "id" for element
+ *
+ * @param $value
+ * @return $this
+ */
+ public function setInputId($value)
+ {
+ return $this->setId($value);
+ }
+
+ /**
+ * Render block HTML
+ *
+ * @return string
+ */
+ public function _toHtml()
+ {
+ if (!$this->getOptions()) {
+ $this->setOptions($this->getSourceOptions());
+ }
+ return parent::_toHtml();
+ }
+
+ /**
+ * @return array[]
+ */
+ private function getSourceOptions()
+ {
+ return [
+ ['label' => \__('Bulky'), 'value' => ShippingConfigUnifaunBuilder::UNIFAUN_TAGS_FUNC_BULKY],
+ ['label' => \__('Cart Price'), 'value' => ShippingConfigUnifaunBuilder::UNIFAUN_TAGS_FUNC_CARTPRICE],
+ ['label' => \__('User Defined'), 'value' => ShippingConfigUnifaunBuilder::UNIFAUN_TAGS_FUNC_USERDEFINED],
+ ['label' => \__('Weight'), 'value' => ShippingConfigUnifaunBuilder::UNIFAUN_TAGS_FUNC_WEIGHT],
+ ];
+ }
+}
diff --git a/Block/Adminhtml/Form/Field/Parameters.php b/Block/Adminhtml/Form/Field/Parameters.php
new file mode 100644
index 0000000..f4d6280
--- /dev/null
+++ b/Block/Adminhtml/Form/Field/Parameters.php
@@ -0,0 +1,82 @@
+addColumn(
+ ShippingConfigUnifaunBuilder::UNIFAUN_TAGS_SETTING_TAG,
+ ['label' => \__('Tag'), 'class' => 'required-entry']
+ );
+ $this->addColumn(
+ ShippingConfigUnifaunBuilder::UNIFAUN_TAGS_SETTING_FUNC,
+ ['label' => \__('Function'), 'size' => '60', 'renderer' => $this->getFunctionRenderer()]
+ );
+ $this->addColumn(
+ ShippingConfigUnifaunBuilder::UNIFAUN_TAGS_SETTING_VALUE,
+ ['label' => \__('Value')]
+ );
+ $this->_addAfter = false;
+ $this->_addButtonLabel = \__('Add');
+ }
+
+ /**
+ * Prepare existing row data object
+ *
+ * @param DataObject $row
+ * @throws LocalizedException
+ */
+ protected function _prepareArrayRow(DataObject $row): void
+ {
+ $options = [];
+
+ $function = $row->getFunc();
+ if ($function !== null) {
+ $options['option_' . $this->getFunctionRenderer()->calcOptionHash($function)] = 'selected="selected"';
+ }
+
+ $row->setData('option_extra_attrs', $options);
+ }
+
+ /**
+ * @return FunctionColumn
+ * @throws LocalizedException
+ */
+ private function getFunctionRenderer()
+ {
+ if (!$this->functionRenderer) {
+ $this->functionRenderer = $this->getLayout()->createBlock(
+ FunctionColumn::class,
+ '',
+ ['data' => ['is_render_to_js_template' => true]]
+ );
+ }
+ return $this->functionRenderer;
+ }
+}
diff --git a/Block/Adminhtml/Sales/Order/Creditmemo/Totals.php b/Block/Adminhtml/Sales/Order/Creditmemo/Totals.php
new file mode 100644
index 0000000..f85bdc1
--- /dev/null
+++ b/Block/Adminhtml/Sales/Order/Creditmemo/Totals.php
@@ -0,0 +1,58 @@
+fee = $fee;
+ }
+
+ /**
+ * Initialize payment fee totals
+ *
+ * @return $this
+ */
+ public function initTotals()
+ {
+ /** @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Totals $parent */
+ $parent = $this->getParentBlock();
+
+ /** @var \Magento\Sales\Model\Order\Creditmemo $creditMemo */
+ $creditMemo = $parent->getCreditmemo();
+
+ if (!$creditMemo->getQlirooneFee()) {
+ return $this;
+ }
+
+ $fee = $this->fee->getFeeObject($creditMemo->getStoreId(), $creditMemo->getQlirooneFee());
+ $parent->addTotalBefore($fee, 'sub_total');
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Block/Adminhtml/Sales/Order/Invoice/Totals.php b/Block/Adminhtml/Sales/Order/Invoice/Totals.php
new file mode 100644
index 0000000..de5ad7a
--- /dev/null
+++ b/Block/Adminhtml/Sales/Order/Invoice/Totals.php
@@ -0,0 +1,58 @@
+fee = $fee;
+ }
+
+ /**
+ * Initialize payment fee totals
+ *
+ * @return $this
+ */
+ public function initTotals()
+ {
+ /** @var \Magento\Sales\Block\Adminhtml\Order\Invoice\Totals $parent */
+ $parent = $this->getParentBlock();
+
+ /** @var \Magento\Sales\Model\Order\Invoice $invoice */
+ $invoice = $parent->getInvoice();
+
+ if (!$invoice->getQlirooneFee()) {
+ return $this;
+ }
+
+ $fee = $this->fee->getFeeObject($invoice->getStoreId(), $invoice->getQlirooneFee());
+ $parent->addTotalBefore($fee, 'sub_total');
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Block/Adminhtml/Sales/Order/Totals.php b/Block/Adminhtml/Sales/Order/Totals.php
new file mode 100644
index 0000000..708a71d
--- /dev/null
+++ b/Block/Adminhtml/Sales/Order/Totals.php
@@ -0,0 +1,58 @@
+fee = $fee;
+ }
+
+ /**
+ * Initialize payment fee totals
+ *
+ * @return $this
+ */
+ public function initTotals()
+ {
+ /** @var \Magento\Sales\Block\Adminhtml\Order\Totals $parent */
+ $parent = $this->getParentBlock();
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $parent->getOrder();
+
+ if (!$order->getQlirooneFee()) {
+ return $this;
+ }
+
+ $fee = $this->fee->getFeeObject($order->getStoreId(), $order->getQlirooneFee());
+ $parent->addTotalBefore($fee, 'sub_total');
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Block/Checkout/Pending.php b/Block/Checkout/Pending.php
new file mode 100644
index 0000000..0de247e
--- /dev/null
+++ b/Block/Checkout/Pending.php
@@ -0,0 +1,138 @@
+qliroManagement = $qliroManagement;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->quoteAgent = $quoteAgent;
+ $this->storeManager = $context->getStoreManager();
+ $this->linkRepository = $linkRepository;
+ }
+
+ /**
+ * Get QliroOne final HTML snippet
+ *
+ * Probably not used...
+ *
+ * @return string
+ */
+ public function getHtmlSnippet()
+ {
+ $quote = $this->quoteAgent->fetchRelevantQuote();
+
+ return $quote ? $this->qliroManagement->setQuote($quote)->getHtmlSnippet() : null;
+ }
+
+ /**
+ * Get a URL for the polling script
+ *
+ * @return string
+ */
+ public function getPollPendingUrl()
+ {
+ /** @var \Magento\Store\Model\Store $store */
+ $store = $this->storeManager->getStore();
+
+ $quote = $this->quoteAgent->fetchRelevantQuote();
+
+ if ($quote) {
+ try {
+ // To update link PlacedAt, if this throws an exception, checkoutStatus will still respond inside an hour...
+ $link = $this->linkRepository->getByQuoteId($quote->getId());
+ $link->setPlacedAt(time());
+ $this->linkRepository->save($link);
+ } catch (\Exception $exception) {
+ // Do nothing
+ }
+ $params = [
+ '_query' => [
+ 'token' => $this->ajaxToken->setQuote($quote)->getToken(),
+ ]
+ ];
+ } else {
+ $params = [];
+ }
+
+ return $store->getUrl('checkout/qliro_ajax/pollPending', $params);
+ }
+
+ /**
+ * Check if debug mode is on
+ *
+ * @return bool
+ */
+ public function isDebug()
+ {
+ return $this->qliroConfig->isDebugMode();
+ }
+}
diff --git a/Block/Checkout/Success.php b/Block/Checkout/Success.php
new file mode 100644
index 0000000..1562be0
--- /dev/null
+++ b/Block/Checkout/Success.php
@@ -0,0 +1,76 @@
+qliroConfig = $qliroConfig;
+ $this->successSession = $successSession;
+ }
+
+ /**
+ * Get QliroOne final HTML snippet
+ *
+ * @return string
+ */
+ public function getHtmlSnippet()
+ {
+ return $this->successSession->getSuccessHtmlSnippet();
+ }
+
+ /**
+ * Get Id of placed order
+ *
+ * @return string
+ */
+ public function getIncrementId()
+ {
+ return $this->successSession->getSuccessIncrementId();
+ }
+
+ /**
+ * Check if debug mode is on
+ *
+ * @return bool
+ */
+ public function isDebug()
+ {
+ return $this->qliroConfig->isDebugMode();
+ }
+}
diff --git a/Block/Form/QliroOne.php b/Block/Form/QliroOne.php
new file mode 100644
index 0000000..7f2f90e
--- /dev/null
+++ b/Block/Form/QliroOne.php
@@ -0,0 +1,16 @@
+warningText === null) {
+ $this->convertAdditionalInformation();
+ }
+ return $this->warningText;
+ }
+
+ /**
+ * If a warning is due
+ *
+ * @return string
+ */
+ public function showWarning()
+ {
+ if ($this->warning === null) {
+ $this->convertAdditionalInformation();
+ }
+ return $this->warning;
+ }
+
+ /**
+ * Takes specific data from AdditionalInformation field and make it available for FE
+ * @return $this
+ */
+ private function convertAdditionalInformation()
+ {
+ return $this;
+ }
+}
diff --git a/Block/Info/QliroOne.php b/Block/Info/QliroOne.php
new file mode 100644
index 0000000..6ce75d2
--- /dev/null
+++ b/Block/Info/QliroOne.php
@@ -0,0 +1,49 @@
+setTemplate('Qliro_QliroOne::info/pdf/qliroone.phtml');
+ return $this->toHtml();
+ }
+
+ /**
+ * @return string
+ */
+ public function getQliroOrderId()
+ {
+ return $this->getInfo()->getAdditionalInformation('qliro_order_id');
+ }
+
+ /**
+ * @return string
+ */
+ public function getQliroReference()
+ {
+ return $this->getInfo()->getAdditionalInformation('qliro_reference');
+ }
+
+ /**
+ * @return string
+ */
+ public function getQliroMethod()
+ {
+ return $this->getInfo()->getAdditionalInformation('qliro_payment_method_code');
+ }
+}
diff --git a/Block/Sales/Totals.php b/Block/Sales/Totals.php
new file mode 100644
index 0000000..c3d581e
--- /dev/null
+++ b/Block/Sales/Totals.php
@@ -0,0 +1,68 @@
+fee = $fee;
+ }
+
+ /**
+ * Check if we nedd display full tax total info
+ *
+ * @return bool
+ */
+ public function displayFullSummary()
+ {
+ return true;
+ }
+
+ /**
+ * Initialize payment fee totals
+ *
+ * @return $this
+ */
+ public function initTotals()
+ {
+ /** @var \Magento\Sales\Block\Order\Totals $parent */
+ $parent = $this->getParentBlock();
+
+ /** @var \Magento\Sales\Model\Order _order */
+ $order = $parent->getOrder();
+
+ if (!$order->getQlirooneFee()) {
+ return $this;
+ }
+
+ $fee = $this->fee->getFeeObject($order->getStoreId(), $order->getQlirooneFee());
+ $parent->addTotalBefore($fee, 'sub_total');
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Console/AbstractCommand.php b/Console/AbstractCommand.php
new file mode 100644
index 0000000..c91a93d
--- /dev/null
+++ b/Console/AbstractCommand.php
@@ -0,0 +1,89 @@
+objectManagerFactory = $objectManagerFactory;
+ parent::__construct();
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|null
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ throw new LocalizedException(__('You must override the execute() method in the concrete command class.'));
+ }
+
+ /**
+ * Gets initialized object manager
+ *
+ * @return ObjectManagerInterface
+ */
+ protected function getObjectManager()
+ {
+ if (null == $this->objectManager) {
+ $area = FrontNameResolver::AREA_CODE;
+ $params = $_SERVER;
+ $params[StoreManager::PARAM_RUN_CODE] = 'admin';
+ $params[StoreManager::PARAM_RUN_TYPE] = 'store';
+ $this->objectManager = $this->objectManagerFactory->create($params);
+
+ /** @var \Magento\Framework\App\State $appState */
+ $appState = $this->objectManager->get('Magento\Framework\App\State');
+
+ $appState->setAreaCode($area);
+ $configLoader = $this->objectManager->get('Magento\Framework\ObjectManager\ConfigLoaderInterface');
+ $this->objectManager->configure($configLoader->load($area));
+
+ /** @var \Magento\Framework\App\AreaList $areaList */
+ $areaList = $this->objectManager->get('Magento\Framework\App\AreaList');
+
+ /** @var \Magento\Framework\App\Area $area */
+ $area = $areaList->getArea($appState->getAreaCode());
+
+ $area->load(Area::PART_TRANSLATE);
+ }
+
+ return $this->objectManager;
+ }
+}
diff --git a/Console/GetOrderCommand.php b/Console/GetOrderCommand.php
new file mode 100644
index 0000000..2f648d7
--- /dev/null
+++ b/Console/GetOrderCommand.php
@@ -0,0 +1,130 @@
+setName(self::COMMAND_RUN);
+ $this->setDescription('Get Order from Qliro');
+ $this->addArgument('orderid', InputArgument::REQUIRED, 'Existing Qliro order id', null);
+ $this->addArgument('create', InputArgument::OPTIONAL, 'Create Magento order from Qliro order', false);
+ }
+
+ /**
+ * Initialize the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ */
+ protected function initialize(InputInterface $input, OutputInterface $output)
+ {
+ parent::initialize($input, $output);
+ $this->orderId = $input->getArgument('orderid');
+ $this->createMagentoOrder = $input->getArgument('create');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|null
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('Getting QliroOne order ');
+
+ /** @var Merchant $merchant */
+ $merchant = $this->getObjectManager()->get(Merchant::class);
+
+ /** @var \Qliro\QliroOne\Model\ContainerMapper $containerMapper */
+ $containerMapper = $this->getObjectManager()->get(ContainerMapper::class);
+
+ try {
+ $responseContainer = $merchant->getOrder($this->orderId);
+ $responsePayload = $containerMapper->toArray($responseContainer);
+
+ fprintf(STDOUT, \json_encode($responsePayload, JSON_PRETTY_PRINT));
+
+ if ($this->createMagentoOrder) {
+ /** @var \Qliro\QliroOne\Model\Management $management */
+ $management = $this->getObjectManager()->get(Management::class);
+
+ $management->placeOrder($responseContainer);
+ $output->writeln('Place order successfull ');
+ }
+ } catch (\Qliro\QliroOne\Model\Exception\TerminalException $exception) {
+ /** @var \GuzzleHttp\Exception\RequestException $origException */
+ $origException = $exception->getPrevious();
+
+ fprintf(STDOUT, 'Merchant API exception:');
+ fprintf(STDOUT, $origException->getMessage());
+
+ } catch (\Qliro\QliroOne\Model\Exception\FailToLockException $exception) {
+ fprintf(STDOUT, 'Locking:');
+ fprintf(STDOUT, $exception->getMessage());
+
+ } catch (\Qliro\QliroOne\Model\Api\Client\Exception\ClientException $exception) {
+ /** @var \GuzzleHttp\Exception\RequestException $origException */
+ $origException = $exception->getPrevious();
+
+ fprintf(STDOUT, 'Merchant API exception:');
+
+ fprintf(
+ STDOUT,
+ \json_encode(
+ [
+ 'request.uri' => $origException->getRequest()->getUri(),
+ 'request.method' => $origException->getRequest()->getMethod(),
+ 'request.headers' => $origException->getRequest()->getHeaders(),
+ 'request.body' => $origException->getRequest()->getBody()->getContents(),
+ 'response.status' => $origException->getResponse()->getStatusCode(),
+ 'response.headers' => $origException->getResponse()->getHeaders(),
+ 'response.body' => $origException->getResponse()->getBody()->getContents(),
+ ],
+ JSON_PRETTY_PRINT
+ )
+ );
+ }
+
+ return 0;
+ }
+}
diff --git a/Console/LockCommand.php b/Console/LockCommand.php
new file mode 100644
index 0000000..03abb94
--- /dev/null
+++ b/Console/LockCommand.php
@@ -0,0 +1,104 @@
+setName(self::COMMAND_RUN);
+ $this->setDescription('Lock order creation for order');
+ $this->addArgument('orderid', InputArgument::REQUIRED, 'Qliro order id', null);
+ $this->addOption('override', 'o', InputOption::VALUE_NONE, 'override lock by sleeping process');
+ }
+
+ /**
+ * Initialize the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ */
+ protected function initialize(InputInterface $input, OutputInterface $output)
+ {
+ parent::initialize($input, $output);
+ $this->orderId = $input->getArgument('orderid');
+ $this->override = $input->getOption('override');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|null
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('Lock Qliro order ');
+
+ /** @var Lock */
+ $lockManager = $this->getObjectManager()->get(Lock::class);
+
+ try {
+ $result = $lockManager->lock($this->orderId, $this->override);
+ $output->writeln(sprintf('%s Lock of Qliro Order ', $result ? 'Successful' : 'Unsuccessful'));
+ } catch (\Qliro\QliroOne\Model\Api\Client\Exception\ClientException $exception) {
+ /** @var \GuzzleHttp\Exception\RequestException $origException */
+ $origException = $exception->getPrevious();
+
+ fprintf(STDOUT, 'Merchant API exception:');
+
+ fprintf(
+ STDOUT,
+ \json_encode(
+ [
+ 'request.uri' => $origException->getRequest()->getUri(),
+ 'request.method' => $origException->getRequest()->getMethod(),
+ 'request.headers' => $origException->getRequest()->getHeaders(),
+ 'request.body' => $origException->getRequest()->getBody()->getContents(),
+ 'response.status' => $origException->getResponse()->getStatusCode(),
+ 'response.headers' => $origException->getResponse()->getHeaders(),
+ 'response.body' => $origException->getResponse()->getBody()->getContents(),
+ ],
+ JSON_PRETTY_PRINT
+ )
+ );
+ }
+
+ return 0;
+ }
+}
diff --git a/Console/PingCommand.php b/Console/PingCommand.php
new file mode 100644
index 0000000..dbf76e0
--- /dev/null
+++ b/Console/PingCommand.php
@@ -0,0 +1,55 @@
+setName(self::COMMAND_RUN);
+ $this->setDescription('Verify QliroOne API');
+ }
+
+ /**
+ * Initialize the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ */
+ protected function initialize(InputInterface $input, OutputInterface $output)
+ {
+ parent::initialize($input, $output);
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|null
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('pong ');
+
+ return 0;
+ }
+}
diff --git a/Console/TestCommand.php b/Console/TestCommand.php
new file mode 100644
index 0000000..78bf2c6
--- /dev/null
+++ b/Console/TestCommand.php
@@ -0,0 +1,125 @@
+setName(self::COMMAND_RUN);
+ $this->setDescription('Verify QliroOne API');
+ }
+
+ /**
+ * Initialize the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ */
+ protected function initialize(InputInterface $input, OutputInterface $output)
+ {
+ parent::initialize($input, $output);
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|null
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('Test ');
+
+ /** @var Service $service */
+ $service = $this->getObjectManager()->get(Service::class);
+
+ /** @var \Qliro\QliroOne\Model\ContainerMapper $containerMapper */
+ $containerMapper = $this->getObjectManager()->get(ContainerMapper::class);
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderCreateRequestInterface $createRequest */
+ $createRequest = $this->getObjectManager()->get(QliroOrderCreateRequestInterface::class);
+
+ $payload = [
+ "MerchantReference" => "211300540",
+ "Currency" => "SEK",
+ "Country" => "SE",
+ "Language" => "sv-se",
+ "MerchantConfirmationUrl" => "http://baw.ddns.net:8080/qliroone/htdocs/sv/checkout/qliro/saveOrder?XDEBUG_SESSION_START=PHPSTORM",
+ "MerchantTermsUrl" => "http://baw.ddns.net:8080/qliroone/htdocs/sv/terms",
+ "MerchantOrderValidationUrl" => "http://baw.ddns.net:8080/qliroone/htdocs/sv/checkout/qliro/validate?XDEBUG_SESSION_START=PHPSTORM",
+ "MerchantOrderAvailableShippingMethodsUrl" => "http://baw.ddns.net:8080/qliroone/htdocs/sv/checkout/qliro/shipping?XDEBUG_SESSION_START=PHPSTORM",
+ "MerchantCheckoutStatusPushUrl" => "http://baw.ddns.net:8080/qliroone/htdocs/qliroapi/order/index/order_id/211300540/token/NmIwMTNmY2Q0YzYwOWE2ZjQ3MzVkMDcyNDMzNTg1ZjMwZDkyMmI1NDhlNDFhN2Q1YWJiZWI1MmVhZWNiYWQwYQ==/",
+ "MerchantOrderManagementStatusPushUrl" => "http://baw.ddns.net:8080/qliroone/htdocs/sv/qliroapi/notification?XDEBUG_SESSION_START=PHPSTORM",
+ "PrimaryColor" => "#000000",
+ "CallToActionColor" => "#0000FF",
+ "BackgroundColor" => "#FFFFFF",
+ "AskForNewsletterSignup" => true,
+ "OrderItems" => [
+ [
+ "MerchantReference" => "S001",
+ "Description" => "Test product - Simple",
+ "Type" => "Product",
+ "Quantity" => 1,
+ "PricePerItemIncVat" => "100.00",
+ "PricePerItemExVat" => "80.00"
+ ]
+ ],
+ ];
+
+ $containerMapper->fromArray($payload, $createRequest);
+
+ $a = 1;
+
+ try {
+ $response = $service->post('checkout/merchantapi/orders', $payload);
+
+ print_r([
+ 'headers' => $service->getResponseHeaders(),
+ 'response' => $response,
+ 'status_code' => $service->getResponseStatusCode(),
+ 'reason' => $service->getResponseReason(),
+ ]);
+ } catch (\GuzzleHttp\Exception\RequestException $exception) {
+ print_r([
+ 'request.uri' => $exception->getRequest()->getUri(),
+ 'request.method' => $exception->getRequest()->getMethod(),
+ 'request.headers' => $exception->getRequest()->getHeaders(),
+ 'request.body' => $exception->getRequest()->getBody()->getContents(),
+ 'response.status' => $exception->getResponse()->getStatusCode(),
+ 'response.headers' => $exception->getResponse()->getHeaders(),
+ 'response.body' => $exception->getResponse()->getBody()->getContents(),
+ ]);
+ }
+
+ //$response = $service->get('checkout/merchantapi/orders/', ['merchantReference' => '211300540']);
+ //if (!is_string($response)) {
+ // $response = var_export($response, true);
+ //}
+ //$output->writeln("$response ");
+
+ return 0;
+ }
+}
diff --git a/Console/UnlockCommand.php b/Console/UnlockCommand.php
new file mode 100644
index 0000000..7596c3d
--- /dev/null
+++ b/Console/UnlockCommand.php
@@ -0,0 +1,104 @@
+setName(self::COMMAND_RUN);
+ $this->setDescription('Unlock order creation for order');
+ $this->addArgument('orderid', InputArgument::REQUIRED, 'Qliro order id', null);
+ $this->addOption('force', 'f', InputOption::VALUE_NONE, 'ignore process id when unlocking');
+ }
+
+ /**
+ * Initialize the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ */
+ protected function initialize(InputInterface $input, OutputInterface $output)
+ {
+ parent::initialize($input, $output);
+ $this->orderId = $input->getArgument('orderid');
+ $this->force = $input->getOption('force');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|null
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('Unlock Qliro order ');
+
+ /** @var Lock */
+ $lockManager = $this->getObjectManager()->get(Lock::class);
+
+ try {
+ $result = $lockManager->unlock($this->orderId, $this->force);
+ $output->writeln(sprintf('%s Unlock of Qliro Order ', $result ? 'Successful' : 'Unsuccessful'));
+ } catch (\Qliro\QliroOne\Model\Api\Client\Exception\ClientException $exception) {
+ /** @var \GuzzleHttp\Exception\RequestException $origException */
+ $origException = $exception->getPrevious();
+
+ fprintf(STDOUT, 'Merchant API exception:');
+
+ fprintf(
+ STDOUT,
+ \json_encode(
+ [
+ 'request.uri' => $origException->getRequest()->getUri(),
+ 'request.method' => $origException->getRequest()->getMethod(),
+ 'request.headers' => $origException->getRequest()->getHeaders(),
+ 'request.body' => $origException->getRequest()->getBody()->getContents(),
+ 'response.status' => $origException->getResponse()->getStatusCode(),
+ 'response.headers' => $origException->getResponse()->getHeaders(),
+ 'response.body' => $origException->getResponse()->getBody()->getContents(),
+ ],
+ JSON_PRETTY_PRINT
+ )
+ );
+ }
+
+ return 0;
+ }
+}
diff --git a/Console/UpdateOrderCommand.php b/Console/UpdateOrderCommand.php
new file mode 100644
index 0000000..0904736
--- /dev/null
+++ b/Console/UpdateOrderCommand.php
@@ -0,0 +1,103 @@
+setName(self::COMMAND_RUN);
+ $this->setDescription('Update Order in Qliro based on quote');
+ $this->addOption('force', 'f', InputOption::VALUE_NONE, 'always send update to qliro');
+ $this->addArgument('orderid', InputArgument::REQUIRED, 'Existing Qliro order id', null);
+ }
+
+ /**
+ * Initialize the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ */
+ protected function initialize(InputInterface $input, OutputInterface $output)
+ {
+ parent::initialize($input, $output);
+ $this->orderId = $input->getArgument('orderid');
+ $this->force = $input->getOption('force');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return int|null
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ $output->writeln('Update QliroOne order ');
+
+ /** @var Management */
+ $management = $this->getObjectManager()->get(Management::class);
+
+ try {
+ $management->updateOrder($this->orderId, $this->force);
+ } catch (\Qliro\QliroOne\Model\Api\Client\Exception\ClientException $exception) {
+ /** @var \GuzzleHttp\Exception\RequestException $origException */
+ $origException = $exception->getPrevious();
+
+ fprintf(STDOUT, 'Merchant API exception:');
+
+ fprintf(
+ STDOUT,
+ \json_encode(
+ [
+ 'request.uri' => $origException->getRequest()->getUri(),
+ 'request.method' => $origException->getRequest()->getMethod(),
+ 'request.headers' => $origException->getRequest()->getHeaders(),
+ 'request.body' => $origException->getRequest()->getBody()->getContents(),
+ 'response.status' => $origException->getResponse()->getStatusCode(),
+ 'response.headers' => $origException->getResponse()->getHeaders(),
+ 'response.body' => $origException->getResponse()->getBody()->getContents(),
+ ],
+ JSON_PRETTY_PRINT
+ )
+ );
+ }
+
+ return 0;
+ }
+}
diff --git a/Controller/Checkout/Totals.php b/Controller/Checkout/Totals.php
new file mode 100644
index 0000000..5f72b45
--- /dev/null
+++ b/Controller/Checkout/Totals.php
@@ -0,0 +1,94 @@
+checkoutSession = $checkoutSession;
+ $this->helper = $helper;
+ $this->resultJson = $resultJson;
+ $this->quoteRepository = $quoteRepository;
+ }
+
+ /**
+ * Trigger to re-calculate the collect Totals
+ *
+ * @return bool
+ */
+ public function execute()
+ {
+ $response = [
+ 'errors' => false,
+ 'message' => ''
+ ];
+
+ try {
+ /** @var \Magento\Quote\Model\Quote $quote */
+ $quote = $this->quoteRepository->get($this->checkoutSession->getQuoteId());
+
+ /** @var array $payment */
+ $payment = $this->helper->jsonDecode($this->getRequest()->getContent());
+ $quote->getPayment()->setMethod($payment['payment']);
+ $quote->collectTotals();
+ $this->quoteRepository->save($quote);
+ } catch (\Exception $e) {
+ $response = [
+ 'errors' => true,
+ 'message' => $e->getMessage()
+ ];
+ }
+
+ /** @var \Magento\Framework\Controller\Result\Raw $resultJson */
+ $resultJson = $this->resultJson->create();
+
+ return $resultJson->setData($response);
+ }
+}
diff --git a/Controller/Qliro/Ajax/PollPending.php b/Controller/Qliro/Ajax/PollPending.php
new file mode 100644
index 0000000..ee69e8f
--- /dev/null
+++ b/Controller/Qliro/Ajax/PollPending.php
@@ -0,0 +1,187 @@
+dataHelper = $dataHelper;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->logManager = $logManager;
+ $this->quoteAgent = $quoteAgent;
+ $this->successSession = $successSession;
+ }
+
+ /**
+ * Dispatch the action
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Qliro One is not active.')
+ ],
+ 403,
+ null,
+ 'AJAX:POLL_SUCCESS:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ $quote = $this->quoteAgent->fetchRelevantQuote();
+ $this->logManager->setMerchantReferenceFromQuote($quote);
+ $this->ajaxToken->setQuote($quote);
+
+ if (!$quote) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('The cart was reset.')
+ ],
+ 401,
+ null,
+ 'AJAX:POLL_SUCCESS:ERROR_QUOTE'
+ );
+ }
+
+ if (!$this->ajaxToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Security token is incorrect.')
+ ],
+ 401,
+ null,
+ 'AJAX:POLL_SUCCESS:ERROR_TOKEN'
+ );
+ }
+
+ $htmlSnippet = null;
+
+ try {
+ $order = $this->qliroManagement->setQuote($quote)->pollPlaceOrder();
+ $htmlSnippet = $this->qliroManagement->getHtmlSnippet();
+ $this->successSession->save($htmlSnippet, $order);
+ } catch (TerminalException $exception) {
+ $previousException = $exception->getPrevious();
+ $isLockFailed = $previousException instanceof FailToLockException;
+ $isStatusWrong = $previousException instanceof OrderPlacementPendingException;
+
+ if ($isLockFailed || $isStatusWrong) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['status' => 'PENDING'],
+ 200,
+ null,
+ 'AJAX:POLL_SUCCESS:PENDING'
+ );
+ }
+
+ $this->quoteAgent->clear();
+
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Order cannot be placed. Please try again, or contact our Customer Service.')
+ ],
+ 400,
+ null,
+ 'AJAX:POLL_SUCCESS:ERROR'
+ );
+ }
+
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'OK',
+ 'htmlSnippet' => $htmlSnippet,
+ 'orderIncrementId' => $order->getIncrementId(),
+ ],
+ 200,
+ null,
+ 'AJAX:POLL_SUCCESS'
+ );
+ }
+}
diff --git a/Controller/Qliro/Ajax/UpdateCustomer.php b/Controller/Qliro/Ajax/UpdateCustomer.php
new file mode 100644
index 0000000..f467789
--- /dev/null
+++ b/Controller/Qliro/Ajax/UpdateCustomer.php
@@ -0,0 +1,156 @@
+dataHelper = $dataHelper;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->checkoutSession = $checkoutSession;
+ $this->logManager = $logManager;
+ $this->quoteAgent = $quoteAgent;
+ }
+
+ /**
+ * Dispatch the action
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Qliro One is not active.')
+ ],
+ 403,
+ null,
+ 'AJAX:UPDATE_CUSTOMER:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ $quote = $this->checkoutSession->getQuote();
+ $this->logManager->setMerchantReferenceFromQuote($quote);
+ $this->ajaxToken->setQuote($quote);
+ $this->quoteAgent->store($quote);
+
+ if (!$this->ajaxToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Security token is incorrect.')
+ ],
+ 401,
+ null,
+ 'AJAX:UPDATE_CUSTOMER:ERROR_TOKEN'
+ );
+ }
+
+ $data = $this->dataHelper->readPreparedPayload($request, 'AJAX:UPDATE_CUSTOMER');
+ if (array_key_exists('address', $data) && is_null($data['address'])) {
+ $data['address'] = [];
+ }
+
+ try {
+ $this->qliroManagement->setQuote($quote)->updateCustomer($data);
+ } catch (\Exception $exception) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Cannot update quote with customer data.')
+ ],
+ 400,
+ null,
+ 'AJAX:UPDATE_CUSTOMER:ERROR'
+ );
+ }
+
+ return $this->dataHelper->sendPreparedPayload(
+ ['status' => 'OK'],
+ 200,
+ null,
+ 'AJAX:UPDATE_CUSTOMER'
+ );
+ }
+}
diff --git a/Controller/Qliro/Ajax/UpdatePaymentMethod.php b/Controller/Qliro/Ajax/UpdatePaymentMethod.php
new file mode 100644
index 0000000..fe6b66d
--- /dev/null
+++ b/Controller/Qliro/Ajax/UpdatePaymentMethod.php
@@ -0,0 +1,144 @@
+dataHelper = $dataHelper;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->checkoutSession = $checkoutSession;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch the action
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Qliro One is not active.')
+ ],
+ 403,
+ null,
+ 'AJAX:UPDATE_PAYMENT_METHOD:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ $quote = $this->checkoutSession->getQuote();
+ $this->logManager->setMerchantReferenceFromQuote($quote);
+ $this->ajaxToken->setQuote($quote);
+
+ if (!$this->ajaxToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Security token is incorrect.')
+ ],
+ 401,
+ null,
+ 'AJAX:UPDATE_PAYMENT_METHOD:ERROR_TOKEN'
+ );
+ }
+
+ $data = $this->dataHelper->readPreparedPayload($request, 'AJAX:UPDATE_PAYMENT_METHOD');
+
+ try {
+ $fee = $data['price'] ?? null;
+ $result = $this->qliroManagement->setQuote($quote)->updateFee($fee);
+ } catch (\Exception $exception) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Cannot update payment method in quote.')
+ ],
+ 400,
+ null,
+ 'AJAX:UPDATE_PAYMENT_METHOD:ERROR'
+ );
+ }
+
+ return $this->dataHelper->sendPreparedPayload(
+ ['status' => $result ? 'OK' : 'SKIPPED'],
+ 200,
+ null,
+ 'AJAX:UPDATE_PAYMENT_METHOD'
+ );
+ }
+}
diff --git a/Controller/Qliro/Ajax/UpdateQuote.php b/Controller/Qliro/Ajax/UpdateQuote.php
new file mode 100644
index 0000000..062fe01
--- /dev/null
+++ b/Controller/Qliro/Ajax/UpdateQuote.php
@@ -0,0 +1,151 @@
+dataHelper = $dataHelper;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->checkoutSession = $checkoutSession;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch the action
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ * @throws \Magento\Framework\Exception\NotFoundException
+ */
+ public function execute()
+ {
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['error' => (string)__('Qliro One is not active.')],
+ 403,
+ null,
+ 'AJAX:UPDATE_QUOTE:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ $quote = $this->checkoutSession->getQuote();
+ $this->logManager->setMerchantReferenceFromQuote($quote);
+ $this->ajaxToken->setQuote($quote);
+
+ if (!$this->ajaxToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['error' => (string)__('Security token is incorrect.')],
+ 401,
+ null,
+ 'AJAX:UPDATE_QUOTE:ERROR_TOKEN'
+ );
+ }
+
+ $this->dataHelper->readPreparedPayload($request, 'AJAX:UPDATE_QUOTE');
+
+ try {
+ $this->qliroManagement->setQuote($quote)->getQliroOrder();
+ } catch (TerminalException $exception) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['error' => (string)__('Cannot fetch Qliro One order.')],
+ 400,
+ null,
+ 'AJAX:UPDATE_QUOTE:ERROR'
+ );
+ }
+
+ if ($quote->isVirtual()) {
+ $billingAddress = $quote->getBillingAddress();
+ $fee = $billingAddress->getQlirooneFee();
+ $shippingCost = 0;
+ } else {
+ $shippingAddress = $quote->getShippingAddress();
+ $fee = $shippingAddress->getQlirooneFee();
+ $shippingCost = $shippingAddress->getShippingInclTax();
+ }
+
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'order' => [
+ 'totalPrice' => $quote->getGrandTotal() - $fee - $shippingCost,
+ ],
+ ],
+ 200,
+ null,
+ 'AJAX:UPDATE_QUOTE'
+ );
+ }
+}
diff --git a/Controller/Qliro/Ajax/UpdateShippingMethod.php b/Controller/Qliro/Ajax/UpdateShippingMethod.php
new file mode 100644
index 0000000..621e4db
--- /dev/null
+++ b/Controller/Qliro/Ajax/UpdateShippingMethod.php
@@ -0,0 +1,150 @@
+dataHelper = $dataHelper;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->checkoutSession = $checkoutSession;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch the action
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Qliro One is not active.')
+ ],
+ 403,
+ null,
+ 'AJAX:UPDATE_SHIPPING_METHOD:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ $quote = $this->checkoutSession->getQuote();
+ $this->logManager->setMerchantReferenceFromQuote($quote);
+ $this->ajaxToken->setQuote($quote);
+
+ if (!$this->ajaxToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Security token is incorrect.')
+ ],
+ 401,
+ null,
+ 'AJAX:UPDATE_SHIPPING_METHOD:ERROR_TOKEN'
+ );
+ }
+
+ $data = $this->dataHelper->readPreparedPayload($request, 'AJAX:UPDATE_SHIPPING_METHOD');
+
+ try {
+ if ($this->qliroConfig->isUnifaunEnabled($quote->getStoreId())) {
+ $shippingMethodCode = \Qliro\QliroOne\Model\Carrier\Unifaun::QLIRO_UNIFAUN_SHIPPING_CODE;
+ } else {
+ $shippingMethodCode = $data['method'] ?? null;
+ }
+ $secondaryOption = $data['secondaryOption'] ?? null;
+ $shippingPrice = $data['price'] ?? null;
+ $result = $this->qliroManagement->setQuote($quote)->updateShippingMethod($shippingMethodCode, $secondaryOption, $shippingPrice);
+ } catch (\Exception $exception) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Cannot update shipping method in quote.')
+ ],
+ 400,
+ null,
+ 'AJAX:UPDATE_SHIPPING_METHOD:ERROR'
+ );
+ }
+
+ return $this->dataHelper->sendPreparedPayload(
+ ['status' => $result ? 'OK' : 'SKIPPED'],
+ 200,
+ null,
+ 'AJAX:UPDATE_SHIPPING_METHOD'
+ );
+ }
+}
diff --git a/Controller/Qliro/Ajax/UpdateShippingPrice.php b/Controller/Qliro/Ajax/UpdateShippingPrice.php
new file mode 100644
index 0000000..5907dcb
--- /dev/null
+++ b/Controller/Qliro/Ajax/UpdateShippingPrice.php
@@ -0,0 +1,144 @@
+dataHelper = $dataHelper;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->checkoutSession = $checkoutSession;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch the action
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Qliro One is not active.')
+ ],
+ 403,
+ null,
+ 'AJAX:UPDATE_SHIPPING_PRICE:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ $quote = $this->checkoutSession->getQuote();
+ $this->logManager->setMerchantReferenceFromQuote($quote);
+ $this->ajaxToken->setQuote($quote);
+
+ if (!$this->ajaxToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Security token is incorrect.')
+ ],
+ 401,
+ null,
+ 'AJAX:UPDATE_SHIPPING_PRICE:ERROR_TOKEN'
+ );
+ }
+
+ $data = $this->dataHelper->readPreparedPayload($request, 'AJAX:UPDATE_SHIPPING_PRICE');
+
+ try {
+ $shippingPrice = $data['price'] ?? ($data['newShippingPrice'] ?? null);
+ $result = $this->qliroManagement->setQuote($quote)->updateShippingPrice($shippingPrice);
+ } catch (\Exception $exception) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ 'status' => 'FAILED',
+ 'error' => (string)__('Cannot update shipping method option in quote.')
+ ],
+ 400,
+ null,
+ 'AJAX:UPDATE_SHIPPING_PRICE:ERROR'
+ );
+ }
+
+ return $this->dataHelper->sendPreparedPayload(
+ ['status' => $result ? 'OK' : 'SKIPPED'],
+ 200,
+ null,
+ 'AJAX:UPDATE_SHIPPING_PRICE'
+ );
+ }
+}
diff --git a/Controller/Qliro/Callback/CheckoutStatus.php b/Controller/Qliro/Callback/CheckoutStatus.php
new file mode 100644
index 0000000..d6c7499
--- /dev/null
+++ b/Controller/Qliro/Callback/CheckoutStatus.php
@@ -0,0 +1,143 @@
+qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->containerMapper = $containerMapper;
+ $this->dataHelper = $dataHelper;
+ $this->callbackToken = $callbackToken;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch request
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ $start = \microtime(true);
+ $this->logManager->info('Notification CheckoutStatus start');
+
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ CheckoutStatusResponseInterface::CALLBACK_RESPONSE =>
+ CheckoutStatusResponseInterface::RESPONSE_NOTIFICATIONS_DISABLED
+ ],
+ 400,
+ null,
+ 'CALLBACK:CHECKOUT_STATUS:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ if (!$this->callbackToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ [
+ CheckoutStatusResponseInterface::CALLBACK_RESPONSE =>
+ CheckoutStatusResponseInterface::RESPONSE_AUTHENTICATE_ERROR
+ ],
+ 400,
+ null,
+ 'CALLBACK:CHECKOUT_STATUS:ERROR_TOKEN'
+ );
+ }
+
+ $payload = $this->dataHelper->readPreparedPayload($request, 'CALLBACK:CHECKOUT_STATUS');
+
+ /** @var \Qliro\QliroOne\Api\Data\CheckoutStatusInterface $updateContainer */
+ $updateContainer = $this->containerMapper->fromArray(
+ $payload,
+ CheckoutStatusInterface::class
+ );
+
+ $responseContainer = $this->qliroManagement->checkoutStatus($updateContainer);
+
+ $response = $this->dataHelper->sendPreparedPayload(
+ $responseContainer,
+ $responseContainer->getCallbackResponse() == CheckoutStatusResponseInterface::RESPONSE_RECEIVED ? 200 : 500,
+ null,
+ 'CALLBACK:CHECKOUT_STATUS'
+ );
+
+ $this->logManager->info('Notification CheckoutStatus done in {duration} seconds', ['duration' => \microtime(true) - $start]);
+
+ return $response;
+ }
+}
diff --git a/Controller/Qliro/Callback/ShippingMethods.php b/Controller/Qliro/Callback/ShippingMethods.php
new file mode 100644
index 0000000..e400ab8
--- /dev/null
+++ b/Controller/Qliro/Callback/ShippingMethods.php
@@ -0,0 +1,137 @@
+qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->containerMapper = $containerMapper;
+ $this->dataHelper = $dataHelper;
+ $this->callbackToken = $callbackToken;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch request
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ $start = \microtime(true);
+ $this->logManager->info('Notification ShippingMethods start');
+
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['error' => UpdateShippingMethodsResponseInterface::REASON_POSTAL_CODE],
+ 400,
+ null,
+ 'CALLBACK:SHIPPING_METHODS:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ if (!$this->callbackToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['error' => UpdateShippingMethodsResponseInterface::REASON_POSTAL_CODE],
+ 400,
+ null,
+ 'CALLBACK:SHIPPING_METHODS:ERROR_TOKEN'
+ );
+ }
+
+ $payload = $this->dataHelper->readPreparedPayload($request, 'CALLBACK:SHIPPING_METHODS');
+
+ /** @var \Qliro\QliroOne\Api\Data\UpdateShippingMethodsNotificationInterface $updateContainer */
+ $updateContainer = $this->containerMapper->fromArray(
+ $payload,
+ UpdateShippingMethodsNotificationInterface::class
+ );
+
+ $responseContainer = $this->qliroManagement->getShippingMethods($updateContainer);
+
+ $response = $this->dataHelper->sendPreparedPayload(
+ $responseContainer,
+ $responseContainer->getDeclineReason() ? 400 : 200,
+ null,
+ 'CALLBACK:SHIPPING_METHODS'
+ );
+
+ $this->logManager->info('Notification ShippingMethods done in {duration} seconds', ['duration' => \microtime(true) - $start]);
+
+ return $response;
+ }
+}
diff --git a/Controller/Qliro/Callback/TransactionStatus.php b/Controller/Qliro/Callback/TransactionStatus.php
new file mode 100644
index 0000000..cd54287
--- /dev/null
+++ b/Controller/Qliro/Callback/TransactionStatus.php
@@ -0,0 +1,138 @@
+qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->containerMapper = $containerMapper;
+ $this->dataHelper = $dataHelper;
+ $this->callbackToken = $callbackToken;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch request
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ * @throws \Exception
+ */
+ public function execute()
+ {
+ $start = \microtime(true);
+ $this->logManager->info('Notification TransactionStatus start');
+
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ [OmStatusResponse::CALLBACK_RESPONSE => OmStatusResponse::RESPONSE_NOTIFICATIONS_DISABLED],
+ 400,
+ null,
+ 'CALLBACK:MANAGEMENT_STATUS:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ if (!$this->callbackToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ [OmStatusResponse::CALLBACK_RESPONSE => OmStatusResponse::RESPONSE_AUTHENTICATE_ERROR],
+ 400,
+ null,
+ 'CALLBACK:MANAGEMENT_STATUS:ERROR_TOKEN'
+ );
+ }
+
+ $payload = $this->dataHelper->readPreparedPayload($request, 'CALLBACK:MANAGEMENT_STATUS');
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderManagementStatusInterface $updateContainer */
+ $updateContainer = $this->containerMapper->fromArray(
+ $payload,
+ QliroOrderManagementStatusInterface::class
+ );
+
+ $responseContainer = $this->qliroManagement->handleTransactionStatus($updateContainer);
+
+ $response = $this->dataHelper->sendPreparedPayload(
+ $responseContainer,
+ $responseContainer->getCallbackResponse() == OmStatusResponse::RESPONSE_RECEIVED ? 200 : 500,
+ null,
+ 'CALLBACK:MANAGEMENT_STATUS'
+ );
+
+ $this->logManager->info('Notification TransactionStatus done in {duration} seconds', ['duration' => \microtime(true) - $start]);
+
+ return $response;
+ }
+}
diff --git a/Controller/Qliro/Callback/Validate.php b/Controller/Qliro/Callback/Validate.php
new file mode 100644
index 0000000..02c001a
--- /dev/null
+++ b/Controller/Qliro/Callback/Validate.php
@@ -0,0 +1,138 @@
+qliroConfig = $qliroConfig;
+ $this->qliroManagement = $qliroManagement;
+ $this->containerMapper = $containerMapper;
+ $this->dataHelper = $dataHelper;
+ $this->callbackToken = $callbackToken;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Dispatch request
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ $start = \microtime(true);
+ $this->logManager->info('Notification Validate start');
+
+ if (!$this->qliroConfig->isActive()) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['error' => ValidateOrderResponseInterface::REASON_OTHER],
+ 400,
+ null,
+ 'CALLBACK:VALIDATE:ERROR_INACTIVE'
+ );
+ }
+
+ /** @var \Magento\Framework\App\Request\Http $request */
+ $request = $this->getRequest();
+
+ if (!$this->callbackToken->verifyToken($request->getParam('token'))) {
+ return $this->dataHelper->sendPreparedPayload(
+ ['error' => ValidateOrderResponseInterface::REASON_OTHER],
+ 400,
+ null,
+ 'CALLBACK:VALIDATE:ERROR_TOKEN'
+ );
+ }
+
+ $payload = $this->dataHelper->readPreparedPayload($request, 'CALLBACK:VALIDATE');
+
+ /** @var \Qliro\QliroOne\Api\Data\ValidateOrderNotificationInterface $validateContainer */
+ $validateContainer = $this->containerMapper->fromArray(
+ $payload,
+ ValidateOrderNotificationInterface::class
+ );
+
+ $responseContainer = $this->qliroManagement->validateQliroOrder($validateContainer);
+
+ $response = $this->dataHelper->sendPreparedPayload(
+ $responseContainer,
+ $responseContainer->getDeclineReason() ? 400 : 200,
+ null,
+ 'CALLBACK:VALIDATE'
+ );
+
+ $this->logManager->info('Notification Validate done in {duration} seconds', ['duration' => \microtime(true) - $start]);
+
+ return $response;
+ }
+}
diff --git a/Controller/Qliro/Index.php b/Controller/Qliro/Index.php
new file mode 100644
index 0000000..f959ae9
--- /dev/null
+++ b/Controller/Qliro/Index.php
@@ -0,0 +1,124 @@
+qliroConfig = $qliroConfig;
+ $this->resultForwardFactory = $resultForwardFactory;
+ }
+
+ /**
+ * Dispatch request
+ *
+ * @return \Magento\Framework\Controller\ResultInterface|ResponseInterface
+ */
+ public function execute()
+ {
+ if (!$this->qliroConfig->isActive()) {
+ $resultForward = $this->resultForwardFactory->create();
+ return $resultForward->forward('noroute');
+ }
+
+ $resultPage = parent::execute();
+
+ if ($resultPage instanceof Redirect) {
+ return $resultPage;
+ }
+
+ $resultPage->getConfig()->getTitle()->set($this->qliroConfig->getTitle());
+
+ return $resultPage;
+ }
+}
diff --git a/Controller/Qliro/Pending.php b/Controller/Qliro/Pending.php
new file mode 100644
index 0000000..1df6512
--- /dev/null
+++ b/Controller/Qliro/Pending.php
@@ -0,0 +1,109 @@
+quoteAgent = $quoteAgent;
+ }
+
+ /**
+ * Dispatch a QliroOne checkout pending page or redirect to the cart
+ *
+ * @return \Magento\Framework\View\Result\Page|\Magento\Framework\Controller\Result\Redirect
+ */
+ public function execute()
+ {
+ if (!$this->quoteAgent->fetchRelevantQuote()) {
+ $resultRedirect = $this->resultRedirectFactory->create();
+
+ return $resultRedirect->setPath('checkout/cart');
+ }
+
+ $resultPage = $this->resultPageFactory->create();
+
+ return $resultPage;
+ }
+}
diff --git a/Controller/Qliro/Success.php b/Controller/Qliro/Success.php
new file mode 100644
index 0000000..c949d58
--- /dev/null
+++ b/Controller/Qliro/Success.php
@@ -0,0 +1,127 @@
+quoteAgent = $quoteAgent;
+ $this->successSession = $successSession;
+ }
+
+ /**
+ * Dispatch a QliroOne checkout success page or redirect to the cart
+ *
+ * @return \Magento\Framework\View\Result\Page|\Magento\Framework\Controller\Result\Redirect
+ */
+ public function execute()
+ {
+ if (!$this->successSession->getSuccessIncrementId()) {
+ return $this->resultRedirectFactory->create()->setPath('checkout/cart');
+ }
+
+ if (!$this->successSession->hasSuccessDisplayed()) {
+ $this->quoteAgent->clear();
+ }
+
+ $resultPage = $this->resultPageFactory->create();
+
+ if (!$this->successSession->hasSuccessDisplayed()) {
+ $this->_eventManager->dispatch(
+ 'checkout_onepage_controller_success_action',
+ ['order_ids' => [$this->successSession->getSuccessOrderId()]]
+ );
+ $this->successSession->setSuccessDisplayed();
+ }
+
+ return $resultPage;
+ }
+}
diff --git a/Gateway/Config/Config.php b/Gateway/Config/Config.php
new file mode 100644
index 0000000..606d992
--- /dev/null
+++ b/Gateway/Config/Config.php
@@ -0,0 +1,13 @@
+configInterface = $configInterface;
+ }
+
+ /**
+ * Retrieve method configured value
+ *
+ * @param array $subject
+ * @param int|null $storeId
+ *
+ * @return mixed
+ */
+ public function handle(array $subject, $storeId = null)
+ {
+ return $this->configInterface->getValue(SubjectReader::readField($subject), $storeId);
+ }
+}
diff --git a/Helper/Data.php b/Helper/Data.php
new file mode 100644
index 0000000..d009bec
--- /dev/null
+++ b/Helper/Data.php
@@ -0,0 +1,259 @@
+containerMapper = $containerMapper;
+ $this->logManager = $logManager;
+ $this->json = $json;
+ $this->resultFactory = $resultFactory;
+ $this->remoteAddress = $remoteAddress;
+ }
+
+ /**
+ * Read payload from request, log and prepare
+ *
+ * @param \Magento\Framework\App\Request\Http $request
+ * @param string $loggerMark
+ * @return array
+ */
+ public function readPreparedPayload(Http $request, $loggerMark = null)
+ {
+ $content = $request->getContent();
+ $this->logManager->setMark($loggerMark); // @todo: what if $loggerMark == null?
+ $payload = [];
+
+ $data = [
+ 'uri' => $request->getRequestUri(),
+ 'method' => $request->getMethod(),
+ ];
+
+ try {
+ $payload = $this->json->unserialize($content);
+ if (!empty($payload['MerchantReference'])) {
+ $this->logManager->setMerchantReference($payload['MerchantReference']);
+ }
+ $data['body'] = $payload;
+ } catch (\InvalidArgumentException $exception) {
+ $data['raw_body'] = $content;
+ $data['exception'] = $exception->getMessage();
+ }
+
+ $this->logManager->debug(
+ '<<< JSON payload has been received and processed.',
+ [
+ 'extra' => [
+ 'payload' => $data,
+ ],
+ ]
+ );
+
+ $this->logManager->setMark(null);
+
+ return $payload;
+ }
+
+ /**
+ * Prepare data for payload, log it for debugging and return in a result object
+ *
+ * @param string|array|\Qliro\QliroOne\Api\Data\ContainerInterface $payload
+ * @param int $resultCode
+ * @param \Magento\Framework\Controller\Result\Json $resultJson
+ * @param string $loggerMark
+ * @return \Magento\Framework\Controller\Result\Json
+ */
+ public function sendPreparedPayload($payload, $resultCode = 200, JsonResult $resultJson = null, $loggerMark = null)
+ {
+ if (!($resultJson instanceof JsonResult)) {
+ $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON);
+ }
+
+ $data = [
+ 'status_code' => $resultCode,
+ ];
+
+ $this->logManager->setMark($loggerMark); // @todo: what if $loggerMark == null?
+ $resultJson->setHttpResponseCode($resultCode);
+
+ if ($payload instanceof ContainerInterface) {
+ $payload = $this->containerMapper->toArray($payload);
+ $data['payload'] = $payload;
+ } elseif (is_string($payload)) {
+ try {
+ $payload = $this->json->unserialize($payload);
+ $data['payload'] = $payload;
+ } catch (\InvalidArgumentException $exception) {
+ $data['exception'] = $exception->getMessage();
+ }
+ } else {
+ $data['payload'] = $payload;
+ }
+
+ $this->logManager->debug('>>> Payload was prepared and sent in JSON response.', ['extra' => $data]);
+
+ $this->logManager->setMark(null);
+ $resultJson->setData($payload);
+
+ return $resultJson;
+ }
+
+ /**
+ * Format price to the format compatible with QliroOne API
+ *
+ * @param float $value
+ * @return string
+ */
+ public function formatPrice($value)
+ {
+ return \number_format($value, 2, '.', false);
+ }
+
+ /**
+ * Get current process PID
+ *
+ * @return int
+ */
+ public function getPid()
+ {
+ return \getmypid();
+ }
+
+ /**
+ * Get client ip
+ *
+ * @return string
+ */
+ public function getRemoteIp()
+ {
+ return $this->remoteAddress->getRemoteAddress();
+ }
+
+ /**
+ * Check if the process specified by its PID is alive.
+ * It works only for the processes of the same user.
+ *
+ * @param int $pid
+ * @return bool
+ */
+ public function isProcessAlive($pid)
+ {
+ if (!is_numeric($pid)) {
+ return false;
+ }
+ $pid = \intval($pid);
+ if (\function_exists('posix_getpgid')) {
+ return \posix_getpgid($pid) !== FALSE;
+ } elseif (\function_exists('posix_kill')) {
+ return \posix_kill($pid, 0);
+ } elseif (defined('PHP_OS') && PHP_OS == 'Linux') {
+ return \file_exists("/proc/$pid");
+ } else {
+ return \shell_exec(sprintf('ps -p %s | wc -l', $pid )) > 1;
+ }
+ }
+
+ /**
+ * Check if two quote addresses match
+ *
+ * @param \Magento\Quote\Model\Quote\Address $address1
+ * @param \Magento\Quote\Model\Quote\Address $address2
+ * @return bool
+ */
+ public function doAddressesMatch($address1, $address2)
+ {
+ $addressData1 = [
+ 'email' => $address1->getEmail(),
+ 'firstname' => $address1->getFirstname(),
+ 'lastname' => $address1->getLastname(),
+ 'care_of' => $address1->getCareOf(),
+ 'company' => $address1->getCompany(),
+ 'street' => $address1->getStreetFull(),
+ 'city' => $address1->getCity(),
+ 'region' => $address1->getRegion(),
+ 'region_id' => $address1->getRegionId(),
+ 'postcode' => $address1->getPostcode(),
+ 'country_id' => $address1->getCountryId(),
+ 'telephone' => $address1->getTelephone(),
+ ];
+
+ $addressData2 = [
+ 'email' => $address2->getEmail(),
+ 'firstname' => $address2->getFirstname(),
+ 'lastname' => $address2->getLastname(),
+ 'care_of' => $address2->getCareOf(),
+ 'company' => $address2->getCompany(),
+ 'street' => $address2->getStreetFull(),
+ 'city' => $address2->getCity(),
+ 'region' => $address2->getRegion(),
+ 'region_id' => $address2->getRegionId(),
+ 'postcode' => $address2->getPostcode(),
+ 'country_id' => $address2->getCountryId(),
+ 'telephone' => $address2->getTelephone(),
+ ];
+
+ return \json_encode($addressData1) == \json_encode($addressData2);
+ }
+}
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..b5bd1b1
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,28 @@
+#!groovy
+
+@Library('platform-jenkins-pipeline') _
+
+pipeline {
+ agent { label 'magento23' }
+
+ stages {
+ stage('Build Module') {
+ steps {
+ buildModule('magento2-module', nodeLabel: 'magento23')
+ }
+ }
+ stage('Publish Package') {
+ steps {
+ bitbucketStatus (key: 'publish_package', name: 'Publishing Package') {
+ generateComposerPackage(moduleName:"${env.GIT_URL}")
+ }
+ }
+ }
+ }
+
+ post {
+ always {
+ sendNotifications()
+ }
+ }
+}
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..3b171a4
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,15 @@
+Qliro AB reserves all rights in the Program as delivered. The Program
+or any portion thereof may not be reproduced in any form whatsoever without
+the written consent of Qliro, except as provided by licence. A licence
+under Qliro's rights in the Program may be available directly from
+Qliro.
+
+Disclaimer:
+THIS NOTICE MAY NOT BE REMOVED FROM THE PROGRAM BY ANY USER THEREOF.
+THE PROGRAM IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE PROGRAM OR THE USE OR OTHER DEALINGS
+IN THE PROGRAM.
diff --git a/Model/Api/Client/Exception/ClientException.php b/Model/Api/Client/Exception/ClientException.php
new file mode 100644
index 0000000..98e5daf
--- /dev/null
+++ b/Model/Api/Client/Exception/ClientException.php
@@ -0,0 +1,19 @@
+service = $service;
+ $this->config = $config;
+ $this->containerMapper = $containerMapper;
+ $this->json = $json;
+ $this->qliroOrderFactory = $qliroOrderFactory;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Perform QliroOne order creation
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCreateRequestInterface $qliroOrderCreateRequest
+ * @return int
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function createOrder(QliroOrderCreateRequestInterface $qliroOrderCreateRequest)
+ {
+ $this->logManager->addTag('sensitive');
+
+ $qliroOrderId = null;
+ $payload = $this->containerMapper->toArray($qliroOrderCreateRequest);
+
+ try {
+ $response = $this->service->post('checkout/merchantapi/orders', $payload);
+ $qliroOrderId = $response['OrderId'] ?? null;
+ } catch (\Exception $exception) {
+ $this->logManager->removeTag('sensitive');
+ $this->handleExceptions($exception);
+ }
+
+ $this->logManager->removeTag('sensitive');
+
+ return $qliroOrderId;
+ }
+
+ /**
+ * Get QliroOne order by its Qliro Order ID
+ *
+ * @param int $qliroOrderId
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderInterface
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function getOrder($qliroOrderId)
+ {
+ $this->logManager->addTag('sensitive');
+
+ /** @var QliroOrderInterface $qliroOrder */
+ $qliroOrder = $this->qliroOrderFactory->create();
+
+ try {
+ $response = $this->service->get('checkout/merchantapi/orders/{OrderId}', ['OrderId' => $qliroOrderId]);
+ $this->containerMapper->fromArray($response, $qliroOrder);
+ } catch (\Exception $exception) {
+ $this->logManager->removeTag('sensitive');
+ $this->handleExceptions($exception);
+ }
+
+ $this->logManager->removeTag('sensitive');
+
+ return $qliroOrder;
+ }
+
+ /**
+ * Update QliroOne order
+ *
+ * @param int $qliroOrderId
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderUpdateRequestInterface $qliroOrderUpdateRequest
+ * @return int
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function updateOrder($qliroOrderId, QliroOrderUpdateRequestInterface $qliroOrderUpdateRequest)
+ {
+ $this->logManager->addTag('sensitive');
+
+ $payload = $this->containerMapper->toArray($qliroOrderUpdateRequest);
+ $payload['OrderId'] = $qliroOrderId;
+
+ try {
+ $response = $this->service->put('checkout/merchantapi/orders/{OrderId}', $payload);
+ } catch (\Exception $exception) {
+ $this->logManager->removeTag('sensitive');
+ $this->handleExceptions($exception);
+ }
+
+ $this->logManager->removeTag('sensitive');
+
+ return $qliroOrderId;
+ }
+
+ /**
+ * Handle exceptions that come from the API response
+ *
+ * @param \Exception $exception
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ private function handleExceptions(\Exception $exception)
+ {
+ if ($exception instanceof RequestException) {
+ $data = $this->json->unserialize($exception->getResponse()->getBody());
+
+ if (isset($data['ErrorCode']) && isset($data['ErrorMessage'])) {
+ if (!($exception instanceof TerminalException)) {
+ $this->logManager->critical($exception, ['extra' => $data]);
+ }
+
+ throw new MerchantApiException(
+ __('Error [%1]: %2', $data['ErrorCode'], $data['ErrorMessage'])
+ );
+ }
+ }
+
+ if (!($exception instanceof TerminalException)) {
+ $this->logManager->critical($exception);
+ }
+
+ throw new ClientException(__('Request to Qliro One has failed.'), $exception);
+ }
+}
diff --git a/Model/Api/Client/OrderManagement.php b/Model/Api/Client/OrderManagement.php
new file mode 100644
index 0000000..d3a3dda
--- /dev/null
+++ b/Model/Api/Client/OrderManagement.php
@@ -0,0 +1,341 @@
+service = $service;
+ $this->config = $config;
+ $this->containerMapper = $containerMapper;
+ $this->json = $json;
+ $this->qliroOrderFactory = $qliroOrderFactory;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Get admin QliroOne order by its Qliro Order ID
+ *
+ * @param int $qliroOrderId
+ * @return \Qliro\QliroOne\Api\Data\AdminOrderInterface
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function getOrder($qliroOrderId)
+ {
+ $container = null;
+
+ try {
+ $response = $this->service->get('checkout/adminapi/orders/{OrderId}', ['OrderId' => $qliroOrderId]);
+
+ /** @var \Qliro\QliroOne\Api\Data\AdminOrderInterface $container */
+ $container = $this->containerMapper->fromArray($response, AdminOrderInterface::class);
+ } catch (\Exception $exception) {
+ $this->handleExceptions($exception);
+ }
+
+ return $container;
+ }
+
+ /**
+ * Send a "Mark items as shipped" request
+ *
+ * @param \Qliro\QliroOne\Api\Data\AdminMarkItemsAsShippedRequestInterface $request
+ * @param int|null $storeId
+ * @return \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function markItemsAsShipped(AdminMarkItemsAsShippedRequestInterface $request, $storeId = null)
+ {
+ $container = null;
+
+ try {
+ $payload = $this->containerMapper->toArray($request);
+ $response = $this->service->post('checkout/adminapi/markitemsasshipped/withitems', $payload, $storeId);
+
+ /** @var \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface $container */
+ $container = $this->containerMapper->fromArray($response, AdminTransactionResponseInterface::class);
+ } catch (\Exception $exception) {
+ $this->handleExceptions($exception);
+ }
+
+ return $container;
+ }
+
+ /**
+ * Cancel admin QliroOne order
+ *
+ * @param \Qliro\QliroOne\Api\Data\AdminCancelOrderRequestInterface $request
+ * @param int|null $storeId
+ * @return \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function cancelOrder(AdminCancelOrderRequestInterface $request, $storeId = null)
+ {
+ $container = null;
+
+ try {
+ $payload = $this->containerMapper->toArray($request);
+ $response = $this->service->post('checkout/adminapi/cancelorder', $payload, $storeId);
+
+ /** @var \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface $container */
+ $container = $this->containerMapper->fromArray($response, AdminTransactionResponseInterface::class);
+ } catch (\Exception $exception) {
+ // Workaround for having cancelOrder NOT throwing exception in case of success
+ if ($exception instanceof RequestException) {
+ $data = $this->json->unserialize($exception->getResponse()->getBody());
+
+ $errorCode = $data['ErrorCode'] ?? null;
+
+ if ($errorCode === 'ORDER_HAS_BEEN_CANCELLED') {
+ /** @var \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface $container */
+ $container = $this->containerMapper->fromArray(
+ ['Status' => CheckoutStatusInterface::STATUS_REFUSED],
+ AdminTransactionResponseInterface::class
+ );
+
+ return $container;
+ }
+ }
+
+ // Otherwise, handle exceptions as usual
+ $this->handleExceptions($exception);
+ }
+
+ return $container;
+ }
+
+ /**
+ * Update QliroOne order merchant reference
+ *
+ * @param \Qliro\QliroOne\Api\Data\AdminUpdateMerchantReferenceRequestInterface $request
+ * @param int|null $storeId
+ * @return \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function updateMerchantReference(AdminUpdateMerchantReferenceRequestInterface $request, $storeId = null)
+ {
+ $container = null;
+
+ try {
+ $payload = $this->containerMapper->toArray($request);
+ $response = $this->service->post('checkout/adminapi/updatemerchantreference', $payload, $storeId);
+
+ /** @var \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface $container */
+ $container = $this->containerMapper->fromArray($response, AdminTransactionResponseInterface::class);
+ } catch (\Exception $exception) {
+ /**
+ * This function is called inside a notification from Qliro. That notification should just respond with ok
+ * Unless something is very wrong. What the call to updatemerchantreference responds with should NOT
+ * make any difference in what that notification should respond! The return from this function only logs
+ * the transactionId....
+ * @todo This needs to be fixed properly once we can debug notifications
+ */
+
+ // Workaround for having updateMerchantReference NOT throwing exception in case of success
+// if ($exception instanceof RequestException) {
+// $data = $this->json->unserialize($exception->getResponse()->getBody());
+//
+// $errorCode = $data['ErrorCode'] ?? null;
+//
+// if ($errorCode === 'ORDER_HAS_BEEN_CANCELLED') {
+// /** @var \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface $container */
+// $container = $this->containerMapper->fromArray(
+// ['Status' => CheckoutStatusInterface::STATUS_REFUSED],
+// AdminTransactionResponseInterface::class
+// );
+//
+// return $container;
+// }
+// }
+//
+// $this->handleExceptions($exception);
+ }
+
+ return $container;
+ }
+
+ /**
+ * Make a call "Return with items"
+ * @todo Not used?
+ *
+ * @param \Qliro\QliroOne\Api\Data\AdminReturnWithItemsRequestInterface $request
+ * @param int|null $storeId
+ * @return \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function returnWithItems(AdminReturnWithItemsRequestInterface $request, $storeId = null)
+ {
+ $container = null;
+
+ try {
+ $payload = $this->containerMapper->toArray($request);
+ $response = $this->service->post('checkout/adminapi/returnwithitems', $payload, $storeId);
+
+ /** @var \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface $container */
+ $container = $this->containerMapper->fromArray($response, AdminTransactionResponseInterface::class);
+ } catch (\Exception $exception) {
+ $this->handleExceptions($exception);
+ }
+
+ return $container;
+ }
+
+ /**
+ * Get admin QliroOne order payment transaction
+ * @todo Not used?
+ *
+ * @param int $paymentTransactionId
+ * @param int|null $storeId
+ * @return \Qliro\QliroOne\Api\Data\AdminOrderPaymentTransactionInterface
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ public function getPaymentTransaction($paymentTransactionId, $storeId = null)
+ {
+ $container = null;
+
+ try {
+ $response = $this->service->get(
+ 'checkout/adminapi/paymenttransactions/{PaymentTransactionId}',
+ ['PaymentTransactionId' => $paymentTransactionId],
+ $storeId
+ );
+
+ /** @var \Qliro\QliroOne\Api\Data\AdminOrderPaymentTransactionInterface $container */
+ $container = $this->containerMapper->fromArray($response, AdminOrderPaymentTransactionInterface::class);
+ } catch (\Exception $exception) {
+ $this->handleExceptions($exception);
+ }
+
+ return $container;
+ }
+
+ /**
+ * Retry a reversal payment
+ *
+ * @param int $paymentReference
+ * @param int|null $storeId
+ * @return \Qliro\QliroOne\Api\Data\AdminOrderPaymentTransactionInterface|null
+ * @throws ClientException
+ */
+ public function retryReversalPayment($paymentReference, $storeId = null)
+ {
+ $container = null;
+
+ try {
+ $response = $this->service->post(
+ 'checkout/adminapi/retryreversalpaymenttransaction',
+ ['PaymentReference' => $paymentReference],
+ $storeId
+ );
+
+ /** @var \Qliro\QliroOne\Api\Data\AdminOrderPaymentTransactionInterface $container */
+ $container = $this->containerMapper->fromArray($response, AdminOrderPaymentTransactionInterface::class);
+ } catch (\Exception $exception) {
+ $this->handleExceptions($exception);
+ }
+
+ return $container;
+ }
+
+ /**
+ * Handle exceptions that come from the API response
+ *
+ * @param \Exception $exception
+ * @throws \Qliro\QliroOne\Model\Api\Client\Exception\ClientException
+ */
+ private function handleExceptions(\Exception $exception)
+ {
+ if ($exception instanceof RequestException) {
+ $data = $this->json->unserialize($exception->getResponse()->getBody());
+
+ if (isset($data['ErrorCode']) && isset($data['ErrorMessage'])) {
+ if (!($exception instanceof TerminalException)) {
+ $this->logManager->critical($exception, ['extra' => $data]);
+ }
+
+ throw new OrderManagementApiException(
+ __('Error [%1]: %2', $data['ErrorCode'], $data['ErrorMessage'])
+ );
+ }
+ }
+
+ if (!($exception instanceof TerminalException)) {
+ $this->logManager->critical($exception);
+ }
+
+ throw new ClientException(__('Request to Qliro One has failed.'), $exception);
+ }
+}
diff --git a/Model/Api/Service.php b/Model/Api/Service.php
new file mode 100644
index 0000000..e618fb4
--- /dev/null
+++ b/Model/Api/Service.php
@@ -0,0 +1,309 @@
+config = $config;
+ $this->client = $client;
+ $this->json = $json;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Perform GET request
+ *
+ * @param string $endpoint
+ * @param array $data
+ * @param int|null $storeId
+ * @return array
+ * @throws \InvalidArgumentException
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function get($endpoint, $data = [], $storeId = null)
+ {
+ $this->applyParams($endpoint, $data);
+
+ return $this->call(self::METHOD_GET, $endpoint, $data, $storeId);
+ }
+
+ /**
+ * Perform POST request
+ *
+ * @param string $endpoint
+ * @param array $data
+ * @param int|null $storeId
+ * @return array
+ * @throws \InvalidArgumentException
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function post($endpoint, $data = [], $storeId = null)
+ {
+ return $this->call(self::METHOD_POST, $endpoint, $data, $storeId);
+ }
+
+ /**
+ * Perform PUT request
+ *
+ * @param string $endpoint
+ * @param array $data
+ * @param int|null $storeId
+ * @return array
+ * @throws \InvalidArgumentException
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function put($endpoint, $data = [], $storeId = null)
+ {
+ $this->applyParams($endpoint, $data);
+
+ return $this->call(self::METHOD_PUT, $endpoint, $data, $storeId);
+ }
+
+ /**
+ * Replace all placeholders within endpoint from the $params array
+ *
+ * @param string $endpoint
+ * @param array $params
+ */
+ private function applyParams(&$endpoint, &$params)
+ {
+ foreach ($params as $key => $value) {
+ if (!is_scalar($value)) {
+ continue;
+ }
+ $modifiedEndpoint = str_replace('{' . $key . '}', $value, $endpoint);
+
+ if ($modifiedEndpoint !== $endpoint) {
+ unset($params[$key]);
+ $endpoint = $modifiedEndpoint;
+ }
+ }
+
+ $endpoint = preg_replace('/\{[^}]+\}/', '*', $endpoint);
+ }
+
+ /**
+ * Perform an API call
+ *
+ * @param string $method
+ * @param string $endpoint
+ * @param array $body
+ * @param int|null $storeId
+ * @return array
+ * @throws \InvalidArgumentException
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ private function call($method, $endpoint, $body = [], $storeId = null)
+ {
+ $this->logManager->setMark('REST API');
+
+ if ($method === self::METHOD_GET) {
+ $payload = '';
+ $options[RequestOptions::QUERY] = $body;
+ } else {
+ if (!isset($body['MerchantApiKey'])) {
+ $body['MerchantApiKey'] = $this->config->getMerchantApiKey($storeId);
+ }
+ $payload = $this->json->serialize($body);
+ $options[RequestOptions::BODY] = $payload;
+ }
+
+ $headers = [
+ self::HEADER_CONTENT_TYPE => self::HEADER_CONTENT_TYPE_JSON,
+ self::HEADER_AUTHENTICATION => $this->getAuthenticationToken($payload, $method, $storeId)
+ ];
+
+ $options[RequestOptions::HEADERS] = $headers;
+ $options[RequestOptions::ON_STATS] = [$this, 'receiveStats'];
+
+ $this->duration = 0.0;
+ $endpointUri = $this->prepareEndpointUri($endpoint, $storeId);
+
+ $this->logManager->debug(
+ '>>> {method} {endpoint}',
+ [
+ 'method' => $method,
+ 'endpoint' => $endpoint,
+ 'extra' => [
+ 'uri' => $endpointUri,
+ 'body' => $body,
+ ],
+ ]
+ );
+
+ try {
+ $response = $this->client->request($method, $endpointUri, $options);
+ $responseData = $this->getResponseData($response);
+
+ $this->logManager->debug(
+ '<<< Result in {duration} seconds',
+ [
+ 'duration' => $this->duration,
+ 'extra' => [
+ 'uri' => $endpointUri,
+ 'request' => $body,
+ 'status_code' => $response->getStatusCode(),
+ 'response' => $responseData,
+ ]
+ ]
+ );
+ } catch (\Exception $exception) {
+ $exceptionData = [
+ 'exception' => $exception->getMessage(),
+ 'uri' => $endpointUri,
+ 'request' => $body,
+ ];
+
+ if ($exception instanceof ClientException) {
+ $response = $exception->getResponse();
+
+ $exceptionData = array_merge($exceptionData, [
+ 'status_code' => $response->getStatusCode(),
+ 'error_reason' => $response->getReasonPhrase(),
+ 'response' => $this->getResponseData($response),
+ ]);
+ }
+
+ $this->logManager->error(
+ '<<< Exception after {duration} seconds',
+ [
+ 'duration' => $this->duration,
+ 'extra' => $exceptionData
+ ]
+ );
+
+ throw new TerminalException($exception->getMessage(), $exception->getCode(), $exception);
+ } finally {
+ $this->logManager->setMark(null);
+ }
+
+ return $responseData;
+ }
+
+ /**
+ * @param \GuzzleHttp\TransferStats $stats
+ */
+ public function receiveStats(TransferStats $stats)
+ {
+ $this->duration = $stats->getTransferTime();
+ }
+
+ /**
+ * Prepare a full URI to the endpoint
+ *
+ * @param string $endpoint
+ * @param int|null $storeId
+ * @return string
+ */
+ private function prepareEndpointUri($endpoint, $storeId = null)
+ {
+ $baseUri = $this->config->getApiType($storeId) === 'prod' ? self::QLIRO_PROD_API_URL : self::QLIRO_SANDBOX_API_URL;
+
+ return implode('/', [$baseUri, trim($endpoint, '/')]);
+ }
+
+ /**
+ * @param string $body
+ * @param string $method
+ * @param int|null $storeId
+ * @return string
+ */
+ private function getAuthenticationToken($body, $method = self::METHOD_POST, $storeId = null)
+ {
+ if ($method === self::METHOD_GET) {
+ $body = '';
+ }
+
+ $secret = $this->config->getMerchantApiSecret($storeId);
+ $secretString = base64_encode(hash('sha256', $body . $secret, true));
+ $token = trim(implode(' ', [self::AUTHENTICATION_PREFIX, $secretString]));
+
+ return $token;
+ }
+
+ /**
+ * Get and decode request data
+ *
+ * @param \Psr\Http\Message\ResponseInterface $response
+ * @return array
+ */
+ private function getResponseData(ResponseInterface $response): array
+ {
+ $responseString = (string)$response->getBody();
+
+ try {
+ $responseData = $responseString ? (array)$this->json->unserialize($responseString) : [];
+ } catch (\InvalidArgumentException $exception) {
+ $responseData = [];
+ }
+
+ return $responseData;
+ }
+}
diff --git a/Model/Carrier/Unifaun.php b/Model/Carrier/Unifaun.php
new file mode 100644
index 0000000..5bf22c1
--- /dev/null
+++ b/Model/Carrier/Unifaun.php
@@ -0,0 +1,152 @@
+_rateResultFactory = $rateResultFactory;
+ $this->_rateMethodFactory = $rateMethodFactory;
+ parent::__construct($scopeConfig, $rateErrorFactory, $logger, $data);
+ $this->checkoutSession = $checkoutSession;
+ $this->linkRepository = $linkRepository;
+ $this->qliroConfig = $qliroConfig;
+ }
+
+ /**
+ * get allowed methods
+ * @return array
+ */
+ public function getAllowedMethods()
+ {
+ return [$this->_code => $this->getConfigData('name')];
+ }
+
+ /**
+ * @return float
+ */
+ private function getShippingPrice()
+ {
+ $quote = $this->getQuote();
+ try {
+ $link = $this->linkRepository->getByQuoteId($quote->getId());
+ if ($link->getUnifaunShippingAmount()) {
+ $shippingPrice = $link->getUnifaunShippingAmount();
+ } else {
+ $configPrice = $this->getConfigData('price');
+ $shippingPrice = $this->getFinalPriceWithHandlingFee($configPrice);
+ }
+ } catch (\Exception $exception) {
+ $configPrice = $this->getConfigData('price');
+ $shippingPrice = $this->getFinalPriceWithHandlingFee($configPrice);
+ }
+
+ return $shippingPrice;
+ }
+
+ /**
+ * @param RateRequest $request
+ * @return bool|Result
+ */
+ public function collectRates(RateRequest $request)
+ {
+ if (!$this->getConfigFlag('active') ||
+ !$this->qliroConfig->isUnifaunEnabled($this->getStore())) {
+ return false;
+ }
+
+ /** @var \Magento\Shipping\Model\Rate\Result $result */
+ $result = $this->_rateResultFactory->create();
+
+ /** @var \Magento\Quote\Model\Quote\Address\RateResult\Method $method */
+ $method = $this->_rateMethodFactory->create();
+
+ $method->setCarrier($this->_code);
+ $method->setCarrierTitle($this->getConfigData('title'));
+
+ $method->setMethod($this->_code);
+ $method->setMethodTitle($this->getConfigData('name'));
+
+ $amount = $this->getShippingPrice();
+
+ $method->setPrice($amount);
+ $method->setCost($amount);
+
+ $result->append($method);
+
+ return $result;
+ }
+
+ /**
+ * Get current quote from checkout session
+ *
+ * @return \Magento\Quote\Model\Quote
+ */
+ private function getQuote()
+ {
+ return $this->checkoutSession->getQuote();
+ }
+}
\ No newline at end of file
diff --git a/Model/CheckoutConfigProvider.php b/Model/CheckoutConfigProvider.php
new file mode 100644
index 0000000..85d7e1b
--- /dev/null
+++ b/Model/CheckoutConfigProvider.php
@@ -0,0 +1,106 @@
+quote = $checkoutSession->getQuote();
+ $this->storeManager = $storeManager;
+ $this->ajaxToken = $ajaxToken;
+ $this->qliroConfig = $qliroConfig;
+ $this->fee = $fee;
+ }
+
+ /**
+ * @return array
+ */
+ public function getConfig()
+ {
+ $feeSetup = $this->fee->getFeeSetup($this->quote->getStoreId());
+ $feeSetup = $this->fee->applyDisplayFlagsToFeeArray($this->quote, $feeSetup);
+ return [
+ 'qliro' => [
+ 'enabled' => $this->qliroConfig->isActive(),
+ 'isDebug' => $this->qliroConfig->isDebugMode(),
+ 'isEagerCheckoutRefresh' => $this->qliroConfig->isEagerCheckoutRefresh(),
+ 'checkoutTitle' => $this->qliroConfig->getTitle(),
+ 'securityToken' => $this->ajaxToken->setQuote($this->quote)->getToken(),
+ 'updateQuoteUrl' => $this->getUrl('checkout/qliro_ajax/updateQuote'),
+ 'updateCustomerUrl' => $this->getUrl('checkout/qliro_ajax/updateCustomer'),
+ 'updateShippingMethodUrl' => $this->getUrl('checkout/qliro_ajax/updateShippingMethod'),
+ 'updateShippingPriceUrl' => $this->getUrl('checkout/qliro_ajax/updateShippingPrice'),
+ 'updatePaymentMethodUrl' => $this->getUrl('checkout/qliro_ajax/updatePaymentMethod'),
+ 'pollPendingUrl' => $this->getUrl('checkout/qliro_ajax/pollPending'),
+ 'qliroone_fee' => [
+ 'fee_setup' => $feeSetup,
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Get a store-specific URL with provided path
+ *
+ * @param string $path
+ * @return string
+ */
+ private function getUrl($path)
+ {
+ $store = $this->storeManager->getStore();
+
+ return $store->getUrl($path);
+ }
+}
diff --git a/Model/Config.php b/Model/Config.php
new file mode 100644
index 0000000..68521f1
--- /dev/null
+++ b/Model/Config.php
@@ -0,0 +1,622 @@
+adapter = $adapter;
+ $this->config = $config;
+ $this->json = $json;
+ }
+
+ /**
+ * Check if the payment method is active
+ *
+ * @return bool
+ */
+ public function isActive()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_ACTIVE);
+ }
+
+ /**
+ * Check if the GeoIP capability should be used
+ *
+ * @return bool
+ */
+ public function isUseGeoIp()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_GEOIP);
+ }
+
+ /**
+ * Check whether debug mode is on
+ *
+ * @return bool
+ */
+ public function isDebugMode()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_DEBUG);
+ }
+
+ /**
+ * Check whether an Eager Checkout Refresh mode is on
+ *
+ * @return bool
+ */
+ public function isEagerCheckoutRefresh()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_EAGER_CHECKOUT_REFRESH);
+ }
+
+ /**
+ * Check whether callbacks should be routed through a public server
+ *
+ * @return bool
+ */
+ public function redirectCallbacks()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_REDIRECT_CALLBACKS);
+ }
+
+ /**
+ * Get url for callback server
+ *
+ * @return string
+ */
+ public function getCallbackUri()
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_CALLBACK_URI);
+ }
+
+ /**
+ * @return int
+ */
+ public function getLoggingLevel()
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_LOGGING_LEVEL);
+ }
+
+ /**
+ * Get payment method title
+ *
+ * @return string
+ */
+ public function getTitle()
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_TITLE);
+ }
+
+ /**
+ * Get the status order will end up on successful payment
+ *
+ * @return string
+ */
+ public function getOrderStatus()
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_ORDER_STATUS);
+ }
+
+ /**
+ * @return bool
+ */
+ public function getAllowSpecific()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_ALLOW_SPECIFIC);
+ }
+
+ /**
+ * @return string
+ */
+ public function getSpecificCountries()
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_COUNTRIES);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return bool
+ */
+ public function shouldCaptureOnShipment($storeId = null)
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_CAPTURE_ON_SHIPMENT, $storeId);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return bool
+ */
+ public function shouldCaptureOnInvoice($storeId = null)
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_CAPTURE_ON_INVOICE, $storeId);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return bool
+ */
+ public function shouldAskForNewsletterSignup($storeId = null)
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_NEWSLETTER_SIGNUP, $storeId);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return bool
+ */
+ public function requireIdentityVerification($storeId = null)
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_REQUIRE_IDENTITY_VERIFICATION, $storeId);
+ }
+
+ /**
+ * Get API type (may be either "sandbox" or "prod"
+ *
+ * @param int|null $storeId
+ * @return string
+ */
+ public function getApiType($storeId = null)
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_API_TYPE, $storeId);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return string
+ */
+ public function getMerchantApiKey($storeId = null)
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_MERCHANT_API_KEY, $storeId);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return string
+ */
+ public function getMerchantApiSecret($storeId = null)
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_MERCHANT_API_SECRET, $storeId);
+ }
+
+ /**
+ * @return bool
+ */
+ public function presetAddress()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_PRESET_ADDRESS);
+ }
+
+ /**
+ * @return string
+ */
+ public function getStylingBackgroundColor()
+ {
+ return $this->checkHexColor($this->adapter->getConfigData(self::QLIROONE_STYLING_BACKGROUND));
+ }
+
+ /**
+ * @return string
+ */
+ public function getStylingPrimaryColor()
+ {
+ return $this->checkHexColor($this->adapter->getConfigData(self::QLIROONE_STYLING_PRIMARY));
+ }
+
+ /**
+ * @return string
+ */
+ public function getStylingCallToActionColor()
+ {
+ return $this->checkHexColor($this->adapter->getConfigData(self::QLIROONE_STYLING_CALL_TO_ACTION));
+ }
+
+ /**
+ * @return string
+ */
+ public function getStylingHoverColor()
+ {
+ return $this->checkHexColor($this->adapter->getConfigData(self::QLIROONE_STYLING_HOVER));
+ }
+
+ /**
+ * @return int
+ */
+ public function getStylingRadius()
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_STYLING_RADIUS);
+ }
+
+ /**
+ * @return int
+ */
+ public function getStylingButtonCurnerRadius()
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_STYLING_BUTTON_CORNER);
+ }
+
+ /**
+ * @return string
+ */
+ public function getFeeMerchantReference()
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_FEE_MERCHANT_REFERENCE);
+ }
+
+ /**
+ * @return string
+ */
+ public function getTermsUrl()
+ {
+ $value = $this->adapter->getConfigData(self::QLIROONE_TERMS_URL);
+
+ return $value ? (string)$value : null;
+ }
+
+ /**
+ * @return string
+ */
+ public function getIntegrityPolicyUrl()
+ {
+ $value = $this->adapter->getConfigData(self::QLIROONE_INTEGRITY_POLICY_URL);
+
+ return $value ? (string)$value : null;
+ }
+
+ /**
+ * Check if HTTP Auth for callbacks is enabled
+ *
+ * @return bool
+ */
+ public function isHttpAuthEnabled()
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_ENABLE_HTTP_AUTH);
+ }
+
+ /**
+ * Get an HTTP Auth username for callbacks
+ *
+ * @return string
+ */
+ public function getCallbackHttpAuthUsername()
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_HTTP_AUTH_USERNAME);
+ }
+
+ /**
+ * Get an HTTP Auth password for callbacks
+ *
+ * @return string
+ */
+ public function getCallbackHttpAuthPassword()
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_HTTP_AUTH_PASSWORD);
+ }
+
+ /**
+ * Get XDebug session flag name for callbacks
+ *
+ * @return string
+ */
+ public function getCallbackXdebugSessionFlagName()
+ {
+ if (!$this->isDebugMode()) {
+ return '';
+ }
+ return (string)$this->adapter->getConfigData(self::QLIROONE_XDEBUG_SESSION_FLAG_NAME);
+ }
+
+ /**
+ * Dummy config for payment method compatibility
+ *
+ * @return boolean
+ */
+ public function shouldUpdateQuoteBilling()
+ {
+ return true;
+ }
+
+ /**
+ * Dummy config for payment method compatibility
+ *
+ * @return boolean
+ */
+ public function shouldUpdateQuoteShipping()
+ {
+ return true;
+ }
+
+ /**
+ * Check if the value a proper HEX color code, return null otherwise
+ *
+ * @param string $value
+ * @return string|null
+ */
+ private function checkHexColor($value)
+ {
+ return preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', trim($value)) ? trim($value) : null;
+ }
+
+ /**
+ * Get TaxClass for Fee
+ *
+ * @param \Magento\Store\Model\Store|int|null $store
+ * @return string|null
+ */
+ public function getFeeTaxClass($store = null)
+ {
+ return $this->config->getValue(
+ self::XML_PATH_TAX_CLASS,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ }
+
+ /**
+ * Check ability to display prices including tax for payment fee in shopping cart
+ *
+ * @param \Magento\Store\Model\Store|int|null $store
+ * @return bool
+ */
+ public function displayCartPaymentFeeIncludeTaxPrice($store = null)
+ {
+ $configValue = $this->config->getValue(
+ self::XML_PATH_PRICE_DISPLAY_CART_PAYMENT_FEE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ return $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH ||
+ $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_INCLUDING_TAX;
+ }
+
+ /**
+ * Check ability to display prices excluding tax for payment fee in shopping cart
+ *
+ * @param \Magento\Store\Model\Store|int|null $store
+ * @return bool
+ */
+ public function displayCartPaymentFeeExcludeTaxPrice($store = null)
+ {
+ $configValue = $this->config->getValue(
+ self::XML_PATH_PRICE_DISPLAY_CART_PAYMENT_FEE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ return $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_EXCLUDING_TAX;
+ }
+
+ /**
+ * Check ability to display both prices for payment fee in shopping cart
+ *
+ * @param \Magento\Store\Model\Store|int|null $store
+ * @return bool
+ */
+ public function displayCartPaymentFeeBothPrices($store = null)
+ {
+ $configValue = $this->config->getValue(
+ self::XML_PATH_PRICE_DISPLAY_CART_PAYMENT_FEE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ return $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH;
+ }
+
+ /**
+ * Check ability to display prices including tax for payment fee in backend sales
+ *
+ * @param \Magento\Store\Model\Store|int|null $store
+ * @return bool
+ */
+ public function displaySalesPaymentFeeIncludeTaxPrice($store = null)
+ {
+ $configValue = $this->config->getValue(
+ self::XML_PATH_PRICE_DISPLAY_SALES_PAYMENT_FEE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ return $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH ||
+ $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_INCLUDING_TAX;
+ }
+
+ /**
+ * Check ability to display prices excluding tax for payment fee in backend sales
+ *
+ * @param \Magento\Store\Model\Store|int|null $store
+ * @return bool
+ */
+ public function displaySalesPaymentFeeExcludeTaxPrice($store = null)
+ {
+ $configValue = $this->config->getValue(
+ self::XML_PATH_PRICE_DISPLAY_SALES_PAYMENT_FEE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ return $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_EXCLUDING_TAX;
+ }
+
+ /**
+ * Check ability to display both prices for payment fee in backend sales
+ *
+ * @param \Magento\Store\Model\Store|int|null $store
+ * @return bool
+ */
+ public function displaySalesPaymentFeeBothPrices($store = null)
+ {
+ $configValue = $this->config->getValue(
+ self::XML_PATH_PRICE_DISPLAY_SALES_PAYMENT_FEE,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ return $configValue == \Magento\Tax\Model\Config::DISPLAY_TYPE_BOTH;
+ }
+
+ /**
+ * Check if shipping prices include tax
+ *
+ * @param null|string|bool|int|Store $store
+ * @return bool
+ */
+ public function paymentFeeIncludesTax($store = null)
+ {
+ $configValue = $this->config->getValue(
+ self::CONFIG_XML_PATH_PAYMENT_FEE_INCLUDES_TAX,
+ \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
+ $store
+ );
+ return (bool)$configValue;
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return bool
+ */
+ public function isUnifaunEnabled($storeId)
+ {
+ return (bool)$this->adapter->getConfigData(self::QLIROONE_UNIFAUN_ENABLED, $storeId);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return string
+ */
+ public function getUnifaunCheckoutId($storeId = null)
+ {
+ return (string)$this->adapter->getConfigData(self::QLIROONE_UNIFAUN_CHECKOUT_ID, $storeId);
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return array
+ */
+ public function getUnifaunParameters($storeId = null)
+ {
+ $str = (string)$this->adapter->getConfigData(self::QLIROONE_UNIFAUN_PARAMETERS, $storeId);
+ if ($str) {
+ return $this->json->unserialize($str);
+ }
+
+ return [];
+ }
+
+ /**
+ * @param int|null $storeId
+ * @return bool
+ */
+ public function getMinimumCustomerAge($storeId = null)
+ {
+ return (int)$this->adapter->getConfigData(self::QLIROONE_MINIMUM_CUSTOMER_AGE, $storeId);
+ }
+}
diff --git a/Model/Config/Source/ApiType.php b/Model/Config/Source/ApiType.php
new file mode 100644
index 0000000..d7116d5
--- /dev/null
+++ b/Model/Config/Source/ApiType.php
@@ -0,0 +1,30 @@
+ 'sandbox', 'label' => __('Sandbox')],
+ ['value' => 'prod', 'label' => __('Production')],
+ ];
+
+ return $result;
+ }
+}
diff --git a/Model/Config/Source/LogLevels.php b/Model/Config/Source/LogLevels.php
new file mode 100644
index 0000000..61ca353
--- /dev/null
+++ b/Model/Config/Source/LogLevels.php
@@ -0,0 +1,39 @@
+logger = $logger;
+ }
+
+ /**
+ * @return array
+ */
+ public function toOptionArray()
+ {
+ $result = [['value' => 0, 'label' => 'No Logging']];
+ foreach ($this->logger->getLevels() as $label => $value) {
+ $result[] = ['value' => $value, 'label' => ucwords(strtolower($label))];
+ }
+
+ return $result;
+ }
+}
diff --git a/Model/ContainerMapper.php b/Model/ContainerMapper.php
new file mode 100644
index 0000000..ba7891f
--- /dev/null
+++ b/Model/ContainerMapper.php
@@ -0,0 +1,258 @@
+objectManager = $objectManager;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Recursively convert an instance of container into associative array ready for JSON payload
+ *
+ * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
+ * @param array $mandatoryFields
+ * @return array
+ */
+ public function toArray(ContainerInterface $container, $mandatoryFields = [])
+ {
+ $result = [];
+
+ foreach ($this->getGetterKeys($container) as $key) {
+ $getterName = 'get' . $key;
+ $value = $container->$getterName();
+
+ if (in_array($key, $mandatoryFields, true) || !is_null($value)) {
+ if ($this->checkIfArrayWithNumericKeys($value)) {
+ $value = $this->iterateArray($value);
+ } elseif ($value instanceof ContainerInterface) {
+ $value = $this->toArray($value);
+ }
+
+ $result[$key] = $value;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param array $data
+ * @param \Qliro\QliroOne\Api\Data\ContainerInterface|string $container
+ * @return \Qliro\QliroOne\Api\Data\ContainerInterface
+ */
+ public function fromArray($data, $container)
+ {
+ if (is_string($container)) {
+ $container = $this->objectManager->create($container);
+ }
+
+ $className = get_class($container);
+ $keyHash = array_flip($this->getSetterKeys($container));
+
+ if (is_null($data)) {
+ $this->logManager->debug(
+ 'Unexpected null',
+ [
+ 'extra' => sprintf('%s; %s', get_class($container), $this->logManager->getStack())
+ ]
+ );
+ }
+ foreach ($data as $key => $value) {
+ $key = ucfirst($key);
+ $setterName = 'set' . $key;
+
+ if (isset($keyHash[$key])) {
+ $setterType = $this->setterTypeCache[$className][$key] ?? null;
+
+ if ($this->checkIfArrayWithNumericKeys($value)) {
+ $value = $this->iterateArray($value, $setterType);
+ } elseif ($setterType) {
+ // If value is an associative array, wrap it into container
+ $subClassName = rtrim($setterType, '[]');
+ $subContainer = $this->fromArray($value, $subClassName);
+ $value = $subContainer;
+ }
+
+ $container->$setterName($value);
+ }
+ }
+
+ return $container;
+ }
+
+ /**
+ * Fetch a cached list of data fields in container that have getters
+ *
+ * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
+ * @return array
+ */
+ private function getGetterKeys(ContainerInterface $container)
+ {
+ $className = get_class($container);
+
+ if (!isset($this->getterCache[$className])) {
+ $this->collectAccessors($container);
+ }
+
+ return $this->getterCache[$className];
+ }
+
+ /**
+ * Fetch a cached list of data fields in container that have setters
+ *
+ * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
+ * @return array
+ */
+ private function getSetterKeys(ContainerInterface $container)
+ {
+ $className = get_class($container);
+
+ if (!isset($this->setterCache[$className])) {
+ $this->collectAccessors($container);
+ }
+
+ return $this->setterCache[$className];
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\ContainerInterface $container
+ */
+ private function collectAccessors(ContainerInterface $container)
+ {
+ $className = get_class($container);
+ $collectedGetters = [];
+ $collectedSetters = [];
+
+ foreach (get_class_methods($container) as $classMethod) {
+ if (preg_match('/^get([A-Z].*)$/', $classMethod, $matches)) {
+ $collectedGetters[] = $matches[1];
+ } elseif (preg_match('/^set([A-Z].*)$/', $classMethod, $matches)) {
+ $key = $matches[1];
+ $collectedSetters[] = $key;
+ $setterName = 'set' . $key;
+
+ try {
+ $method = new \ReflectionMethod($container, $setterName);
+ $params = $method->getParameters();
+ $setterClass = $params[0]->getClass();
+ $setterType = $setterClass ? $setterClass->getName() : null;
+
+ if (!$setterType) {
+ $doc = $method->getDocComment();
+
+ if (preg_match('/@param\s+([^\s]+)\s+\$' . $params[0]->getName() . '/', $doc, $matches)) {
+ $setterType = $matches[1];
+
+ if (strpos($setterType, '\\') === false) {
+ $class = new \ReflectionClass($container);
+ $namespace = $class->getNamespaceName();
+ $setterType = ltrim(implode('\\', [trim($namespace, '\\'), $setterType]), '\\');
+
+ if (!class_exists(rtrim($setterType, '[]'))) {
+ $setterType = null;
+ }
+ }
+ }
+ }
+ } catch (\ReflectionException $e) {
+ $setterType = null;
+ }
+
+ $this->setterTypeCache[$className][$key] = $setterType;
+ }
+ }
+
+ $this->getterCache[$className] = $collectedGetters;
+ $this->setterCache[$className] = $collectedSetters;
+ }
+
+ /**
+ * Iterate an array and convert its elements
+ *
+ * @param array $data
+ * @param bool|string $setterType
+ * @return array
+ */
+ private function iterateArray($data, $setterType = false)
+ {
+ foreach ($data as $key => $value) {
+ if ($this->checkIfArrayWithNumericKeys($value)) {
+ // If the value a numeric key array, iterate again each element of it
+ $value = $this->iterateArray($value, $setterType);
+ } elseif ($setterType === false && ($value instanceof ContainerInterface)) {
+ // If value is a container, convert it to array
+ $value = $this->toArray($value);
+ } elseif (is_array($value)) {
+ // If value is an associative array, wrap it into container
+ $className = rtrim($setterType, '[]');
+ $container = $this->fromArray($value, $className);
+ $value = $container;
+ }
+
+ $data[$key] = $value;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Check if the argument is an array with numeric keys starting with 0
+ *
+ * @param array $item
+ * @return bool
+ */
+ private function checkIfArrayWithNumericKeys($item)
+ {
+ return is_array($item) && isset($item[0]);
+ }
+}
diff --git a/Model/Exception/FailToLockException.php b/Model/Exception/FailToLockException.php
new file mode 100644
index 0000000..0159c39
--- /dev/null
+++ b/Model/Exception/FailToLockException.php
@@ -0,0 +1,17 @@
+config = $config;
+ $this->checkoutSession = $checkoutSession;
+ $this->priceCurrency = $priceCurrency;
+ $this->catalogHelper = $catalogHelper;
+ $this->storeManager = $storeManager;
+ $this->customerSession = $customerSession;
+ $this->taxClassKeyFactory = $taxClassKeyFactory;
+ $this->quoteDetailsFactory = $quoteDetailsFactory;
+ $this->quoteDetailsItemFactory = $quoteDetailsItemFactory;
+ $this->taxCalculation = $taxCalculation;
+ $this->addressFactory = $addressFactory;
+ $this->regionFactory = $regionFactory;
+ $this->customerGroupRepository = $customerGroupRepository;
+ }
+
+ /**
+ * Sets the fee including Tax, quote should be recalculated after this, to update all remaining fields
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @param float $fee
+ */
+ public function setQlirooneFeeInclTax(\Magento\Quote\Model\Quote $quote, $fee)
+ {
+ if ($quote->isVirtual()) {
+ $quote->getBillingAddress()->setQlirooneFee($fee);
+ } else {
+ $quote->getShippingAddress()->setQlirooneFee($fee);
+ }
+ }
+
+ /**
+ * Returns the amount of the fee, if defined. It can be fixed or a percent of the order sum
+ * This function must not depend on display settings
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return float|int
+ */
+ public function getQlirooneFeeInclTax(\Magento\Quote\Model\Quote $quote)
+ {
+ if ($quote->isVirtual()) {
+ $fee = $quote->getBillingAddress()->getQlirooneFee();
+ } else {
+ $fee = $quote->getShippingAddress()->getQlirooneFee();
+ }
+ $price = $this->getCalcTaxPrice($quote, $fee, 1);
+
+ return $price;
+ }
+
+ /**
+ * Return Fee exluding tax
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return float|int
+ */
+ public function getQlirooneFeeExclTax(\Magento\Quote\Model\Quote $quote)
+ {
+ $price = $this->getQlirooneFeeInclTax($quote);
+ $price = $this->getCalcTaxPrice($quote, $price, 0);
+
+ return $price;
+ }
+
+ /**
+ * @todo Improvement. Proper currency conversion to handle display currencies
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return float|int
+ */
+ public function getBaseQlirooneFeeInclTax(\Magento\Quote\Model\Quote $quote)
+ {
+ return $this->getQlirooneFeeInclTax($quote);
+ }
+
+ /**
+ * @todo Improvement. Proper currency conversion to handle display currencies
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return float|int
+ */
+ public function getBaseQlirooneFeeExclTax(\Magento\Quote\Model\Quote $quote)
+ {
+ return $this->getQlirooneFeeExclTax($quote);
+ }
+
+ /**
+ * Get the summary for cart and checkout
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @param float $amount
+ * @return array|null
+ */
+ public function getFeeArray($quote, $amount)
+ {
+ $feeSetup = $this->getFeeSetup($quote->getStoreId());
+ if (!$amount) {
+// Ideally, returning null should block the fee from appearing, but it doesn't
+// return null;
+ }
+ $result = [
+ 'code' => Config::TOTALS_FEE_CODE,
+ 'title' => __($feeSetup[Config::CONFIG_FEE_TITLE]),
+ 'value' => $amount,
+ ];
+ return $result;
+ }
+
+ /**
+ * Get the object, used for Totals in both FE and BE on orders, creditnotes and invoices
+ *
+ * @param int $storeId
+ * @param float $amount
+ * @return \Magento\Framework\DataObject|null
+ */
+ public function getFeeObject($storeId, $amount)
+ {
+ $feeSetup = $this->getFeeSetup($storeId);
+ if (!$amount) {
+ return null;
+ }
+ if ($feeSetup) {
+ $title = __($feeSetup[Config::CONFIG_FEE_TITLE]);
+ } else {
+ $title = __('Payment fee');
+ }
+ $result = new \Magento\Framework\DataObject([
+ 'code' => Config::TOTALS_FEE_CODE,
+ 'strong' => false,
+ 'value' => $amount,
+ 'label' => $title,
+ ]);
+ return $result;
+ }
+
+ /**
+ * Will return fee setup, including an amount of zero
+ *
+ * @param int $storeId
+ * @return array
+ */
+ public function getFeeSetup($storeId)
+ {
+ if (!$this->config->isActive($storeId)) {
+ return null;
+ }
+ if (!$this->methodsWithFee) {
+ $title = $this->config->getFeeMerchantReference();
+ $this->methodsWithFee = array(
+ Config::CONFIG_FEE_AMOUNT => 0,
+ Config::CONFIG_FEE_TITLE => $title,
+ );
+ }
+ return $this->methodsWithFee;
+ }
+
+ /**
+ * Picks up the amounts from Fees and runs them through the getTaxPrice function,
+ * which changes things depending on display settings etc
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @param array $feeCalc
+ * @return array
+ */
+ public function applyDisplayFlagsToFeeArray($quote, $feeCalc)
+ {
+ if ($feeCalc[Config::CONFIG_FEE_AMOUNT]) {
+ $price = $feeCalc[Config::CONFIG_FEE_AMOUNT];
+ $feeCalc[Config::CONFIG_FEE_AMOUNT] = $this->getTaxPrice($quote, $price);
+ }
+
+ return $feeCalc;
+ }
+
+ /**
+ * Get current quote from checkout session
+ *
+ * @return \Magento\Quote\Model\Quote
+ */
+ public function getQuote()
+ {
+ return $this->checkoutSession->getQuote();
+ }
+
+ /**
+ * Returns the price including or excluding tax, depending on flags being sent in and display settings
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @param float $price
+ * @param bool|null $includingTax
+ * @param bool|null $feeIncludesTax
+ * @return float
+ */
+ private function getTaxPrice($quote, $price, $includingTax = null, $feeIncludesTax = null)
+ {
+ $pseudoProduct = new \Magento\Framework\DataObject();
+ $pseudoProduct->setTaxClassId(
+ $this->config->getFeeTaxClass($quote->getStoreId())
+ );
+
+ $shippingAddress = null;
+ $billingAddress = null;
+ $ctc = null;
+
+ if ($feeIncludesTax === null) {
+ $feeIncludesTax = $this->config->paymentFeeIncludesTax($quote->getStoreId());
+ }
+
+ $price = $this->catalogHelper->getTaxPrice(
+ $pseudoProduct,
+ $price,
+ $includingTax,
+ $shippingAddress,
+ $billingAddress,
+ $ctc,
+ $quote->getStoreId(),
+ $feeIncludesTax
+ );
+
+ return $price;
+ }
+
+ /**
+ * Returns the price including or excluding tax, NOT depending on display settings
+ * Basically a copy of above used function $this->catalogHelper->getTaxPrice
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @param float $price
+ * @param bool $includingTax
+ * @param bool|null $feeIncludesTax
+ * @return float
+ */
+ private function getCalcTaxPrice($quote, $price, $includingTax, $feeIncludesTax = null)
+ {
+ if (!$price) {
+ return $price;
+ }
+
+ $product = new \Magento\Framework\DataObject();
+ $product->setTaxClassId(
+ $this->config->getFeeTaxClass($quote->getStoreId())
+ );
+
+ $shippingAddress = null;
+ $billingAddress = null;
+ $ctc = null;
+ $roundPrice = true;
+
+ $store = $this->storeManager->getStore($quote->getStoreId());
+ if ($feeIncludesTax === null) {
+ $feeIncludesTax = $this->config->paymentFeeIncludesTax($quote->getStoreId());
+ }
+
+ $shippingAddressDataObject = null;
+ if ($shippingAddress === null) {
+ $shippingAddressDataObject =
+ $this->convertDefaultTaxAddress($this->customerSession->getDefaultTaxShippingAddress());
+ } elseif ($shippingAddress instanceof \Magento\Customer\Model\Address\AbstractAddress) {
+ $shippingAddressDataObject = $shippingAddress->getDataModel();
+ }
+
+ $billingAddressDataObject = null;
+ if ($billingAddress === null) {
+ $billingAddressDataObject =
+ $this->convertDefaultTaxAddress($this->customerSession->getDefaultTaxBillingAddress());
+ } elseif ($billingAddress instanceof \Magento\Customer\Model\Address\AbstractAddress) {
+ $billingAddressDataObject = $billingAddress->getDataModel();
+ }
+
+ $taxClassKey = $this->taxClassKeyFactory->create();
+ $taxClassKey->setType(TaxClassKeyInterface::TYPE_ID)
+ ->setValue($product->getTaxClassId());
+
+ if ($ctc === null && $this->customerSession->getCustomerGroupId() != null) {
+ $ctc = $this->customerGroupRepository->getById($this->customerSession->getCustomerGroupId())
+ ->getTaxClassId();
+ }
+
+ $customerTaxClassKey = $this->taxClassKeyFactory->create();
+ $customerTaxClassKey->setType(TaxClassKeyInterface::TYPE_ID)
+ ->setValue($ctc);
+
+ $item = $this->quoteDetailsItemFactory->create();
+ $item->setQuantity(1)
+ ->setCode($product->getSku())
+ ->setShortDescription($product->getShortDescription())
+ ->setTaxClassKey($taxClassKey)
+ ->setIsTaxIncluded($feeIncludesTax)
+ ->setType('product')
+ ->setUnitPrice($price);
+
+ $quoteDetails = $this->quoteDetailsFactory->create();
+ $quoteDetails->setShippingAddress($shippingAddressDataObject)
+ ->setBillingAddress($billingAddressDataObject)
+ ->setCustomerTaxClassKey($customerTaxClassKey)
+ ->setItems([$item])
+ ->setCustomerId($this->customerSession->getCustomerId());
+
+ $storeId = null;
+ if ($store) {
+ $storeId = $store->getId();
+ }
+ $taxDetails = $this->taxCalculation->calculateTax($quoteDetails, $storeId, $roundPrice);
+ $items = $taxDetails->getItems();
+ $taxDetailsItem = array_shift($items);
+
+ if ($includingTax) {
+ $price = $taxDetailsItem->getPriceInclTax();
+ } else {
+ $price = $taxDetailsItem->getPrice();
+ }
+
+ if ($roundPrice) {
+ return $this->priceCurrency->round($price);
+ } else {
+ return $price;
+ }
+ }
+
+ /**
+ * @param array $taxAddress
+ * @return \Magento\Customer\Api\Data\AddressInterface|null
+ */
+ private function convertDefaultTaxAddress(array $taxAddress = null)
+ {
+ if (empty($taxAddress)) {
+ return null;
+ }
+ /** @var \Magento\Customer\Api\Data\AddressInterface $addressDataObject */
+ $addressDataObject = $this->addressFactory->create()
+ ->setCountryId($taxAddress['country_id'])
+ ->setPostcode($taxAddress['postcode']);
+
+ if (isset($taxAddress['region_id'])) {
+ $addressDataObject->setRegion($this->regionFactory->create()->setRegionId($taxAddress['region_id']));
+ }
+ return $addressDataObject;
+ }
+
+}
\ No newline at end of file
diff --git a/Model/GeoIp/DefaultResolver.php b/Model/GeoIp/DefaultResolver.php
new file mode 100644
index 0000000..604bbf3
--- /dev/null
+++ b/Model/GeoIp/DefaultResolver.php
@@ -0,0 +1,30 @@
+ 'sv-se',
+ 'en_US' => 'en-us',
+ 'fi_FI' => 'fi-fi',
+ 'da_DK' => 'da-dk',
+ 'de_DE' => 'de-de',
+ 'nb_NO' => 'nb-no',
+ 'nn_NO' => 'nb-no',
+ ];
+
+ /**
+ * @var \Magento\Framework\Locale\Resolver
+ */
+ private $localeResolver;
+
+ /**
+ * Inject dependencies
+ *
+ * @param \Magento\Framework\Locale\Resolver $localeResolver
+ */
+ public function __construct(
+ Resolver $localeResolver
+ ) {
+ $this->localeResolver = $localeResolver;
+ }
+
+ /**
+ * Get a prepared string that contains a QliroOne compatible language
+ *
+ * @return string
+ */
+ public function getLanguage()
+ {
+ $locale = $this->localeResolver->getLocale();
+
+ return $this->languageMap[$locale] ?? 'en-us';
+ }
+}
diff --git a/Model/Link.php b/Model/Link.php
new file mode 100644
index 0000000..dff53e2
--- /dev/null
+++ b/Model/Link.php
@@ -0,0 +1,243 @@
+_init(ResourceModel\Link::class);
+ }
+
+ /**
+ * @return string
+ */
+ /**
+ * @inheritdoc
+ */
+ public function getLinkId()
+ {
+ return $this->getData(self::FIELD_ID);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getIsActive()
+ {
+ return (bool)$this->getData(self::FIELD_IS_ACTIVE);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getReference()
+ {
+ return $this->getData(self::FIELD_REFERENCE);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQuoteId()
+ {
+ return $this->getData(self::FIELD_QUOTE_ID);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQliroOrderId()
+ {
+ return $this->getData(self::FIELD_QLIRO_ORDER_ID);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQliroOrderStatus()
+ {
+ return $this->getData(self::FIELD_QLIRO_ORDER_STATUS);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getOrderId()
+ {
+ return $this->getData(self::FIELD_ORDER_ID);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getRemoteIp()
+ {
+ return $this->getData(self::FIELD_REMOTE_IP);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getCreatedAt()
+ {
+ return $this->getData(self::FIELD_CREATED_AT);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getUpdatedAt()
+ {
+ return $this->getData(self::FIELD_UPDATED_AT);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getPlacedAt()
+ {
+ return $this->getData(self::FIELD_PLACED_AT);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQuoteSnapshot() {
+
+ return $this->getData(self::FIELD_QUOTE_SNAPSHOT);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMessage()
+ {
+ return $this->getData(self::FIELD_MESSAGE);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getUnifaunShippingAmount()
+ {
+ return $this->getData(self::FIELD_UNIFAUN_SHIPPING_AMOUNT);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setIsActive($value)
+ {
+ return $this->setData(self::FIELD_IS_ACTIVE, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setReference($value)
+ {
+ return $this->setData(self::FIELD_REFERENCE, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setQuoteId($value)
+ {
+ return $this->setData(self::FIELD_QUOTE_ID, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setQliroOrderId($value)
+ {
+ return $this->setData(self::FIELD_QLIRO_ORDER_ID, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setQliroOrderStatus($value)
+ {
+ return $this->setData(self::FIELD_QLIRO_ORDER_STATUS, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setOrderId($value)
+ {
+ return $this->setData(self::FIELD_ORDER_ID, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setRemoteIp($value)
+ {
+ return $this->setData(self::FIELD_REMOTE_IP, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setCreatedAt($value)
+ {
+ return $this->setData(self::FIELD_CREATED_AT, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setUpdatedAt($value)
+ {
+ return $this->setData(self::FIELD_UPDATED_AT, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setPlacedAt($value)
+ {
+ return $this->setData(self::FIELD_PLACED_AT, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setQuoteSnapshot($value)
+ {
+ return $this->setData(self::FIELD_QUOTE_SNAPSHOT, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setMessage($value)
+ {
+ return $this->setData(self::FIELD_MESSAGE, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setUnifaunShippingAmount($value)
+ {
+ return $this->setData(self::FIELD_UNIFAUN_SHIPPING_AMOUNT, $value);
+ }
+}
diff --git a/Model/Link/Repository.php b/Model/Link/Repository.php
new file mode 100644
index 0000000..3aa9ac0
--- /dev/null
+++ b/Model/Link/Repository.php
@@ -0,0 +1,256 @@
+linkResourceModel = $linkResourceModel;
+ $this->linkFactory = $linkFactory;
+ $this->searchResultFactory = $searchResultFactory;
+ $this->collectionFactory = $collectionFactory;
+ }
+
+ /**
+ * Save a link
+ *
+ * @param \Qliro\QliroOne\Api\Data\LinkInterface $link
+ * @return \Qliro\QliroOne\Api\Data\LinkInterface
+ * @throws \Magento\Framework\Exception\AlreadyExistsException
+ */
+ public function save(LinkInterface $link)
+ {
+ $this->linkResourceModel->save($link);
+
+ return $link;
+ }
+
+ /**
+ * Get a link by its ID
+ *
+ * @inheritdoc
+ */
+ public function get($id, $onlyActive = true)
+ {
+ return $this->getByField($id, null, $onlyActive);
+ }
+
+ /**
+ * Get a link by Magento quote ID
+ *
+ * @inheritdoc
+ */
+ public function getByQuoteId($quoteId, $onlyActive = true)
+ {
+ return $this->getByField($quoteId, Link::FIELD_QUOTE_ID, $onlyActive);
+ }
+
+ /**
+ * Get a link by Magento order ID
+ *
+ * @inheritdoc
+ */
+ public function getByOrderId($orderId, $onlyActive = true)
+ {
+ return $this->getByField($orderId, Link::FIELD_ORDER_ID, $onlyActive);
+ }
+
+ /**
+ * Get a link by Qliro order ID
+ *
+ * @inheritdoc
+ */
+ public function getByQliroOrderId($qliroOrderId, $onlyActive = true)
+ {
+ return $this->getByField($qliroOrderId, Link::FIELD_QLIRO_ORDER_ID, $onlyActive);
+ }
+
+ /**
+ * Get a link by Magento order ID
+ *
+ * @inheritdoc
+ */
+ public function getByReference($value, $onlyActive = true)
+ {
+ return $this->getByField($value, Link::FIELD_REFERENCE, $onlyActive);
+ }
+
+ /**
+ * Delete a link
+ *
+ * @param \Qliro\QliroOne\Api\Data\LinkInterface $link
+ * @return \Qliro\QliroOne\Model\ResourceModel\Link
+ * @throws \Exception
+ */
+ public function delete(LinkInterface $link)
+ {
+ return $this->linkResourceModel->delete($link);
+ }
+
+ /**
+ * Get a result of search among links by given search criteria
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @return \Qliro\QliroOne\Api\LinkSearchResultInterface
+ */
+ public function getList(SearchCriteriaInterface $searchCriteria)
+ {
+ /** @var \Qliro\QliroOne\Model\ResourceModel\Link\Collection $collection */
+ $collection = $this->collectionFactory->create();
+
+ $this->addFiltersToCollection($searchCriteria, $collection);
+ $this->addSortOrdersToCollection($searchCriteria, $collection);
+ $this->addPaginationToCollection($searchCriteria, $collection);
+
+ $collection->load();
+
+ return $this->buildSearchResult($searchCriteria, $collection);
+ }
+
+ /**
+ * Add filters to collection
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\Link\Collection $collection
+ */
+ private function addFiltersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
+ $fields = $conditions = [];
+ foreach ($filterGroup->getFilters() as $filter) {
+ $fields[] = $filter->getField();
+ $conditions[] = [$filter->getConditionType() => $filter->getValue()];
+ }
+ $collection->addFieldToFilter($fields, $conditions);
+ }
+ }
+
+ /**
+ * Add sort order to collection
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\Link\Collection $collection
+ */
+ private function addSortOrdersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ foreach ((array) $searchCriteria->getSortOrders() as $sortOrder) {
+ $direction = $sortOrder->getDirection() == SortOrder::SORT_ASC ? 'asc' : 'desc';
+ $collection->addOrder($sortOrder->getField(), $direction);
+ }
+ }
+
+ /**
+ * Add pagination to collection
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\Link\Collection $collection
+ */
+ private function addPaginationToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ $collection->setPageSize($searchCriteria->getPageSize());
+ $collection->setCurPage($searchCriteria->getCurrentPage());
+ }
+
+ /**
+ * Build search result
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\Link\Collection $collection
+ * @return \Qliro\QliroOne\Api\LinkSearchResultInterface
+ */
+ private function buildSearchResult(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ /** @var \Qliro\QliroOne\Api\LinkSearchResultInterface $searchResults */
+ $searchResults = $this->searchResultFactory->create();
+
+ $searchResults->setSearchCriteria($searchCriteria);
+ $searchResults->setItems($collection->getItems());
+ $searchResults->setTotalCount($collection->getSize());
+
+ return $searchResults;
+ }
+
+ /**
+ * Get a link by a specified field
+ *
+ * @param string|int $value
+ * @param string $field
+ * @param bool $onlyActive
+ * @return \Qliro\QliroOne\Model\Link
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function getByField($value, $field, $onlyActive = true)
+ {
+ /** @var \Qliro\QliroOne\Model\Link $link */
+ if ($onlyActive) {
+ $collection = $this->collectionFactory->create()
+ ->addFieldToFilter($field, $value)
+ ->addFieldToFilter(Link::FIELD_IS_ACTIVE, 1);
+ $link = $collection->getFirstItem();
+ } else {
+ $link = $this->linkFactory->create();
+ $this->linkResourceModel->load($link, $value, $field);
+ }
+
+ if (!$link->getId()) {
+ throw new NoSuchEntityException(__('Cannot find a link with %1 = "%2"', $field, $value));
+ }
+
+ return $link;
+ }
+}
diff --git a/Model/Link/SearchResult.php b/Model/Link/SearchResult.php
new file mode 100644
index 0000000..06c1c1a
--- /dev/null
+++ b/Model/Link/SearchResult.php
@@ -0,0 +1,20 @@
+_init(ResourceModel\LogRecord::class);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDate()
+ {
+ return $this->getData(self::FIELD_DATE);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMessage()
+ {
+ return $this->getData(self::FIELD_MESSAGE);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getExtra()
+ {
+ return $this->getData(self::FIELD_EXTRA);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getLevel()
+ {
+ return $this->getData(self::FIELD_LEVEL);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getTag()
+ {
+ return $this->getData(self::FIELD_TAGS);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getProcessId()
+ {
+ return $this->getData(self::FIELD_PROCESS_ID);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setDate($value)
+ {
+ return $this->setData(self::FIELD_DATE, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setMessage($value)
+ {
+ return $this->setData(self::FIELD_MESSAGE, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setLevel($value)
+ {
+ return $this->setData(self::FIELD_LEVEL, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setTag($value)
+ {
+ return $this->setData(self::FIELD_TAGS, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setProcessId($value)
+ {
+ return $this->setData(self::FIELD_PROCESS_ID, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setExtra($value)
+ {
+ return $this->setData(self::FIELD_EXTRA, $value);
+ }
+}
diff --git a/Model/Logger/ConnectionProvider.php b/Model/Logger/ConnectionProvider.php
new file mode 100644
index 0000000..4c22722
--- /dev/null
+++ b/Model/Logger/ConnectionProvider.php
@@ -0,0 +1,72 @@
+connectionFactory = $connectionFactory;
+ $this->deploymentConfig = $deploymentConfig;
+ }
+
+ /**
+ * Get a log DB connection that uses same config as default connection, but is separate
+ *
+ * @return AdapterInterface
+ * @throws \DomainException
+ */
+ public function getConnection()
+ {
+ if (!$this->connection) {
+ $connectionName = ResourceConnection::DEFAULT_CONNECTION;
+
+ $connectionConfig = $this->deploymentConfig->get(
+ ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/' . $connectionName
+ );
+
+ if ($connectionConfig) {
+ $this->connection = $this->connectionFactory->create($connectionConfig);
+ } else {
+ throw new \DomainException("Connection '$connectionName' is not defined");
+ }
+
+ }
+
+ return $this->connection;
+ }
+}
\ No newline at end of file
diff --git a/Model/Logger/Formatter.php b/Model/Logger/Formatter.php
new file mode 100644
index 0000000..4ead977
--- /dev/null
+++ b/Model/Logger/Formatter.php
@@ -0,0 +1,121 @@
+maxNestingLevel = max($maxNestingLevel, 0);
+ $this->exceptionTraceAsString = (bool)$exceptionTraceAsString;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function format(array $record)
+ {
+ return $this->formatArray($record);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function formatBatch(array $records)
+ {
+ foreach ($records as $key => $record) {
+ $records[$key] = $this->format($record);
+ }
+
+ return $records;
+ }
+
+ /**
+ * @param array $record
+ * @param int $nestingLevel
+ * @return array|string
+ */
+ private function formatArray(array $record, $nestingLevel = 0)
+ {
+ if ($this->maxNestingLevel == 0 || $nestingLevel <= $this->maxNestingLevel) {
+ foreach ($record as $name => $value) {
+ if ($value instanceof \DateTime) {
+ $record[$name] = $this->formatDate($value, $nestingLevel + 1);
+ } elseif ($value instanceof \Exception) {
+ $record[$name] = $this->formatException($value, $nestingLevel + 1);
+ } elseif (is_array($value)) {
+ $record[$name] = $this->formatArray($value, $nestingLevel + 1);
+ } elseif (is_object($value)) {
+ $record[$name] = $this->formatObject($value, $nestingLevel + 1);
+ }
+ }
+ } else {
+ $record = '[...]';
+ }
+
+ return $record;
+ }
+
+ /**
+ * @param object $value
+ * @param int $nestingLevel
+ * @return array|string
+ */
+ private function formatObject($value, $nestingLevel)
+ {
+ $objectVars = get_object_vars($value);
+ $objectVars['class'] = get_class($value);
+
+ return $this->formatArray($objectVars, $nestingLevel);
+ }
+
+ /**
+ * @param \Exception $exception
+ * @param int $nestingLevel
+ * @return array|string
+ */
+ private function formatException(\Exception $exception, $nestingLevel)
+ {
+ $formattedException = [
+ 'class' => get_class($exception),
+ 'message' => $exception->getMessage(),
+ 'code' => $exception->getCode(),
+ 'file' => $exception->getFile() . ':' . $exception->getLine(),
+ ];
+
+ if ($this->exceptionTraceAsString === true) {
+ $formattedException['trace'] = $exception->getTraceAsString();
+ } else {
+ $formattedException['trace'] = $exception->getTrace();
+ }
+
+ return $this->formatArray($formattedException, $nestingLevel);
+ }
+
+ /**
+ * @param \DateTime $value
+ * @param int $nestingLevel
+ * @return string
+ */
+ private function formatDate(\DateTime $value, $nestingLevel)
+ {
+ return $value->format('Y-m-d H:i:s.u');
+ }
+}
diff --git a/Model/Logger/Handler.php b/Model/Logger/Handler.php
new file mode 100644
index 0000000..cdde2d0
--- /dev/null
+++ b/Model/Logger/Handler.php
@@ -0,0 +1,129 @@
+formatter = $formatter;
+ $this->config = $config;
+
+ parent::__construct();
+ $this->connectionProvider = $connectionProvider;
+ }
+
+ /**
+ * Make the level be dynamically aware of the configured log level
+ *
+ * @param array $record
+ * @return bool
+ */
+ public function isHandling(array $record)
+ {
+ return $record['level'] >= $this->getLevel();
+
+ }
+
+ /**
+ * Make the level be dynamically aware of the configured log level
+ *
+ * @return int
+ */
+ public function getLevel()
+ {
+ $this->level = Logger::toMonologLevel($this->config->getLoggingLevel());
+
+ return $this->level;
+ }
+
+ /**
+ * @param array $record
+ * @throws \DomainException
+ */
+ protected function write(array $record)
+ {
+ $context = $record['context'];
+ $record = $record['formatted'];
+
+ $mark = $context['mark'] ?? null;
+ $message = ($mark ? sprintf('%s: ', strtoupper($mark)) : null) . $record['message'];
+
+ $connection = $this->connectionProvider->getConnection();
+ $connection->insert(
+ $connection->getTableName(LogRecord::TABLE_LOG),
+ [
+ LogRecordInterface::FIELD_DATE => $record['datetime'],
+ LogRecordInterface::FIELD_LEVEL => $record['level_name'],
+ LogRecordInterface::FIELD_MESSAGE => $message,
+ LogRecordInterface::FIELD_REFERENCE => $context['reference'] ?? '',
+ LogRecordInterface::FIELD_TAGS => $context['tags'] ?? '',
+ LogRecordInterface::FIELD_PROCESS_ID => $context['process_id'] ?? '',
+ LogRecordInterface::FIELD_EXTRA => $this->encodeExtra($context['extra'] ?? ''),
+ ]
+ );
+ }
+
+ /**
+ * @param array|string $data
+ * @return string
+ */
+ private function encodeExtra($data)
+ {
+ try {
+ $serializedData = is_array($data) ? $this->serialize($data) : $data;
+ } catch (\Exception $exception) {
+ $serializedData = null;
+ }
+
+ return $serializedData;
+ }
+
+ /**
+ * Serialize JSON using pretty print and some other options
+ *
+ * @param array $data
+ * @return false|string
+ */
+ private function serialize($data)
+ {
+ return \json_encode($data, JSON_PRETTY_PRINT + JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE);
+ }
+}
diff --git a/Model/Logger/Manager.php b/Model/Logger/Manager.php
new file mode 100644
index 0000000..67dd6b5
--- /dev/null
+++ b/Model/Logger/Manager.php
@@ -0,0 +1,329 @@
+psrLogger = $psrLogger;
+ $this->logResource = $logResource;
+ $this->linkRepository = $linkRepository;
+ }
+
+ /**
+ * System is unusable.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function emergency($message, array $context = [])
+ {
+ $this->psrLogger->emergency($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Action must be taken immediately.
+ * Example: Entire website down, database unavailable, etc. This should
+ * trigger the SMS alerts and wake you up.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function alert($message, array $context = [])
+ {
+ $this->psrLogger->alert($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Critical conditions.
+ * Example: Application component unavailable, unexpected exception.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function critical($message, array $context = [])
+ {
+ $this->psrLogger->critical($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Runtime errors that do not require immediate action but should typically
+ * be logged and monitored.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function error($message, array $context = [])
+ {
+ $this->psrLogger->error($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Exceptional occurrences that are not errors.
+ * Example: Use of deprecated APIs, poor use of an API, undesirable things
+ * that are not necessarily wrong.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function warning($message, array $context = [])
+ {
+ $this->psrLogger->warning($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Normal but significant events.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function notice($message, array $context = [])
+ {
+ $this->psrLogger->notice($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Interesting events.
+ * Example: User logs in, SQL logs.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function info($message, array $context = [])
+ {
+ $this->psrLogger->info($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Detailed debug information.
+ *
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function debug($message, array $context = [])
+ {
+ $this->psrLogger->debug($message, $this->prepareContext($context));
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param mixed $level
+ * @param string $message
+ * @param array $context
+ * @return void
+ */
+ public function log($level, $message, array $context = [])
+ {
+ $this->psrLogger->log($level, $message, $this->prepareContext($context));
+ }
+
+ /**
+ * As soon as possible place a tag in the logger to ensure that all log lines can be linked to a checkout session.
+ * The tag should be the merchant reference.
+ * We will back-patch the log with the new mercant reference
+ *
+ * @param string $value
+ * @return $this
+ */
+ public function setMerchantReference($value)
+ {
+ $this->merchantReference = $value;
+ if (!empty($value)) {
+ $this->logResource->patchMerchantReference($value);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return $this
+ */
+ public function setMerchantReferenceFromQuote($quote)
+ {
+ if ($quote) {
+ try {
+ $quoteId = $quote->getEntityId();
+ $link = $this->linkRepository->getByQuoteId($quoteId);
+ $this->setMerchantReference($link->getReference());
+ } catch (\Exception $exception) {
+ // Do nothing
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Add a tag to any futher logging context
+ *
+ * @param string $tag
+ */
+ public function addTag($tag)
+ {
+ $this->tags[] = $tag;
+ $this->tags = array_unique($this->tags);
+ }
+
+ /**
+ * remove a tag from any futher logging context
+ *
+ * @param string $tag
+ */
+ public function removeTag($tag)
+ {
+ $this->tags = array_diff($this->tags, [$tag]);
+ }
+
+ /**
+ * Clear all tags from any further logging context
+ */
+ public function clearTags()
+ {
+ $this->tags = [];
+ }
+
+ /**
+ * Set message mark.
+ * In fact, by setting a mark it pushes previously set mark to the stack.
+ * When mark is then set to null, the previously set mark is restored from the stack.
+ * It allows to set marks in the folded functions, allowing to restore logger context when exiting the function.
+ *
+ * @param string $mark
+ */
+ public function setMark($mark)
+ {
+ if ($mark) {
+ array_unshift($this->marks, $mark);
+ } else {
+ array_shift($this->marks);
+ }
+ }
+
+ /**
+ * @param int $levels
+ * @return string
+ */
+ public function getStack($levels = 5)
+ {
+ $exception = new \Exception;
+ $stack = '';
+ $skip = strpos($exception->getFile(), 'module-qliroone/') + 16;
+ foreach (array_slice($exception->getTrace(), 1, $levels) as $one) {
+ $stack .= sprintf('|%s:%s', substr($one['file'], $skip), $one['line']);
+ }
+
+ return substr($stack, 1);
+ }
+ /**
+ * @param array $context
+ * @return array
+ */
+ private function prepareContext($context)
+ {
+ if (!empty($this->merchantReference)) {
+ $context['reference'] = $this->merchantReference;
+ }
+
+ $contextTags = $this->unpackTags($context['tags'] ?? '');
+ $context['tags'] = $this->packTags(array_unique(array_merge($contextTags, $this->tags)));
+
+ if (!empty($this->marks)) {
+ $context['mark'] = $this->marks[0];
+ }
+
+ $context['process_id'] = \getmypid();
+
+ return $context;
+ }
+
+ /**
+ * @param array $tagsData
+ * @return string
+ */
+ private function packTags($tagsData)
+ {
+ if (is_string($tagsData)) {
+ $tagsData = $this->unpackTags($tagsData);
+ }
+
+ return is_array($tagsData) && !empty($tagsData)
+ ? trim(implode(',', array_map('trim', (array)$tagsData)), ',')
+ : '';
+ }
+
+ /**
+ * @param string $tagsString
+ * @return array
+ */
+ private function unpackTags($tagsString)
+ {
+ return $tagsString ? explode(',', $tagsString) : [];
+ }
+}
diff --git a/Model/Management.php b/Model/Management.php
new file mode 100644
index 0000000..ee9805e
--- /dev/null
+++ b/Model/Management.php
@@ -0,0 +1,289 @@
+adminManagement = $adminManagement;
+ $this->checkoutStatusManagement = $checkoutStatusManagement;
+ $this->htmlSnippetManagement = $htmlSnippetManagement;
+ $this->paymentManagement = $paymentManagement;
+ $this->placeOrderManagement = $placeOrderManagement;
+ $this->qliroOrderManagement = $qliroOrderManagement;
+ $this->quoteManagement = $quoteManagement;
+ $this->shippingMethodManagement = $shippingMethodManagement;
+ $this->transactionStatusManagement = $transactionStatusManagement;
+ }
+
+ /**
+ * Fetch a QliroOne order and return it as a container
+ *
+ * @param bool $allowRecreate
+ * @return QliroOrderInterface
+ * @throws \Magento\Framework\Exception\AlreadyExistsException
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function getQliroOrder($allowRecreate = true)
+ {
+ return $this->qliroOrderManagement->setQuote($this->getQuote())->get($allowRecreate);
+ }
+
+ /**
+ * Fetch an HTML snippet from QliroOne order
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return string
+ */
+ public function getHtmlSnippet()
+ {
+ return $this->htmlSnippetManagement->setQuote($this->getQuote())->get();
+ }
+
+ /**
+ * Update quote with received data in the container and return a list of available shipping methods
+ *
+ * @param UpdateShippingMethodsNotificationInterface $updateContainer
+ * @return \Qliro\QliroOne\Api\Data\UpdateShippingMethodsResponseInterface
+ */
+ public function getShippingMethods(UpdateShippingMethodsNotificationInterface $updateContainer)
+ {
+ return $this->shippingMethodManagement->get($updateContainer);
+ }
+
+ /**
+ * Update quote with received data in the container and validate QliroOne order
+ *
+ * @param ValidateOrderNotificationInterface $validateContainer
+ * @return \Qliro\QliroOne\Api\Data\ValidateOrderResponseInterface
+ */
+ public function validateQliroOrder(ValidateOrderNotificationInterface $validateContainer)
+ {
+ return $this->qliroOrderManagement->validate($validateContainer);
+ }
+
+ /**
+ * Poll for Magento order placement and return order increment ID if successful
+ *
+ * @return Order
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function pollPlaceOrder()
+ {
+ return $this->placeOrderManagement->setQuote($this->getQuote())->poll();
+ }
+
+ /**
+ * @param CheckoutStatusInterface $checkoutStatus
+ * @return \Qliro\QliroOne\Api\Data\CheckoutStatusResponseInterface
+ */
+ public function checkoutStatus(CheckoutStatusInterface $checkoutStatus)
+ {
+ return $this->checkoutStatusManagement->update($checkoutStatus);
+ }
+
+ /**
+ * 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 QliroOrderInterface $qliroOrder
+ * @param string $state
+ * @return Order
+ * @throws \Qliro\QliroOne\Model\Exception\FailToLockException
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function placeOrder(QliroOrderInterface $qliroOrder, $state = Order::STATE_PENDING_PAYMENT)
+ {
+ return $this->placeOrderManagement->setQuote($this->getQuote())->execute($qliroOrder, $state);
+ }
+
+ /**
+ * Update customer with data from QliroOne frontend callback
+ *
+ * @param array $customerData
+ * @throws \Exception
+ */
+ public function updateCustomer($customerData)
+ {
+ $this->quoteManagement->setQuote($this->getQuote())->updateCustomer($customerData);
+ }
+
+ /**
+ * Update selected shipping method in quote
+ * Return true in case shipping method was set, or false if the quote is virtual or method was not changed
+ *
+ * @param string $code
+ * @param string|null $secondaryOption
+ * @param float|null $price
+ * @return bool
+ * @throws \Exception
+ */
+ public function updateShippingMethod($code, $secondaryOption = null, $price = null)
+ {
+ return $this->shippingMethodManagement->setQuote($this->getQuote())->update($code, $secondaryOption, $price);
+ }
+
+ /**
+ * Update shipping price in quote
+ * Return true in case shipping price was set, or false if the quote is virtual or update didn't happen
+ *
+ * @param float|null $price
+ * @return bool
+ * @throws \Exception
+ */
+ public function updateShippingPrice($price)
+ {
+ return $this->quoteManagement->setQuote($this->getQuote())->updateShippingPrice($price);
+ }
+
+ /**
+ * Update selected shipping method in quote
+ * Return true in case shipping method was set, or false if the quote is virtual or method was not changed
+ *
+ * @param float $fee
+ * @return bool
+ * @throws \Exception
+ */
+ public function updateFee($fee)
+ {
+ return $this->quoteManagement->setQuote($this->getQuote())->updateFee($fee);
+ }
+
+ /**
+ * Cancel QliroOne order
+ *
+ * @param int $qliroOrderId
+ * @return \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function cancelQliroOrder($qliroOrderId)
+ {
+ return $this->qliroOrderManagement->cancel($qliroOrderId);
+ }
+
+ /**
+ * @param \Magento\Payment\Model\InfoInterface $payment
+ * @param float $amount
+ * @return void
+ * @throws \Exception
+ */
+ public function captureByInvoice($payment, $amount)
+ {
+ $this->paymentManagement->captureByInvoice($payment, $amount);
+ }
+
+ /**
+ * @param \Magento\Sales\Model\Order\Shipment $shipment
+ * @return void
+ * @throws \Exception
+ */
+ public function captureByShipment($shipment)
+ {
+ $this->paymentManagement->captureByShipment($shipment);
+ }
+
+ /**
+ * Handles Order Management Status Transaction notifications
+ *
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @return \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatusResponse
+ */
+ public function handleTransactionStatus($qliroOrderManagementStatus)
+ {
+ return $this->transactionStatusManagement->handle($qliroOrderManagementStatus);
+ }
+
+ /**
+ * Get Admin Qliro order after it was already placed
+ *
+ * @param int $qliroOrderId
+ * @return \Qliro\QliroOne\Api\Data\AdminOrderInterface
+ */
+ public function getAdminQliroOrder($qliroOrderId)
+ {
+ return $this->adminManagement->getQliroOrder($qliroOrderId);
+ }
+}
diff --git a/Model/Management/AbstractManagement.php b/Model/Management/AbstractManagement.php
new file mode 100644
index 0000000..235093b
--- /dev/null
+++ b/Model/Management/AbstractManagement.php
@@ -0,0 +1,57 @@
+quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Get quote from the Management class
+ *
+ * @return \Magento\Quote\Model\Quote
+ */
+ public function getQuote()
+ {
+ if (!($this->quote instanceof CartInterface)) {
+ throw new \LogicException('Quote must be set before it is fetched.');
+ }
+
+ return $this->quote;
+ }
+}
diff --git a/Model/Management/Admin.php b/Model/Management/Admin.php
new file mode 100644
index 0000000..e28671d
--- /dev/null
+++ b/Model/Management/Admin.php
@@ -0,0 +1,77 @@
+orderManagementApi = $orderManagementApi;
+ $this->linkRepository = $linkRepository;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Get Admin Qliro order after it was already placed
+ *
+ * @param int $qliroOrderId
+ * @return \Qliro\QliroOne\Api\Data\AdminOrderInterface
+ */
+ public function getQliroOrder($qliroOrderId)
+ {
+ $qliroOrder = null; // Placeholder, QliroOne order will never be returned as null
+
+ try {
+ $link = $this->linkRepository->getByQliroOrderId($qliroOrderId);
+ $this->logManager->setMerchantReference($link->getReference());
+ $qliroOrder = $this->orderManagementApi->getOrder($qliroOrderId);
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => isset($link) ? $link->getOrderId() : $qliroOrderId,
+ ],
+ ]
+ );
+ }
+
+ return $qliroOrder;
+ }
+}
diff --git a/Model/Management/CheckoutStatus.php b/Model/Management/CheckoutStatus.php
new file mode 100644
index 0000000..0a94099
--- /dev/null
+++ b/Model/Management/CheckoutStatus.php
@@ -0,0 +1,250 @@
+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);
+ }
+}
diff --git a/Model/Management/HtmlSnippet.php b/Model/Management/HtmlSnippet.php
new file mode 100644
index 0000000..8dec6aa
--- /dev/null
+++ b/Model/Management/HtmlSnippet.php
@@ -0,0 +1,46 @@
+qliroOrder = $qliroOrder;
+ }
+
+ /**
+ * Fetch an HTML snippet from QliroOne order
+ *
+ * @return string
+ */
+ public function get()
+ {
+ try {
+ return $this->qliroOrder->setQuote($this->getQuote())->get()->getOrderHtmlSnippet();
+ } catch (\Exception $exception) {
+ $openTag = '';
+ $closeTag = ' ';
+
+ return __('QliroOne Checkout has failed to load. Please try to %1reload page%2.', $openTag, $closeTag);
+ }
+ }
+}
diff --git a/Model/Management/Payment.php b/Model/Management/Payment.php
new file mode 100644
index 0000000..f023b1c
--- /dev/null
+++ b/Model/Management/Payment.php
@@ -0,0 +1,288 @@
+qliroConfig = $qliroConfig;
+ $this->orderManagementApi = $orderManagementApi;
+ $this->linkRepository = $linkRepository;
+ $this->logManager = $logManager;
+ $this->transactionBuilder = $transactionBuilder;
+ $this->orderRepository = $orderRepository;
+ $this->orderManagementStatusInterfaceFactory = $orderManagementStatusInterfaceFactory;
+ $this->orderManagementStatusRepository = $orderManagementStatusRepository;
+ $this->invoiceMarkItemsAsShippedRequestBuilder = $invoiceMarkItemsAsShippedRequestBuilder;
+ $this->shipmentMarkItemsAsShippedRequestBuilder = $shipmentMarkItemsAsShippedRequestBuilder;
+ }
+
+ /**
+ * Create payment transaction, which will hold and handle the Order Management features.
+ * This saves payment and transaction, possibly also the order.
+ *
+ * This should have been done differently, with authorization keyword in method etc...
+ *
+ * @param \Magento\Sales\Model\Order $order
+ * @param QliroOrderInterface $qliroOrder
+ * @param string $state
+ * @throws \Exception
+ */
+ public function createPaymentTransaction($order, $qliroOrder, $state = Order::STATE_PENDING_PAYMENT)
+ {
+ $this->logManager->setMark('PAYMENT TRANSACTION');
+
+ try {
+ /** @var \Magento\Sales\Model\Order\Payment $payment */
+ $payment = $order->getPayment();
+
+ $payment->setLastTransId($qliroOrder->getOrderId());
+ $transactionId = 'qliroone-' . $qliroOrder->getOrderId();
+ $payment->setTransactionId($transactionId);
+ $payment->setIsTransactionClosed(false);
+
+ $formattedPrice = $order->getBaseCurrency()->formatTxt(
+ $order->getGrandTotal()
+ );
+
+ $message = __('Qliro One authorized amount of %1.', $formattedPrice);
+
+ /** @var \Magento\Sales\Api\Data\TransactionInterface $transaction */
+ $transaction = $this->transactionBuilder->setPayment($payment)->setOrder($order)->setTransactionId(
+ $payment->getTransactionId()
+ )->build(\Magento\Sales\Api\Data\TransactionInterface::TYPE_AUTH);
+
+ $payment->addTransactionCommentsToOrder($transaction, $message);
+ $payment->setSkipOrderProcessing(true);
+ $payment->save();
+
+ if (empty($status)) {
+ if ($order->getState() != $state) {
+ $order->setState($state);
+ $this->orderRepository->save($order);
+ }
+ } else {
+ if ($order->getState() != $state || $order->getStatus() != $status) {
+ $order->setState($state)->setStatus($status);
+ $this->orderRepository->save($order);
+ }
+ }
+
+ $transaction->save();
+ } catch (\Exception $exception) {
+ throw $exception;
+ } finally {
+ $this->logManager->setMark(null);
+ }
+ }
+
+ /**
+ * @param \Magento\Payment\Model\InfoInterface $payment
+ * @param float $amount
+ * @return void
+ * @throws \Exception
+ */
+ public function captureByInvoice($payment, $amount)
+ {
+ if ($payment->getData(self::QLIRO_SKIP_ACTUAL_CAPTURE)) {
+ return;
+ }
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $payment->getOrder();
+ $link = $this->linkRepository->getByOrderId($order->getId());
+ $this->logManager->setMerchantReference($link->getReference());
+
+ $this->invoiceMarkItemsAsShippedRequestBuilder->setPayment($payment);
+ $this->invoiceMarkItemsAsShippedRequestBuilder->setAmount($amount);
+
+ $request = $this->invoiceMarkItemsAsShippedRequestBuilder->create();
+ $result = $this->orderManagementApi->markItemsAsShipped($request, $order->getStoreId());
+
+ try {
+ /** @var \Qliro\QliroOne\Model\OrderManagementStatus $omStatus */
+ $omStatus = $this->orderManagementStatusInterfaceFactory->create();
+ $omStatus->setRecordId($payment->getId());
+ $omStatus->setRecordType(OrderManagementStatusInterface::RECORD_TYPE_PAYMENT);
+ $omStatus->setTransactionId($result->getPaymentTransactionId());
+ $omStatus->setTransactionStatus(QliroOrderManagementStatusInterface::STATUS_CREATED);
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_DONE);
+ $omStatus->setMessage('Capture Requested for Invoice');
+ $omStatus->setQliroOrderId($link->getQliroOrderId());
+
+ $this->orderManagementStatusRepository->save($omStatus);
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'payment_id' => $payment->getId(),
+ ],
+ ]
+ );
+ }
+
+ if ($result->getStatus() == 'Created') {
+ if ($result->getPaymentTransactionId()) {
+ $payment->setTransactionId($result->getPaymentTransactionId());
+ }
+ } else {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Unable to capture payment for this order.')
+ );
+ }
+ }
+
+ /**
+ * @param \Magento\Sales\Model\Order\Shipment $shipment
+ * @return void
+ * @throws \Exception
+ */
+ public function captureByShipment($shipment)
+ {
+ if (!$this->qliroConfig->shouldCaptureOnShipment($shipment->getStoreId())) {
+ return;
+ }
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $shipment->getOrder();
+ $link = $this->linkRepository->getByOrderId($order->getId());
+ $this->logManager->setMerchantReference($link->getReference());
+
+ $this->shipmentMarkItemsAsShippedRequestBuilder->setShipment($shipment);
+ $request = $this->shipmentMarkItemsAsShippedRequestBuilder->create();
+
+ if (count($request->getOrderItems()) == 0) {
+ return;
+ }
+
+ $result = $this->orderManagementApi->markItemsAsShipped($request, $order->getStoreId());
+
+ try {
+ /** @var \Qliro\QliroOne\Model\OrderManagementStatus $omStatus */
+ $omStatus = $this->orderManagementStatusInterfaceFactory->create();
+
+ $omStatus->setRecordId($shipment->getId());
+ $omStatus->setRecordType(OrderManagementStatusInterface::RECORD_TYPE_SHIPMENT);
+ $omStatus->setTransactionId($result->getPaymentTransactionId());
+ $omStatus->setTransactionStatus(QliroOrderManagementStatusInterface::STATUS_CREATED);
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_DONE);
+ $omStatus->setMessage('Capture Requested for Shipment');
+ $omStatus->setQliroOrderId($link->getQliroOrderId());
+
+ $this->orderManagementStatusRepository->save($omStatus);
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'shipment_id' => $shipment->getId(),
+ ],
+ ]
+ );
+ }
+
+ if ($result->getStatus() != 'Created') {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Unable to mark items as shipped.')
+ );
+ }
+ }
+}
diff --git a/Model/Management/PlaceOrder.php b/Model/Management/PlaceOrder.php
new file mode 100644
index 0000000..09f8944
--- /dev/null
+++ b/Model/Management/PlaceOrder.php
@@ -0,0 +1,513 @@
+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);
+ }
+}
diff --git a/Model/Management/QliroOrder.php b/Model/Management/QliroOrder.php
new file mode 100644
index 0000000..5e1176e
--- /dev/null
+++ b/Model/Management/QliroOrder.php
@@ -0,0 +1,394 @@
+qliroConfig = $qliroConfig;
+ $this->merchantApi = $merchantApi;
+ $this->orderManagementApi = $orderManagementApi;
+ $this->linkRepository = $linkRepository;
+ $this->quoteRepository = $quoteRepository;
+ $this->containerMapper = $containerMapper;
+ $this->logManager = $logManager;
+ $this->validateOrderBuilder = $validateOrderBuilder;
+ $this->quoteFromValidateConverter = $quoteFromValidateConverter;
+ $this->quoteFromOrderConverter = $quoteFromOrderConverter;
+ $this->lock = $lock;
+ $this->updateRequestBuilder = $updateRequestBuilder;
+ $this->orderRepository = $orderRepository;
+ $this->orderManagementStatusInterfaceFactory = $orderManagementStatusInterfaceFactory;
+ $this->orderManagementStatusRepository = $orderManagementStatusRepository;
+ $this->quoteManagement = $quoteManagement;
+ }
+
+ /**
+ * Fetch a QliroOne order and return it as a container
+ *
+ * @param bool $allowRecreate
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderInterface
+ * @throws \Magento\Framework\Exception\AlreadyExistsException
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function get($allowRecreate = true)
+ {
+ $link = $this->quoteManagement->setQuote($this->getQuote())->getLinkFromQuote();
+ $this->logManager->setMark('GET QLIRO ORDER');
+
+ $qliroOrder = null; // Logical placeholder, may never happen
+
+ try {
+ $qliroOrderId = $link->getQliroOrderId();
+ $qliroOrder = $this->merchantApi->getOrder($qliroOrderId);
+
+ if ($this->lock->lock($qliroOrderId)) {
+ if (empty($link->getOrderId())) {
+ if ($qliroOrder->isPlaced()) {
+ if ($allowRecreate) {
+ $link->setIsActive(false);
+ $link->setMessage("getQliroOrder - allowRecreate");
+ $this->linkRepository->save($link);
+
+ return $this->get(false); // Recursion, but will max call it once
+ }
+ /*
+ * Reaching this point implies that the link between Qliro and Magento is out of sync.
+ * It should not happen.
+ */
+ throw new \LogicException('Order has already been processed.');
+ }
+ try {
+ $this->quoteFromOrderConverter->convert($qliroOrder, $this->getQuote());
+ $this->quoteManagement->recalculateAndSaveQuote();
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'link_id' => $link->getId(),
+ 'quote_id' => $link->getQuoteId(),
+ 'qliro_order_id' => $qliroOrderId,
+ ],
+ ]
+ );
+
+ $this->lock->unlock($qliroOrderId);
+ throw $exception;
+ }
+ }
+
+ $this->lock->unlock($qliroOrderId);
+ } else {
+ $this->logManager->debug(
+ 'An order is in preparation, not possible to update the quote',
+ [
+ 'extra' => [
+ 'link_id' => $link->getId(),
+ 'quote_id' => $link->getQuoteId(),
+ 'qliro_order_id' => $qliroOrderId,
+ ],
+ ]
+ );
+ }
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'link_id' => $link->getId(),
+ 'quote_id' => $link->getQuoteId(),
+ 'qliro_order_id' => $qliroOrderId ?? null,
+ ],
+ ]
+ );
+
+ throw new TerminalException('Couldn\'t fetch the QliroOne order.', null, $exception);
+ } finally {
+ $this->logManager->setMark(null);
+ }
+
+ return $qliroOrder;
+ }
+
+ /**
+ * Update quote with received data in the container and validate QliroOne order
+ *
+ * @param \Qliro\QliroOne\Api\Data\ValidateOrderNotificationInterface $validateContainer
+ * @return \Qliro\QliroOne\Api\Data\ValidateOrderResponseInterface
+ */
+ public function validate(ValidateOrderNotificationInterface $validateContainer)
+ {
+ /** @var \Qliro\QliroOne\Api\Data\ValidateOrderResponseInterface $responseContainer */
+ $responseContainer = $this->containerMapper->fromArray(
+ ['DeclineReason' => ValidateOrderResponseInterface::REASON_OTHER],
+ ValidateOrderResponseInterface::class
+ );
+
+ try {
+ $link = $this->linkRepository->getByQliroOrderId($validateContainer->getOrderId());
+ $this->logManager->setMerchantReference($link->getReference());
+
+ try {
+ $this->setQuote($this->quoteRepository->get($link->getQuoteId()));
+ $this->quoteFromValidateConverter->convert($validateContainer, $this->getQuote());
+ $this->quoteManagement->setQuote($this->getQuote())->recalculateAndSaveQuote();
+
+ return $this->validateOrderBuilder->setQuote($this->getQuote())->setValidationRequest(
+ $validateContainer
+ )->create();
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $validateContainer->getOrderId(),
+ 'quote_id' => $link->getQuoteId(),
+ ],
+ ]
+ );
+
+ return $responseContainer;
+ }
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $validateContainer->getOrderId(),
+ ],
+ ]
+ );
+
+ return $responseContainer;
+ }
+ }
+
+ /**
+ * Cancel QliroOne order
+ *
+ * @param int $qliroOrderId
+ * @return \Qliro\QliroOne\Api\Data\AdminTransactionResponseInterface
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function cancel($qliroOrderId)
+ {
+ $this->logManager->setMark('CANCEL QLIRO ORDER');
+
+ $responseContainer = null; // Logical placeholder, returning null may never happen
+
+ try {
+ /** @var \Qliro\QliroOne\Model\QliroOrder\Admin\CancelOrderRequest $request */
+ $request = $this->containerMapper->fromArray(
+ ['OrderId' => $qliroOrderId],
+ CancelOrderRequest::class
+ );
+
+ /*
+ * First we try to load an active link, then, when it fails, we try to load the inactive link
+ * and throw a specific exception if that exists.
+ */
+ try {
+ $link = $this->linkRepository->getByQliroOrderId($qliroOrderId);
+ $order = $this->orderRepository->get($link->getOrderId());
+ $request->setMerchantApiKey($this->qliroConfig->getMerchantApiKey($order->getStoreId()));
+ } catch (NoSuchEntityException $exception) {
+ $this->linkRepository->getByQliroOrderId($qliroOrderId, false);
+ throw new LinkInactiveException('This order has already been processed and the link deactivated.');
+ }
+
+ $responseContainer = $this->orderManagementApi->cancelOrder($request, $order->getStoreId());
+
+ /** @var \Qliro\QliroOne\Model\OrderManagementStatus $omStatus */
+ $omStatus = $this->orderManagementStatusInterfaceFactory->create();
+
+ $omStatus->setRecordType(OrderManagementStatusInterface::RECORD_TYPE_CANCEL);
+ $omStatus->setRecordId($link->getOrderId());
+ $omStatus->setTransactionId($responseContainer->getPaymentTransactionId());
+ $omStatus->setTransactionStatus($responseContainer->getStatus());
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_DONE);
+ $omStatus->setMessage('Cancellation requested');
+ $omStatus->setQliroOrderId($qliroOrderId);
+ $this->orderManagementStatusRepository->save($omStatus);
+
+ $link->setIsActive(false);
+ $this->linkRepository->save($link);
+ } catch (LinkInactiveException $exception) {
+ throw new TerminalException(
+ 'Couldn\'t request to cancel QliroOne order with inactive link.',
+ null,
+ $exception
+ );
+ } catch (\Exception $exception) {
+ $logData = [
+ 'qliro_order_id' => $qliroOrderId,
+ ];
+
+ if (isset($omStatus)) {
+ $logData = array_merge($logData, [
+ 'transaction_id' => $omStatus->getTransactionId(),
+ 'transaction_status' => $omStatus->getTransactionStatus(),
+ 'record_type' => $omStatus->getRecordType(),
+ 'record_id' => $omStatus->getRecordId(),
+ ]);
+ }
+
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => $logData,
+ ]
+ );
+
+ throw new TerminalException('Couldn\'t request to cancel QliroOne order.', null, $exception);
+ } finally {
+ $this->logManager->setMark(null);
+ }
+
+ return $responseContainer;
+ }
+}
diff --git a/Model/Management/Quote.php b/Model/Management/Quote.php
new file mode 100644
index 0000000..d9e1c65
--- /dev/null
+++ b/Model/Management/Quote.php
@@ -0,0 +1,538 @@
+qliroConfig = $qliroConfig;
+ $this->merchantApi = $merchantApi;
+ $this->createRequestBuilder = $createRequestBuilder;
+ $this->linkFactory = $linkFactory;
+ $this->linkRepository = $linkRepository;
+ $this->hashResolver = $hashResolver;
+ $this->quoteRepository = $quoteRepository;
+ $this->containerMapper = $containerMapper;
+ $this->logManager = $logManager;
+ $this->updateRequestBuilder = $updateRequestBuilder;
+ $this->json = $json;
+ $this->customerConverter = $customerConverter;
+ $this->fee = $fee;
+ $this->helper = $helper;
+ $this->eventManager = $eventManager;
+ }
+
+ /**
+ * Recalculate the quote, its totals, it's addresses and shipping rates, then saving quote
+ *
+ * @throws \Exception
+ */
+ public function recalculateAndSaveQuote()
+ {
+ $data['method'] = QliroOne::PAYMENT_METHOD_CHECKOUT_CODE;
+
+ $quote = $this->getQuote();
+ $customer = $quote->getCustomer();
+ $shippingAddress = $quote->getShippingAddress();
+ $billingAddress = $quote->getBillingAddress();
+
+ if ($quote->isVirtual()) {
+ $billingAddress->setPaymentMethod($data['method']);
+ } else {
+ $shippingAddress->setPaymentMethod($data['method']);
+ }
+
+ $billingAddress->save();
+
+ if (!$quote->isVirtual()) {
+ $shippingAddress->save();
+ }
+
+ $quote->assignCustomerWithAddressChange($customer, $billingAddress, $shippingAddress);
+ $quote->setTotalsCollectedFlag(false);
+
+ if (!$quote->isVirtual()) {
+ if ($this->qliroConfig->isUnifaunEnabled($quote->getStoreId())) {
+ $shippingAddress->setShippingMethod(
+ \Qliro\QliroOne\Model\Carrier\Unifaun::QLIRO_UNIFAUN_SHIPPING_CODE
+ );
+ }
+ $shippingAddress->setCollectShippingRates(true)->collectShippingRates()->save();
+ }
+
+ $extensionAttributes = $quote->getExtensionAttributes();
+
+ if (!empty($extensionAttributes)) {
+ $shippingAssignments = $extensionAttributes->getShippingAssignments();
+
+ if ($shippingAssignments) {
+ foreach ($shippingAssignments as $assignment) {
+ $assignment->getShipping()->setMethod($shippingAddress->getShippingMethod());
+ }
+ }
+ }
+ $quote->collectTotals();
+
+ $payment = $quote->getPayment();
+ $payment->importData($data);
+
+ $shippingAddress->save();
+ $this->quoteRepository->save($quote);
+ }
+
+ /**
+ * Get a link for the current quote
+ *
+ * @return \Qliro\QliroOne\Api\Data\LinkInterface
+ * @throws \Magento\Framework\Exception\AlreadyExistsException
+ */
+ public function getLinkFromQuote()
+ {
+ $quote = $this->getQuote();
+ $quoteId = $quote->getEntityId();
+
+ try {
+ $link = $this->linkRepository->getByQuoteId($quoteId);
+ } catch (NoSuchEntityException $exception) {
+ /** @var \Qliro\QliroOne\Api\Data\LinkInterface $link */
+ $link = $this->linkFactory->create();
+ $link->setRemoteIp($this->helper->getRemoteIp());
+ $link->setIsActive(true);
+ $link->setQuoteId($quoteId);
+ }
+
+ if ($link->getQliroOrderId()) {
+ $this->update($link->getQliroOrderId());
+ } else {
+ $this->logManager->debug('create new qliro order'); // @todo: remove
+ $orderReference = $this->generateOrderReference();
+ $this->logManager->setMerchantReference($orderReference);
+
+ $request = $this->createRequestBuilder->setQuote($quote)->create();
+ $request->setMerchantReference($orderReference);
+
+ try {
+ $orderId = $this->merchantApi->createOrder($request);
+ } catch (\Exception $exception) {
+ $orderId = null;
+ }
+
+ $hash = $this->generateUpdateHash($quote);
+ $link->setQuoteSnapshot($hash);
+
+ $link->setIsActive(true);
+ $link->setReference($orderReference);
+ $link->setQliroOrderId($orderId);
+ $this->linkRepository->save($link);
+ }
+
+ return $link;
+ }
+
+ /**
+ * Update qliro order with information in quote
+ *
+ * @param int|null $orderId
+ * @param bool $force
+ */
+ public function update($orderId, $force = false)
+ {
+ $this->logManager->setMark('UPDATE ORDER');
+
+ try {
+ $link = $this->linkRepository->getByQliroOrderId($orderId);
+ $this->logManager->setMerchantReference($link->getReference());
+
+ $isQliroOrderStatusEmpty = empty($link->getQliroOrderStatus());
+ $isQliroOrderStatusInProcess = $link->getQliroOrderStatus() == CheckoutStatusInterface::STATUS_IN_PROCESS;
+
+ if ($isQliroOrderStatusEmpty || $isQliroOrderStatusInProcess) {
+ $this->logManager->debug('update qliro order'); // @todo: remove
+ $quoteId = $link->getQuoteId();
+
+ try {
+ /** @var \Magento\Quote\Model\Quote $quote */
+ $quote = $this->quoteRepository->get($quoteId);
+
+ $hash = $this->generateUpdateHash($quote);
+
+ $this->logManager->debug(
+ sprintf(
+ 'order hash is %s',
+ $link->getQuoteSnapshot() === $hash ? 'same' : 'different'
+ )
+ ); // @todo: remove
+
+ if ($force || $this->canUpdateOrder($hash, $link)) {
+ $request = $this->updateRequestBuilder->setQuote($quote)->create();
+ $this->merchantApi->updateOrder($orderId, $request);
+ $link->setQuoteSnapshot($hash);
+ $this->linkRepository->save($link);
+ $this->logManager->debug(sprintf('updated order %s', $orderId)); // @todo: remove
+ }
+ } catch (\Exception $exception) {
+ if ($link && $link->getId()) {
+ $link->setIsActive(false);
+ $link->setMessage($exception->getMessage());
+ $this->linkRepository->save($link);
+ }
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $orderId,
+ 'quote_id' => $quoteId,
+ 'link_id' => $link->getId()
+ ],
+ ]
+ );
+ }
+ } else {
+ $this->logManager->debug('Can\'t update QliroOne order');
+ }
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $orderId,
+ ],
+ ]
+ );
+ } finally {
+ $this->logManager->setMark(null);
+ }
+ }
+
+ /**
+ * Check if QliroOne order can be updated
+ *
+ * @param string $hash
+ * @param \Qliro\QliroOne\Api\Data\LinkInterface $link
+ * @return bool
+ */
+ private function canUpdateOrder($hash, LinkInterface $link)
+ {
+ return empty($this->getQuote()->getShippingAddress()->getShippingMethod()) || $link->getQuoteSnapshot() !== $hash;
+ }
+
+ /**
+ * Generate a hash for quote content comparison
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return string
+ */
+ private function generateUpdateHash($quote)
+ {
+ $request = $this->updateRequestBuilder->setQuote($quote)->create();
+ $data = $this->containerMapper->toArray($request);
+ unset($data['AvailableShippingMethods']);
+ sort($data);
+
+ try {
+ $serializedData = $this->json->serialize($data);
+ } catch (\InvalidArgumentException $exception) {
+ $serializedData = null;
+ }
+
+ $hash = $serializedData ? md5($serializedData) : null;
+
+ $this->logManager->debug(
+ sprintf('generateUpdateHash: %s', $hash),
+ ['extra' => var_export($data, true)]
+ ); // @todo: remove
+
+ return $hash;
+ }
+
+ /**
+ * Generate a QliroOne unique order reference
+ *
+ * @return string
+ */
+ public function generateOrderReference()
+ {
+ $quote = $this->getQuote();
+ $hash = $this->hashResolver->resolveHash($quote);
+ $this->validateHash($hash);
+ $hashLength = self::REFERENCE_MIN_LENGTH;
+
+ do {
+ $isUnique = false;
+ $shortenedHash = substr($hash, 0, $hashLength);
+
+ try {
+ $this->linkRepository->getByReference($shortenedHash);
+
+ if ((++$hashLength) > HashResolverInterface::HASH_MAX_LENGTH) {
+ $hash = $this->hashResolver->resolveHash($quote);
+ $this->validateHash($hash);
+ $hashLength = self::REFERENCE_MIN_LENGTH;
+ }
+ } catch (NoSuchEntityException $exception) {
+ $isUnique = true;
+ }
+ } while (!$isUnique);
+
+ return $shortenedHash;
+ }
+
+ /**
+ * Update customer with data from QliroOne frontend callback
+ *
+ * @param array $customerData
+ * @throws \Exception
+ */
+ public function updateCustomer($customerData)
+ {
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $qliroCustomer */
+ $qliroCustomer = $this->containerMapper->fromArray($customerData, QliroOrderCustomerInterface::class);
+
+ $this->customerConverter->convert($qliroCustomer, $this->getQuote());
+ $this->recalculateAndSaveQuote();
+ }
+
+ /**
+ * Update shipping price in quote
+ * Return true in case shipping price was set, or false if the quote is virtual or update didn't happen
+ *
+ * @param float|null $price
+ * @return bool
+ * @throws \Exception
+ */
+ public function updateShippingPrice($price)
+ {
+ $quote = $this->getQuote();
+
+ if ($price && !$quote->isVirtual()) {
+ // @codingStandardsIgnoreStart
+ // phpcs:disable
+ $container = new DataObject(
+ [
+ 'shipping_price' => $price,
+ 'can_save_quote' => false,
+ ]
+ );
+ // @codingStandardsIgnoreEnd
+ // phpcs:enable
+
+ $this->eventManager->dispatch(
+ 'qliroone_shipping_price_update_before',
+ [
+ 'quote' => $quote,
+ 'container' => $container,
+ ]
+ );
+ $this->updateReceivedAmount($container);
+
+ if ($container->getCanSaveQuote()) {
+ $this->recalculateAndSaveQuote();
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * If freight amount comes from Qliro, it's Unifaun and that amount has to be stored for Carrier to pick up
+ *
+ * @param $container
+ */
+ public function updateReceivedAmount($container)
+ {
+ try {
+ $quote = $this->getQuote();
+ if ($this->qliroConfig->isUnifaunEnabled($quote->getStoreId())) {
+ $link = $this->linkRepository->getByQuoteId($quote->getId());
+ if ($link->getUnifaunShippingAmount() != $container->getData('shipping_price')) {
+ $link->setUnifaunShippingAmount($container->getData('shipping_price'));
+ $this->linkRepository->save($link);
+ $container->setData('can_save_quote', true);
+ }
+ }
+ } catch (\Exception $exception) {
+ }
+ }
+
+ /**
+ * Update selected shipping method in quote
+ * Return true in case shipping method was set, or false if the quote is virtual or method was not changed
+ *
+ * @param float $fee
+ * @return bool
+ * @throws \Exception
+ */
+ public function updateFee($fee)
+ {
+ try {
+ $this->fee->setQlirooneFeeInclTax($this->getQuote(), $fee);
+ $this->recalculateAndSaveQuote();
+ } catch (\Exception $exception) {
+ $link = $this->getLinkFromQuote();
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $link->getOrderId(),
+ ],
+ ]
+ );
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Validate hash against QliroOne order merchant reference requirements
+ *
+ * @param string $hash
+ */
+ private function validateHash($hash)
+ {
+ if (!preg_match(HashResolverInterface::VALIDATE_MERCHANT_REFERENCE, $hash)) {
+ throw new \DomainException(sprintf('Merchant reference \'%s\' will not be accepted by Qliro', $hash));
+ }
+ }
+}
diff --git a/Model/Management/ShippingMethod.php b/Model/Management/ShippingMethod.php
new file mode 100644
index 0000000..5870f9d
--- /dev/null
+++ b/Model/Management/ShippingMethod.php
@@ -0,0 +1,223 @@
+linkRepository = $linkRepository;
+ $this->quoteRepository = $quoteRepository;
+ $this->shippingMethodsBuilder = $shippingMethodsBuilder;
+ $this->containerMapper = $containerMapper;
+ $this->logManager = $logManager;
+ $this->quoteFromShippingMethodsConverter = $quoteFromShippingConverter;
+ $this->eventManager = $eventManager;
+ $this->quoteManagement = $quoteManagement;
+ }
+
+ /**
+ * Update quote with received data in the container and return a list of available shipping methods
+ *
+ * @param \Qliro\QliroOne\Api\Data\UpdateShippingMethodsNotificationInterface $updateContainer
+ * @return \Qliro\QliroOne\Api\Data\UpdateShippingMethodsResponseInterface
+ */
+ public function get(UpdateShippingMethodsNotificationInterface $updateContainer)
+ {
+ /** @var \Qliro\QliroOne\Api\Data\UpdateShippingMethodsResponseInterface $declineContainer */
+ $declineContainer = $this->containerMapper->fromArray(
+ ['DeclineReason' => UpdateShippingMethodsResponseInterface::REASON_POSTAL_CODE],
+ UpdateShippingMethodsResponseInterface::class
+ );
+
+ try {
+ $link = $this->linkRepository->getByQliroOrderId($updateContainer->getOrderId());
+ $this->logManager->setMerchantReference($link->getReference());
+
+ try {
+ $this->setQuote($this->quoteRepository->get($link->getQuoteId()));
+ $this->quoteFromShippingMethodsConverter->convert($updateContainer, $this->getQuote());
+ $this->quoteManagement->setQuote($this->getQuote())->recalculateAndSaveQuote();
+
+ return $this->shippingMethodsBuilder->setQuote($this->getQuote())->create();
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $updateContainer->getOrderId(),
+ 'quote_id' => $link->getQuoteId(),
+ ],
+ ]
+ );
+
+ return $declineContainer;
+ }
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $updateContainer->getOrderId(),
+ ],
+ ]
+ );
+
+ return $declineContainer;
+ }
+ }
+
+ /**
+ * Update selected shipping method in quote
+ * Return true in case shipping method was set, or false if the quote is virtual or method was not changed
+ *
+ * @param string $code
+ * @param string|null $secondaryOption
+ * @param float|null $price
+ * @return bool
+ * @throws \Exception
+ */
+ public function update($code, $secondaryOption = null, $price = null)
+ {
+ $quote = $this->getQuote();
+
+ if ($code && !$quote->isVirtual()) {
+ $shippingAddress = $quote->getShippingAddress();
+
+ if (!$shippingAddress->getPostcode()) {
+ $billingAddress = $quote->getBillingAddress();
+ $shippingAddress->addData(
+ [
+ 'email' => $billingAddress->getEmail(),
+ 'firstname' => $billingAddress->getFirstname(),
+ 'lastname' => $billingAddress->getLastname(),
+ 'company' => $billingAddress->getCompany(),
+ 'street' => $billingAddress->getStreetFull(),
+ 'city' => $billingAddress->getCity(),
+ 'region' => $billingAddress->getRegion(),
+ 'region_id' => $billingAddress->getRegionId(),
+ 'postcode' => $billingAddress->getPostcode(),
+ 'country_id' => $billingAddress->getCountryId(),
+ 'telephone' => $billingAddress->getTelephone(),
+ 'same_as_billing' => true,
+ ]
+ );
+ }
+
+ // @codingStandardsIgnoreStart
+ // phpcs:disable
+ $container = new DataObject(
+ [
+ 'shipping_method' => $code,
+ 'secondary_option' => $secondaryOption,
+ 'shipping_price' => $price,
+ 'can_save_quote' => $shippingAddress->getShippingMethod() !== $code,
+ ]
+ );
+ // @codingStandardsIgnoreEnd
+ // phpcs:enable
+
+ $this->eventManager->dispatch(
+ 'qliroone_shipping_method_update_before',
+ [
+ 'quote' => $quote,
+ 'container' => $container,
+ ]
+ );
+ $this->quoteManagement->setQuote($this->getQuote())->updateReceivedAmount($container);
+
+ if (!$container->getCanSaveQuote()) {
+ return false;
+ }
+
+ $shippingAddress->setShippingMethod($container->getShippingMethod());
+ $this->quoteManagement->recalculateAndSaveQuote();
+
+ // For some reason shipping code that was previously set, is not applied
+ if ($shippingAddress->getShippingMethod() !== $container->getShippingMethod()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/Model/Management/TransactionStatus.php b/Model/Management/TransactionStatus.php
new file mode 100644
index 0000000..c8577c5
--- /dev/null
+++ b/Model/Management/TransactionStatus.php
@@ -0,0 +1,266 @@
+linkRepository = $linkRepository;
+ $this->logManager = $logManager;
+ $this->lock = $lock;
+ $this->qliroOrderManagementStatusResponseFactory = $qliroOrderManagementStatusResponseFactory;
+ $this->orderManagementStatusInterfaceFactory = $orderManagementStatusInterfaceFactory;
+ $this->orderManagementStatusRepository = $orderManagementStatusRepository;
+ $this->statusUpdateHandlerPool = $statusUpdateHandlerPool;
+ }
+
+ /**
+ * Handles Order Management Status Transaction notifications
+ *
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @return \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatusResponse
+ */
+ public function handle($qliroOrderManagementStatus)
+ {
+ $qliroOrderId = $qliroOrderManagementStatus->getOrderId();
+
+ try {
+ $link = $this->linkRepository->getByQliroOrderId($qliroOrderId);
+ $this->logManager->setMerchantReference($link->getReference());
+
+ $orderId = $link->getOrderId();
+
+ if (empty($orderId)) {
+ /* Should not happen, but if it does, respond with this to stop new notifications */
+ return $this->qliroOrderManagementStatusRespond(
+ QliroOrderManagementStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND
+ );
+ } elseif (!$this->updateTransactionStatus($qliroOrderManagementStatus)) {
+ return $this->qliroOrderManagementStatusRespond(
+ QliroOrderManagementStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND
+ );
+ }
+ } catch (NoSuchEntityException $exception) {
+ /* No more qliro notifications should be sent */
+ return $this->qliroOrderManagementStatusRespond(
+ QliroOrderManagementStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND
+ );
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $qliroOrderId,
+ ],
+ ]
+ );
+
+ return $this->qliroOrderManagementStatusRespond(
+ QliroOrderManagementStatusResponseInterface::RESPONSE_ORDER_NOT_FOUND
+ );
+ }
+
+ return $this->qliroOrderManagementStatusRespond(
+ QliroOrderManagementStatusResponseInterface::RESPONSE_RECEIVED
+ );
+ }
+
+ /**
+ * If a transaction is received that is of same type as previou, same transaction id and marked as handled, it does
+ * not have to be handled, since it was done already the first time it arrived.
+ * Reply true when properly handled
+ *
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @return bool
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ * @throws \Magento\Framework\Exception\AlreadyExistsException
+ */
+ private function updateTransactionStatus($qliroOrderManagementStatus)
+ {
+ $result = true;
+
+ try {
+ $qliroOrderId = $qliroOrderManagementStatus->getOrderId();
+
+ /** @var \Qliro\QliroOne\Model\OrderManagementStatus $omStatus */
+ $omStatus = $this->orderManagementStatusInterfaceFactory->create();
+ $omStatus->setTransactionId($qliroOrderManagementStatus->getPaymentTransactionId());
+ $omStatus->setTransactionStatus($qliroOrderManagementStatus->getStatus());
+ $omStatus->setQliroOrderId($qliroOrderId);
+ $omStatus->setMessage('Notification update');
+
+ $handleTransaction = true;
+
+ try {
+ /** @var \Qliro\QliroOne\Model\OrderManagementStatus $omStatusParent */
+ $omStatusParent = $this->orderManagementStatusRepository->getParent(
+ $qliroOrderManagementStatus->getPaymentTransactionId()
+ );
+
+ if ($omStatusParent) {
+ $omStatus->setRecordId($omStatusParent->getRecordId());
+ $omStatus->setRecordType($omStatusParent->getRecordType());
+ }
+
+ /** @var \Qliro\QliroOne\Model\OrderManagementStatus $omStatusPrevious */
+ $omStatusPrevious = $this->orderManagementStatusRepository->getPrevious(
+ $qliroOrderManagementStatus->getPaymentTransactionId()
+ );
+
+ if ($omStatusPrevious) {
+ if ($omStatus->getTransactionStatus() == $omStatusPrevious->getTransactionStatus()) {
+ $handleTransaction = false;
+ }
+ }
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $qliroOrderId,
+ 'transaction_id' => $omStatus->getTransactionId(),
+ 'transaction_status' => $omStatus->getTransactionStatus(),
+ 'record_type' => $omStatus->getRecordType(),
+ 'record_id' => $omStatus->getRecordId(),
+ ],
+ ]
+ );
+ $result = false;
+ }
+
+ if ($handleTransaction) {
+ if ($this->lock->lock($qliroOrderId)) {
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_NEW);
+ $this->orderManagementStatusRepository->save($omStatus);
+ if ($this->statusUpdateHandlerPool->handle($qliroOrderManagementStatus, $omStatus)) {
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_DONE);
+ }
+ $this->lock->unlock($qliroOrderId);
+ } else {
+ $omStatus->setMessage('Skipped due to lock');
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_SKIPPED);
+ }
+ } else {
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_SKIPPED);
+ }
+
+ $this->orderManagementStatusRepository->save($omStatus);
+ } catch (\Exception $exception) {
+ $logData = [
+ 'qliro_order_id' => $qliroOrderId ?? null,
+ ];
+
+ if (isset($omStatus)) {
+ $logData = array_merge($logData, [
+ 'transaction_id' => $omStatus->getTransactionId(),
+ 'transaction_status' => $omStatus->getTransactionStatus(),
+ 'record_type' => $omStatus->getRecordType(),
+ 'record_id' => $omStatus->getRecordId(),
+ ]);
+ }
+
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => $logData,
+ ]
+ );
+
+ if (isset($omStatus) && $omStatus && $omStatus->getId()) {
+ $omStatus->setNotificationStatus(OrderManagementStatusInterface::NOTIFICATION_STATUS_ERROR);
+ $this->orderManagementStatusRepository->save($omStatus);
+ }
+ $this->lock->unlock($qliroOrderId);
+
+ $result = false;
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param string $result
+ * @return mixed
+ */
+ private function qliroOrderManagementStatusRespond($result)
+ {
+ return $this->qliroOrderManagementStatusResponseFactory->create()->setCallbackResponse($result);
+ }
+}
diff --git a/Model/Method/QliroOne.php b/Model/Method/QliroOne.php
new file mode 100644
index 0000000..093e13a
--- /dev/null
+++ b/Model/Method/QliroOne.php
@@ -0,0 +1,380 @@
+adapter = $adapter;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getCode()
+ {
+ return $this->adapter->getCode();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getFormBlockType()
+ {
+ return $this->adapter->getFormBlockType();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getTitle()
+ {
+ return $this->adapter->getTitle();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setStore($storeId)
+ {
+ $this->adapter->setStore($storeId);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getStore()
+ {
+ return $this->adapter->getStore();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canOrder()
+ {
+ return $this->adapter->canOrder();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canAuthorize()
+ {
+ return $this->adapter->canAuthorize();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canCapture()
+ {
+ return $this->adapter->canCapture();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canCapturePartial()
+ {
+ return $this->adapter->canCapturePartial();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canCaptureOnce()
+ {
+ return $this->adapter->canCaptureOnce();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canRefund()
+ {
+ return $this->adapter->canRefund();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canRefundPartialPerInvoice()
+ {
+ return $this->adapter->canRefundPartialPerInvoice();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canVoid()
+ {
+ return $this->adapter->canVoid();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canUseInternal()
+ {
+ return $this->adapter->canUseInternal();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canUseCheckout()
+ {
+ return $this->adapter->canUseCheckout();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canEdit()
+ {
+ return $this->adapter->canEdit();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canFetchTransactionInfo()
+ {
+ return $this->adapter->canFetchTransactionInfo();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function fetchTransactionInfo(InfoInterface $payment, $transactionId)
+ {
+ return $this->adapter->fetchTransactionInfo($payment, $transactionId);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isGateway()
+ {
+ return $this->adapter->isGateway();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isOffline()
+ {
+ return $this->adapter->isOffline();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isInitializeNeeded()
+ {
+ return $this->adapter->isInitializeNeeded();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canUseForCountry($country)
+ {
+ return $this->adapter->canUseForCountry($country);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canUseForCurrency($currencyCode)
+ {
+ return $this->adapter->canUseForCurrency($currencyCode);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getInfoBlockType()
+ {
+ return $this->adapter->getInfoBlockType();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getInfoInstance()
+ {
+ return $this->adapter->getInfoInstance();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setInfoInstance(InfoInterface $info)
+ {
+ $this->adapter->setInfoInstance($info);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function validate()
+ {
+ return $this->adapter->validate();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function order(InfoInterface $payment, $amount)
+ {
+ throw new \Exception("order - feature not implemented\n");
+ return $this->adapter->order($payment, $amount);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function authorize(InfoInterface $payment, $amount)
+ {
+ throw new \Exception("authorize - feature not implemented\n");
+ return $this->adapter->authorize($payment, $amount);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function capture(InfoInterface $payment, $amount)
+ {
+ return $this->adapter->capture($payment, $amount);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function refund(InfoInterface $payment, $amount)
+ {
+ throw new \Exception("refund - feature not implemented\n");
+ return $this->adapter->refund($payment, $amount);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function cancel(InfoInterface $payment)
+ {
+ return $this->adapter->cancel($payment);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function void(InfoInterface $payment)
+ {
+ return $this->adapter->void($payment);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function canReviewPayment()
+ {
+ //throw new \Exception("canReviewPayment - Stop\n");
+ return $this->adapter->canReviewPayment();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function acceptPayment(InfoInterface $payment)
+ {
+ //throw new \Exception("acceptPayment - Stop\n");
+ return $this->adapter->acceptPayment($payment);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function denyPayment(InfoInterface $payment)
+ {
+ //throw new \Exception("denyPayment - Stop\n");
+ return $this->adapter->denyPayment($payment);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getConfigData($field, $storeId = null)
+ {
+ return $this->adapter->getConfigData($field, $storeId);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function assignData(DataObject $data)
+ {
+ return $this->adapter->assignData($data);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isAvailable(CartInterface $quote = null)
+ {
+ return $this->adapter->isAvailable($quote);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isActive($storeId = null)
+ {
+ return $this->adapter->isActive($storeId);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function initialize($paymentAction, $stateObject)
+ {
+ return $this->adapter->initialize($paymentAction, $stateObject);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getConfigPaymentAction()
+ {
+ return $this->adapter->getConfigPaymentAction();
+ }
+}
diff --git a/Model/Method/QliroOne/Cancel.php b/Model/Method/QliroOne/Cancel.php
new file mode 100644
index 0000000..d4d2b43
--- /dev/null
+++ b/Model/Method/QliroOne/Cancel.php
@@ -0,0 +1,113 @@
+linkRepository = $linkRepository;
+ $this->logManager = $logManager;
+ $this->qliroManagement = $qliroManagement;
+ }
+
+ /**
+ * Cancel command
+ *
+ * @param array $commandSubject
+ * @return null
+ * @throws \Exception
+ */
+ public function execute(array $commandSubject)
+ {
+ if (isset($commandSubject['payment'])) {
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $commandSubject['payment']->getOrder();
+ $orderId = $order->getId();
+ } else {
+ $orderId = null;
+ }
+
+ try {
+ try {
+ $link = $this->linkRepository->getByOrderId($orderId);
+ } catch (NoSuchEntityException $exception) {
+ $this->linkRepository->getByOrderId($orderId, false);
+ throw new LinkInactiveException('This order has already been processed and the link deactivated.');
+ }
+
+ $this->logManager->setMerchantReference($link->getReference());
+
+ $link->setMessage(sprintf('Order #%s marked as canceled', $orderId));
+ $this->linkRepository->save($link);
+
+ $this->qliroManagement->cancelQliroOrder($link->getQliroOrderId());
+ $this->logManager->info(
+ 'Canceled order, requested a QliroOne order cancellation',
+ [
+ 'extra' => [
+ 'order_id' => $orderId,
+ 'qliro_order_id' => $link->getQliroOrderId(),
+ ]
+ ]
+ );
+ } catch (LinkInactiveException $exception) {
+ return null;
+ } catch (\Exception $exception) {
+ $logData = [
+ 'order_id' => $orderId,
+ 'qliro_order_id' => isset($link) ? $link->getQliroOrderId() : null,
+ 'exception' => $exception,
+ ];
+
+ if (!($exception instanceof TerminalException)) {
+ $this->logManager->critical($exception, ['extra' => $logData]);
+
+ throw $exception;
+ }
+
+ $this->logManager->debug('Cancellation was unsuccessful.', ['extra' => $logData]);
+ }
+
+ return null;
+ }
+}
diff --git a/Model/Method/QliroOne/Capture.php b/Model/Method/QliroOne/Capture.php
new file mode 100644
index 0000000..39e4c1d
--- /dev/null
+++ b/Model/Method/QliroOne/Capture.php
@@ -0,0 +1,70 @@
+qliroManagement = $qliroManagement;
+ $this->qliroConfig = $qliroConfig;
+ }
+
+ /**
+ * Capture command
+ *
+ * @param array $commandSubject
+ *
+ * @return ResultInterface|null
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function execute(array $commandSubject)
+ {
+ /** @var \Magento\Payment\Model\InfoInterface $payment */
+ $payment = $commandSubject['payment']->getPayment();
+ $amount = $commandSubject['amount'];
+
+ try {
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $payment->getOrder();
+ if ($this->qliroConfig->shouldCaptureOnInvoice($order ? $order->getStoreId() : null)) {
+ $this->qliroManagement->captureByInvoice($payment, $amount);
+ }
+ } catch (\Exception $exception) {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Unable to capture payment for this order.')
+ );
+ }
+
+ return $this;
+ }
+}
diff --git a/Model/Newsletter/Subscription.php b/Model/Newsletter/Subscription.php
new file mode 100644
index 0000000..b97e0f1
--- /dev/null
+++ b/Model/Newsletter/Subscription.php
@@ -0,0 +1,166 @@
+subscriberFactory = $subscriberFactory;
+ $this->customerSession = $customerSession;
+ $this->customerUrl = $customerUrl;
+ $this->scopeConfig = $scopeConfig;
+ $this->messageManager = $messageManager;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Validates that if the current user is a guest, that they can subscribe to a newsletter.
+ *
+ * @param int $storeId
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @return void
+ */
+ private function validateGuestSubscription($storeId)
+ {
+ if (
+ $this->scopeConfig->getValue(Subscriber::XML_PATH_ALLOW_GUEST_SUBSCRIBE_FLAG, ScopeInterface::SCOPE_STORE, $storeId) != 1
+ && !$this->customerSession->isLoggedIn()
+ ) {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __(
+ 'Sorry, but the administrator denied subscription for guests. Please register .',
+ $this->customerUrl->getRegisterUrl()
+ )
+ );
+ }
+ }
+
+ /**
+ * Validates the format of the email address
+ *
+ * @param string $email
+ * @throws \Magento\Framework\Exception\LocalizedException
+ * @return void
+ */
+ private function validateEmailFormat($email)
+ {
+ if (!\filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ throw new \Magento\Framework\Exception\LocalizedException(__('Please enter a valid email address.'));
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function addSubscription($email, $storeId)
+ {
+ try {
+ $this->validateEmailFormat($email);
+ $this->validateGuestSubscription($storeId);
+
+ $subscriber = $this->subscriberFactory->create()->loadByEmail($email);
+ if ($subscriber->getId() || $subscriber->getSubscriberStatus() != Subscriber::STATUS_SUBSCRIBED) {
+ $status = $this->subscriberFactory->create()->subscribe($email);
+ $this->logManager->info('Added {email} as subscriber', ['email' => $email]);
+ if ($status == Subscriber::STATUS_NOT_ACTIVE) {
+ $this->messageManager->addSuccessMessage(__('The confirmation request has been sent.'));
+ } else {
+ $this->messageManager->addSuccessMessage(__('Thank you for your subscription.'));
+ }
+ }
+ } catch (\Magento\Framework\Exception\LocalizedException $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'email' => $email,
+ 'storeId' => $storeId
+ ],
+ ]
+ );
+ $this->messageManager->addExceptionMessage(
+ $exception,
+ __('There was a problem with the subscription: %1', $exception->getMessage())
+ );
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'email' => $email,
+ 'storeId' => $storeId
+ ],
+ ]
+ );
+ $this->messageManager->addExceptionMessage($exception, __('Something went wrong with the subscription.'));
+ }
+ }
+}
diff --git a/Model/Notification/CheckoutStatus.php b/Model/Notification/CheckoutStatus.php
new file mode 100644
index 0000000..32b5588
--- /dev/null
+++ b/Model/Notification/CheckoutStatus.php
@@ -0,0 +1,118 @@
+orderId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMerchantReference()
+ {
+ return $this->merchantReference;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTimestamp()
+ {
+ return $this->timeStamp;
+ }
+
+ /**
+ * @param int $value
+ * @return $this
+ */
+ public function setOrderId($value)
+ {
+ $this->orderId = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setMerchantReference($value)
+ {
+ $this->merchantReference = $value;
+
+ return $this;
+ }
+
+ /**
+ * Set customer checkout status.
+ * May take one of the following values:
+ * - "InProcess" - The order is created but the customer hasn't completed the purchase yet
+ * - "OnHold" - The customer has completed the order, but it is pending until a manual assessment is made
+ * - "Completed" - The order is confirmed and the payment is complete
+ * - "Refused" - For some reason, the order was refused
+ *
+ * @param string $value
+ * @return $this
+ */
+ public function setStatus($value)
+ {
+ $this->status = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setTimestamp($value)
+ {
+ $this->timeStamp = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/Notification/CheckoutStatusResponse.php b/Model/Notification/CheckoutStatusResponse.php
new file mode 100644
index 0000000..65a9ba9
--- /dev/null
+++ b/Model/Notification/CheckoutStatusResponse.php
@@ -0,0 +1,39 @@
+callbackResponse;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setCallbackResponse($value)
+ {
+ $this->callbackResponse = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/Notification/QliroOrderManagementStatus.php b/Model/Notification/QliroOrderManagementStatus.php
new file mode 100644
index 0000000..c18a2f3
--- /dev/null
+++ b/Model/Notification/QliroOrderManagementStatus.php
@@ -0,0 +1,331 @@
+orderId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMerchantReference()
+ {
+ return $this->merchantReference;
+ }
+
+ /**
+ * Can return one of the statuses declared in the interface
+ *
+ * @return string
+ */
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ /**
+ * @return int
+ */
+ public function getPaymentTransactionId()
+ {
+ return $this->paymentTransactionId;
+ }
+
+ /**
+ * @return float
+ */
+ public function getAmount()
+ {
+ return $this->amount;
+ }
+
+ /**
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPaymentType()
+ {
+ return $this->paymentType;
+ }
+
+ /**
+ * @return string
+ */
+ public function getProviderTransactionId()
+ {
+ return $this->providerTransactionid;
+ }
+
+ /**
+ * @return string
+ */
+ public function getProviderResultCode()
+ {
+ return $this->providerResultCode;
+ }
+
+ /**
+ * @return string
+ */
+ public function getProviderResultDescription()
+ {
+ return $this->providerResultDescription;
+ }
+
+ /**
+ * @return string
+ */
+ public function getOriginalPaymentTransactionId()
+ {
+ return $this->originalPaymentTransactionId;
+ }
+
+ /**
+ * @return int
+ */
+ public function getPaymentReference()
+ {
+ return $this->paymentReference;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTimestamp()
+ {
+ return $this->timeStamp;
+ }
+
+ /**
+ * @param int $value
+ * @return $this
+ */
+ public function setOrderId($value)
+ {
+ $this->orderId = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setMerchantReference($value)
+ {
+ $this->merchantReference = $value;
+
+ return $this;
+ }
+
+ /**
+ * Can only be set to one of the statuses declared in the interface
+ *
+ * @param string $value
+ * @return $this
+ */
+ public function setStatus($value)
+ {
+ $this->status = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param int $value
+ * @return $this
+ */
+ public function setPaymentTransactionId($value)
+ {
+ $this->paymentTransactionId = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param float $value
+ * @return $this
+ */
+ public function setAmount($value)
+ {
+ $this->amount = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setCurrency($value)
+ {
+ $this->currency = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setPaymentType($value)
+ {
+ $this->paymentType = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setProviderTransactionId($value)
+ {
+ $this->providerTransactionid = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setProviderResultCode($value)
+ {
+ $this->providerResultCode = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setProviderResultDescription($value)
+ {
+ $this->providerResultDescription = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setOriginalPaymentTransactionId($value)
+ {
+ $this->originalPaymentTransactionId = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param int $value
+ * @return $this
+ */
+ public function setPaymentReference($value)
+ {
+ $this->paymentReference = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setTimestamp($value)
+ {
+ $this->timeStamp = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/Notification/QliroOrderManagementStatusResponse.php b/Model/Notification/QliroOrderManagementStatusResponse.php
new file mode 100644
index 0000000..f919b9e
--- /dev/null
+++ b/Model/Notification/QliroOrderManagementStatusResponse.php
@@ -0,0 +1,39 @@
+callbackResponse;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setCallbackResponse($value)
+ {
+ $this->callbackResponse = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/Notification/ShippingMethodsResponse.php b/Model/Notification/ShippingMethodsResponse.php
new file mode 100644
index 0000000..537fb38
--- /dev/null
+++ b/Model/Notification/ShippingMethodsResponse.php
@@ -0,0 +1,63 @@
+availableShippingMethods;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDeclineReason()
+ {
+ return $this->declineReason;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderShippingMethodInterface[] $value
+ * @return $this
+ */
+ public function setAvailableShippingMethods($value)
+ {
+ $this->availableShippingMethods = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setDeclineReason($value)
+ {
+ $this->declineReason = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/Notification/UpdateShippingMethods.php b/Model/Notification/UpdateShippingMethods.php
new file mode 100644
index 0000000..01bdb78
--- /dev/null
+++ b/Model/Notification/UpdateShippingMethods.php
@@ -0,0 +1,197 @@
+orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return UpdateShippingMethods
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantReference()
+ {
+ return $this->merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return UpdateShippingMethods
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCountryCode()
+ {
+ return $this->countryCode;
+ }
+
+ /**
+ * @param string $code
+ * @return UpdateShippingMethods
+ */
+ public function setCountryCode($code)
+ {
+ $this->countryCode = $code;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface
+ */
+ public function getCustomer()
+ {
+ return $this->customer;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $customer
+ * @return UpdateShippingMethods
+ */
+ public function setCustomer($customer)
+ {
+ $this->customer = $customer;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function getShippingAddress()
+ {
+ return $this->shippingAddress;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $shippingAddress
+ * @return UpdateShippingMethods
+ */
+ public function setShippingAddress($shippingAddress)
+ {
+ $this->shippingAddress = $shippingAddress;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getOrderItems()
+ {
+ return $this->orderItems;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @return UpdateShippingMethods
+ */
+ public function setOrderItems($orderItems)
+ {
+ $this->orderItems = $orderItems;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getSelectedShippingMethod()
+ {
+ return $this->selectedShippingMethod;
+ }
+
+ /**
+ * @param string $selectedShippingMethod
+ * @return UpdateShippingMethods
+ */
+ public function setSelectedShippingMethod($selectedShippingMethod)
+ {
+ $this->selectedShippingMethod = $selectedShippingMethod;
+
+ return $this;
+ }
+}
diff --git a/Model/Notification/ValidateOrder.php b/Model/Notification/ValidateOrder.php
new file mode 100644
index 0000000..f2e06b3
--- /dev/null
+++ b/Model/Notification/ValidateOrder.php
@@ -0,0 +1,275 @@
+orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return $this
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantReference()
+ {
+ return $this->merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return $this
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return $this
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderIdentityVerificationInterface
+ */
+ public function getIdentityVerification()
+ {
+ return $this->identityVerification;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderIdentityVerificationInterface $identityVerification
+ * @return $this
+ */
+ public function setIdentityVerification($identityVerification)
+ {
+ $this->identityVerification = $identityVerification;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface
+ */
+ public function getCustomer()
+ {
+ return $this->customer;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $customer
+ * @return $this
+ */
+ public function setCustomer($customer)
+ {
+ $this->customer = $customer;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function getShippingAddress()
+ {
+ return $this->shippingAddress;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $shippingAddress
+ * @return $this
+ */
+ public function setShippingAddress($shippingAddress)
+ {
+ $this->shippingAddress = $shippingAddress;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getOrderItems()
+ {
+ return $this->orderItems;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @return $this
+ */
+ public function setOrderItems($orderItems)
+ {
+ $this->orderItems = $orderItems;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderPaymentMethodInterface
+ */
+ public function getPaymentMethod()
+ {
+ return $this->paymentMethod;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderPaymentMethodInterface $paymentMethod
+ * @return $this
+ */
+ public function setPaymentMethod($paymentMethod)
+ {
+ $this->paymentMethod = $paymentMethod;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getSelectedShippingMethod()
+ {
+ return $this->selectedShippingMethod;
+ }
+
+ /**
+ * @param string $selectedShippingMethod
+ * @return $this
+ */
+ public function setSelectedShippingMethod($selectedShippingMethod)
+ {
+ $this->selectedShippingMethod = $selectedShippingMethod;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getSelectedShippingSecondaryOption()
+ {
+ return $this->selectedShippingSecondaryOption;
+ }
+
+ /**
+ * @param string $selectedShippingSecondaryOption
+ * @return $this
+ */
+ public function setSelectedShippingSecondaryOption($selectedShippingSecondaryOption)
+ {
+ $this->selectedShippingSecondaryOption = $selectedShippingSecondaryOption;
+
+ return $this;
+ }
+}
diff --git a/Model/Notification/ValidateOrderResponse.php b/Model/Notification/ValidateOrderResponse.php
new file mode 100644
index 0000000..13dd6ba
--- /dev/null
+++ b/Model/Notification/ValidateOrderResponse.php
@@ -0,0 +1,39 @@
+declineReason;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setDeclineReason($value)
+ {
+ $this->declineReason = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/Order/OrderPlacer.php b/Model/Order/OrderPlacer.php
new file mode 100644
index 0000000..0e29330
--- /dev/null
+++ b/Model/Order/OrderPlacer.php
@@ -0,0 +1,209 @@
+quoteRepository = $quoteRepository;
+ $this->cartManagement = $cartManagement;
+ $this->orderRepository = $orderRepository;
+ $this->eventManager = $eventManager;
+ $this->customerRepository = $customerRepository;
+ $this->guestPaymentInformationManagement = $guestPaymentInformationManagement;
+ $this->paymentInformationManagement = $paymentInformationManagement;
+ $this->quoteIdMaskFactory = $quoteIdMaskFactory;
+ }
+
+ /**
+ * Place order should be very small, all validations and updates should be done before calling this
+ * Onepage::METHOD_REGISTER should not be possible to get
+ *
+ * @param Quote $quote
+ * @return \Magento\Sales\Model\Order
+ * @throws \Magento\Framework\Exception\CouldNotSaveException
+ */
+ public function place($quote)
+ {
+ switch ($this->getCheckoutMethod($quote)) {
+ case Onepage::METHOD_GUEST:
+ $this->prepareGuestQuote($quote);
+ $quote->save(); // quoteRepository->save does stupid things...
+ $quoteIdMask = $this->quoteIdMaskFactory->create()->load($quote->getId(),'quote_id');
+ $maskedCartId = $quoteIdMask->getMaskedId();
+ $orderId = $this->guestPaymentInformationManagement->savePaymentInformationAndPlaceOrder(
+ $maskedCartId,
+ $quote->getCustomerEmail(),
+ $quote->getPayment()
+ );
+ break;
+ default:
+ $this->prepareCustomerQuote($quote);
+ $quote->save(); // quoteRepository->save does stupid things...
+ $orderId = $this->paymentInformationManagement->savePaymentInformationAndPlaceOrder(
+ $quote->getId(),
+ $quote->getPayment()
+ );
+ break;
+ }
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $this->orderRepository->get($orderId);
+
+ return $order;
+ }
+
+ /**
+ * Get quote checkout method
+ * No need to test for guest, as it's impossible to get to checkout if that's disallowed.
+ *
+ * @param Quote $quote
+ * @return string
+ */
+ private function getCheckoutMethod($quote)
+ {
+ if ($quote->getCustomerId()) {
+ $quote->setCheckoutMethod(Onepage::METHOD_CUSTOMER);
+ return $quote->getCheckoutMethod();
+ }
+ if (!$quote->getCheckoutMethod()) {
+ $quote->setCheckoutMethod(Onepage::METHOD_GUEST);
+ }
+
+ return $quote->getCheckoutMethod();
+ }
+
+ /**
+ * Prepare quote for guest checkout order submit
+ *
+ * @param Quote $quote
+ * @return $this
+ */
+ private function prepareGuestQuote($quote)
+ {
+ $quote->setCustomerId(null)
+ ->setCustomerEmail($quote->getBillingAddress()->getEmail())
+ ->setCustomerIsGuest(true)
+ ->setCustomerGroupId(\Magento\Customer\Api\Data\GroupInterface::NOT_LOGGED_IN_ID);
+ return $this;
+ }
+
+ /**
+ * Prepare quote for customer order submit
+ *
+ * @param Quote $quote
+ * @return void
+ * @SuppressWarnings(PHPMD.CyclomaticComplexity)
+ */
+ private function prepareCustomerQuote($quote)
+ {
+ $billing = $quote->getBillingAddress();
+ $shipping = $quote->isVirtual() ? null : $quote->getShippingAddress();
+
+ /** @var \Magento\Customer\Model\Customer $customer */
+ $customer = $this->customerRepository->getById($quote->getCustomerId());
+ $hasDefaultBilling = (bool)$customer->getDefaultBilling();
+ $hasDefaultShipping = (bool)$customer->getDefaultShipping();
+
+ if ($shipping && !$shipping->getSameAsBilling() &&
+ (!$shipping->getCustomerId() || $shipping->getSaveInAddressBook())
+ ) {
+ $shippingAddress = $shipping->exportCustomerAddress();
+ if (!$hasDefaultShipping) {
+ //Make provided address as default shipping address
+ $shippingAddress->setIsDefaultShipping(true);
+ $hasDefaultShipping = true;
+ }
+ $quote->addCustomerAddress($shippingAddress);
+ $shipping->setCustomerAddressData($shippingAddress);
+ }
+
+ if (!$billing->getCustomerId() || $billing->getSaveInAddressBook()) {
+ $billingAddress = $billing->exportCustomerAddress();
+ if (!$hasDefaultBilling) {
+ //Make provided address as default shipping address
+ if (!$hasDefaultShipping) {
+ //Make provided address as default shipping address
+ $billingAddress->setIsDefaultShipping(true);
+ }
+ $billingAddress->setIsDefaultBilling(true);
+ }
+ $quote->addCustomerAddress($billingAddress);
+ $billing->setCustomerAddressData($billingAddress);
+ }
+ }
+
+}
diff --git a/Model/Order/Total/Creditmemo/Fee.php b/Model/Order/Total/Creditmemo/Fee.php
new file mode 100644
index 0000000..e04bd4c
--- /dev/null
+++ b/Model/Order/Total/Creditmemo/Fee.php
@@ -0,0 +1,43 @@
+getOrder();
+ if ($order->getQlirooneFeeInvoiced() > 0) {
+ $feeAmount = $order->getQlirooneFeeInvoiced() - $order->getQlirooneFeeRefunded();
+ if ($feeAmount > 0) {
+ $basefeeAmount = $order->getBaseQlirooneFeeInvoiced() - $order->getBaseQlirooneFeeRefunded();
+
+ $feeAmountTax = $order->getQlirooneFeeTax();
+ $basefeeAmountTax = $order->getBaseQlirooneFeeTax();
+ $creditmemo->setQlirooneFee($feeAmount);
+ $creditmemo->setBaseQlirooneFee($basefeeAmount);
+
+ $creditmemo->setGrandTotal($creditmemo->getGrandTotal() + $feeAmount - $feeAmountTax);
+ $creditmemo->setBaseGrandTotal($creditmemo->getBaseGrandTotal() + $basefeeAmount - $basefeeAmountTax);
+ $order->setQlirooneFeeRefunded($order->getQlirooneFeeRefunded() + $feeAmount);
+ $order->setBaseQlirooneFeeRefunded($order->getBaseQlirooneFeeRefunded() + $basefeeAmount);
+ }
+ }
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Model/Order/Total/Creditmemo/Tax.php b/Model/Order/Total/Creditmemo/Tax.php
new file mode 100644
index 0000000..83eb8a7
--- /dev/null
+++ b/Model/Order/Total/Creditmemo/Tax.php
@@ -0,0 +1,35 @@
+getQlirooneFee() > 0) {
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $creditmemo->getOrder();
+
+ $feeAmountTax = $order->getQlirooneFeeTax();
+ $feeBaseAmountTax = $order->getBaseQlirooneFeeTax();
+ $creditmemo->setQlirooneFeeTax($feeAmountTax);
+ $creditmemo->setBaseQlirooneFeeTax($feeBaseAmountTax);
+
+// Logic tells me that this should be in, but adding this, will cause it to double the taxes...
+// $creditmemo->setTaxAmount($creditmemo->getTaxAmount() + $feeAmountTax);
+// $creditmemo->setBaseTaxAmount($creditmemo->getBaseTaxAmount() + $feeBaseAmountTax);
+ }
+
+ return $this;
+ }
+}
diff --git a/Model/Order/Total/Invoice/Fee.php b/Model/Order/Total/Invoice/Fee.php
new file mode 100644
index 0000000..5e4850f
--- /dev/null
+++ b/Model/Order/Total/Invoice/Fee.php
@@ -0,0 +1,40 @@
+getOrder();
+ if (!$order->getQlirooneFeeInvoiced()) {
+ $feeAmount = $order->getQlirooneFee();
+ $feeAmountTax = $order->getQlirooneFeeTax();
+ $basefeeAmount = $order->getBaseQlirooneFee();
+ $basefeeAmountTax = $order->getBaseQlirooneFeeTax();
+ $invoice->setQlirooneFee($feeAmount);
+ $invoice->setBaseQlirooneFee($basefeeAmount);
+
+ $invoice->setGrandTotal($invoice->getGrandTotal() + $feeAmount - $feeAmountTax);
+ $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() + $basefeeAmount - $basefeeAmountTax);
+ $order->setQlirooneFeeInvoiced($feeAmount);
+ $order->setBaseQlirooneFeeInvoiced($basefeeAmount);
+ }
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Model/Order/Total/Invoice/Tax.php b/Model/Order/Total/Invoice/Tax.php
new file mode 100644
index 0000000..e21fa56
--- /dev/null
+++ b/Model/Order/Total/Invoice/Tax.php
@@ -0,0 +1,36 @@
+getQlirooneFee() > 0) {
+ /** @var \Magento\Sales\Model\Order $order */
+ $order = $invoice->getOrder();
+
+ $feeAmountTax = $order->getQlirooneFeeTax();
+ $feeBaseAmountTax = $order->getBaseQlirooneFeeTax();
+ $invoice->setQlirooneFeeTax($feeAmountTax);
+ $invoice->setBaseQlirooneFeeTax($feeBaseAmountTax);
+
+ $invoice->setGrandTotal($invoice->getGrandTotal() + $feeAmountTax);
+ $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() + $feeBaseAmountTax);
+ $invoice->setTaxAmount($invoice->getTaxAmount() + $feeAmountTax);
+ $invoice->setBaseTaxAmount($invoice->getBaseTaxAmount() + $feeBaseAmountTax);
+ }
+
+ return $this;
+ }
+}
diff --git a/Model/OrderManagementStatus.php b/Model/OrderManagementStatus.php
new file mode 100644
index 0000000..fa64de7
--- /dev/null
+++ b/Model/OrderManagementStatus.php
@@ -0,0 +1,171 @@
+_init(ResourceModel\OrderManagementStatus::class);
+ }
+
+ /**
+ * @return string
+ */
+ /**
+ * @inheritdoc
+ */
+ public function getId()
+ {
+ return $this->getData(self::FIELD_ID);
+ }
+
+ /**
+ * @return string
+ */
+ public function getDate()
+ {
+ return $this->getData(self::FIELD_DATE);
+ }
+
+ /**
+ * @return int
+ */
+ public function getTransactionId()
+ {
+ return $this->getData(self::FIELD_TRANSACTION_ID);
+ }
+
+ /**
+ * One of the defined record types declared above
+ *
+ * @return string
+ */
+ public function getRecordType()
+ {
+ return $this->getData(self::FIELD_RECORD_TYPE);
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getRecordId()
+ {
+ return $this->getData(self::FIELD_RECORD_ID);
+ }
+
+ /**
+ * @return string
+ */
+ public function getTransactionStatus()
+ {
+ return $this->getData(self::FIELD_TRANSACTION_STATUS);
+ }
+
+ /**
+ * @return string
+ */
+ public function getNotificationStatus()
+ {
+ return $this->getData(self::FIELD_NOTIFICATION_STATUS);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getMessage()
+ {
+ return $this->getData(self::FIELD_MESSAGE);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getQliroOrderId()
+ {
+ return $this->getData(self::FIELD_QLIRO_ORDER_ID);
+ }
+
+ /**
+ * @var string $value
+ * @return $this
+ */
+ public function setDate($value)
+ {
+ return $this->setData(self::FIELD_DATE, $value);
+ }
+
+ /**
+ * @var int $value
+ * @return $this
+ */
+ public function setTransactionId($value)
+ {
+ return $this->setData(self::FIELD_TRANSACTION_ID, $value);
+ }
+
+ /**
+ * @var string $value
+ * @return $this
+ */
+ public function setRecordType($value)
+ {
+ return $this->setData(self::FIELD_RECORD_TYPE, $value);
+ }
+
+ /**
+ * @var int $value
+ * @return $this
+ */
+ public function setRecordId($value)
+ {
+ return $this->setData(self::FIELD_RECORD_ID, $value);
+ }
+
+ /**
+ * @var string $value
+ * @return $this
+ */
+ public function setTransactionStatus($value)
+ {
+ return $this->setData(self::FIELD_TRANSACTION_STATUS, $value);
+ }
+
+ /**
+ * @var string $value
+ * @return $this
+ */
+ public function setNotificationStatus($value)
+ {
+ return $this->setData(self::FIELD_NOTIFICATION_STATUS, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setMessage($value)
+ {
+ return $this->setData(self::FIELD_MESSAGE, $value);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setQliroOrderId($value)
+ {
+ return $this->setData(self::FIELD_QLIRO_ORDER_ID, $value);
+ }
+}
diff --git a/Model/OrderManagementStatus/Repository.php b/Model/OrderManagementStatus/Repository.php
new file mode 100644
index 0000000..055ed97
--- /dev/null
+++ b/Model/OrderManagementStatus/Repository.php
@@ -0,0 +1,278 @@
+OrderManagementStatusResourceModel = $OrderManagementStatusResourceModel;
+ $this->OrderManagementStatusFactory = $OrderManagementStatusFactory;
+ $this->searchResultFactory = $searchResultFactory;
+ $this->collectionFactory = $collectionFactory;
+ $this->searchCriteriaBuilder = $searchCriteriaBuilder;
+ $this->sortOrderBuilder = $sortOrderBuilder;
+ }
+
+ /**
+ * Save a OrderManagementStatus
+ *
+ * @param \Qliro\QliroOne\Api\Data\OrderManagementStatusInterface $OrderManagementStatus
+ * @return \Qliro\QliroOne\Api\Data\OrderManagementStatusInterface
+ * @throws \Magento\Framework\Exception\AlreadyExistsException
+ */
+ public function save(OrderManagementStatusInterface $OrderManagementStatus)
+ {
+ $this->OrderManagementStatusResourceModel->save($OrderManagementStatus);
+
+ return $OrderManagementStatus;
+ }
+
+ /**
+ * Get a OrderManagementStatus by its ID
+ *
+ * @inheritdoc
+ */
+ public function get($id)
+ {
+ return $this->getByField($id, null);
+ }
+
+ /**
+ * Get parent by its transaction id. They might reuse their transaction ids, so I find the newest one
+ *
+ * @param int $id
+ * @return \Qliro\QliroOne\Api\Data\OrderManagementStatusInterface|null
+ */
+ public function getParent($id)
+ {
+ /** @var \Magento\Framework\Api\SortOrder $sortOrder */
+ $sortOrder = $this->sortOrderBuilder->setField('date')->setDirection(SortOrder::SORT_DESC)->create();
+
+ /** @var \Magento\Framework\Api\SearchCriteria $search */
+ $search = $this->searchCriteriaBuilder
+ ->addFilter('transaction_id',$id, 'eq')
+ ->addFilter('record_type', 'null', 'neq')
+ ->addSortOrder($sortOrder)
+ ->create();
+
+ $searchResult = $this->getList($search);
+ foreach ($searchResult->getItems() as $parent) {
+ return $parent;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get last transaction received of this transaction id, that was successfully handled
+ *
+ * @param int $id
+ * @return \Qliro\QliroOne\Api\Data\OrderManagementStatusInterface|null
+ */
+ public function getPrevious($id)
+ {
+ /** @var \Magento\Framework\Api\SortOrder $sortOrder */
+ $sortOrder = $this->sortOrderBuilder->setField('date')->setDirection(SortOrder::SORT_DESC)->create();
+
+ /** @var \Magento\Framework\Api\SearchCriteria $search */
+ $search = $this->searchCriteriaBuilder
+ ->addFilter('transaction_id',$id, 'eq')
+ ->addFilter('notification_status',OrderManagementStatusInterface::NOTIFICATION_STATUS_DONE, 'eq')
+ ->addSortOrder($sortOrder)
+ ->create();
+
+ $searchResult = $this->getList($search);
+ foreach ($searchResult->getItems() as $previous) {
+ return $previous;
+ }
+
+ return null;
+ }
+
+ /**
+ * Delete a OrderManagementStatus
+ *
+ * @param \Qliro\QliroOne\Api\Data\OrderManagementStatusInterface $OrderManagementStatus
+ * @return \Qliro\QliroOne\Model\ResourceModel\OrderManagementStatus
+ * @throws \Exception
+ */
+ public function delete(OrderManagementStatusInterface $OrderManagementStatus)
+ {
+ return $this->OrderManagementStatusResourceModel->delete($OrderManagementStatus);
+ }
+
+ /**
+ * Get a result of search among OrderManagementStatuss by given search criteria
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @return \Qliro\QliroOne\Api\OrderManagementStatusSearchResultInterface
+ */
+ public function getList(SearchCriteriaInterface $searchCriteria)
+ {
+ /** @var \Qliro\QliroOne\Model\ResourceModel\OrderManagementStatus\Collection $collection */
+ $collection = $this->collectionFactory->create();
+
+ $this->addFiltersToCollection($searchCriteria, $collection);
+ $this->addSortOrdersToCollection($searchCriteria, $collection);
+ $this->addPaginationToCollection($searchCriteria, $collection);
+
+ $collection->load();
+
+ return $this->buildSearchResult($searchCriteria, $collection);
+ }
+
+ /**
+ * Add filters to collection
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\OrderManagementStatus\Collection $collection
+ */
+ private function addFiltersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ foreach ($searchCriteria->getFilterGroups() as $filterGroup) {
+ $fields = $conditions = [];
+ foreach ($filterGroup->getFilters() as $filter) {
+ $fields[] = $filter->getField();
+ $conditions[] = [$filter->getConditionType() => $filter->getValue()];
+ }
+ $collection->addFieldToFilter($fields, $conditions);
+ }
+ }
+
+ /**
+ * Add sort order to collection
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\OrderManagementStatus\Collection $collection
+ */
+ private function addSortOrdersToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ foreach ((array) $searchCriteria->getSortOrders() as $sortOrder) {
+ $direction = $sortOrder->getDirection() == SortOrder::SORT_ASC ? 'asc' : 'desc';
+ $collection->addOrder($sortOrder->getField(), $direction);
+ }
+ }
+
+ /**
+ * Add pagination to collection
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\OrderManagementStatus\Collection $collection
+ */
+ private function addPaginationToCollection(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ $collection->setPageSize($searchCriteria->getPageSize());
+ $collection->setCurPage($searchCriteria->getCurrentPage());
+ }
+
+ /**
+ * Build search result
+ *
+ * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
+ * @param \Qliro\QliroOne\Model\ResourceModel\OrderManagementStatus\Collection $collection
+ * @return \Qliro\QliroOne\Api\OrderManagementStatusSearchResultInterface
+ */
+ private function buildSearchResult(SearchCriteriaInterface $searchCriteria, Collection $collection)
+ {
+ /** @var \Qliro\QliroOne\Api\OrderManagementStatusSearchResultInterface $searchResults */
+ $searchResults = $this->searchResultFactory->create();
+
+ $searchResults->setSearchCriteria($searchCriteria);
+ $searchResults->setItems($collection->getItems());
+ $searchResults->setTotalCount($collection->getSize());
+
+ return $searchResults;
+ }
+
+ /**
+ * Get a OrderManagementStatus by a specified field
+ *
+ * @param string|int $value
+ * @param string $field
+ * @return \Qliro\QliroOne\Model\OrderManagementStatus
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ private function getByField($value, $field)
+ {
+ /** @var \Qliro\QliroOne\Model\OrderManagementStatus $OrderManagementStatus */
+ $OrderManagementStatus = $this->OrderManagementStatusFactory->create();
+ $this->OrderManagementStatusResourceModel->load($OrderManagementStatus, $value, $field);
+
+ if (!$OrderManagementStatus->getId()) {
+ throw new NoSuchEntityException(__('Cannot find a OrderManagementStatus with %1 = "%2"', $field, $value));
+ }
+
+ return $OrderManagementStatus;
+ }
+}
diff --git a/Model/OrderManagementStatus/SearchResult.php b/Model/OrderManagementStatus/SearchResult.php
new file mode 100644
index 0000000..432917b
--- /dev/null
+++ b/Model/OrderManagementStatus/SearchResult.php
@@ -0,0 +1,20 @@
+logManager = $logManager;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleSuccess($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->log($qliroOrderManagementStatus, $omStatus);
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleCancelled($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->log($qliroOrderManagementStatus, $omStatus);
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleError($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->log($qliroOrderManagementStatus, $omStatus);
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleInProcess($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->log($qliroOrderManagementStatus, $omStatus);
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleOnHold($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->log($qliroOrderManagementStatus, $omStatus);
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleUserInteraction($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->log($qliroOrderManagementStatus, $omStatus);
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleCreated($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->log($qliroOrderManagementStatus, $omStatus);
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ private function log($qliroOrderManagementStatus, $omStatus)
+ {
+ $merchantReference = $qliroOrderManagementStatus->getMerchantReference();
+ $this->logManager->setMerchantReference($merchantReference);
+
+ $logData = [
+ 'status' => $qliroOrderManagementStatus->getStatus(),
+ 'qliro_order_id' => $qliroOrderManagementStatus->getOrderId(),
+ 'transaction_id' => $omStatus->getTransactionId(),
+ 'transaction_status' => $omStatus->getTransactionStatus(),
+ 'record_type' => $omStatus->getRecordType(),
+ 'record_id' => $omStatus->getRecordId(),
+ ];
+
+ $this->logManager->info('Order cancellation transaction changed status', ['extra' => $logData]);
+ }
+}
diff --git a/Model/OrderManagementStatus/Update/Handler/Payment.php b/Model/OrderManagementStatus/Update/Handler/Payment.php
new file mode 100644
index 0000000..af677e8
--- /dev/null
+++ b/Model/OrderManagementStatus/Update/Handler/Payment.php
@@ -0,0 +1,252 @@
+paymentRepository = $paymentRepository;
+ $this->paymentTransactionRepository = $paymentTransactionRepository;
+ $this->orderRepository = $orderRepository;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleSuccess($qliroOrderManagementStatus, $omStatus)
+ {
+ $payment = $this->getPayment($omStatus);
+ $order = $payment->getOrder();
+
+ /*
+ * Update Order
+ */
+ try {
+ if ($order->getState() == Order::STATE_HOLDED) {
+ $order->unhold();
+ }
+
+ $formattedPrice = $order->getBaseCurrency()->formatTxt(
+ $qliroOrderManagementStatus->getAmount()
+ );
+
+ $order->addStatusHistoryComment(__('Capture of %1 confirmed successful', $formattedPrice));
+
+ $this->orderRepository->save($order);
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $qliroOrderManagementStatus->getOrderId(),
+ 'payment_id' => $payment->getId(),
+ ],
+ ]
+ );
+ throw new TerminalException('Could not handle Invoice Success', null, $exception);
+ }
+
+ /*
+ * Update Payment Transaction
+ */
+ try {
+ /** @var \Magento\Sales\Model\Order\Payment\Transaction $paymentTransaction */
+ $paymentTransaction = $this->getPaymentTransaction(
+ $qliroOrderManagementStatus->getPaymentTransactionId(),
+ $payment->getId(),
+ $order->getId()
+ );
+
+ $paymentTransaction->setAdditionalInformation(
+ 'provider_result_description',
+ $qliroOrderManagementStatus->getProviderResultDescription()
+ );
+ $paymentTransaction->setAdditionalInformation(
+ 'provider_result_code',
+ $qliroOrderManagementStatus->getProviderResultCode()
+ );
+ $paymentTransaction->setAdditionalInformation(
+ 'provider_transaction_id',
+ $qliroOrderManagementStatus->getProviderTransactionId()
+ );
+ $paymentTransaction->setAdditionalInformation(
+ 'payment_reference',
+ $qliroOrderManagementStatus->getPaymentReference()
+ );
+
+ $this->paymentTransactionRepository->save($paymentTransaction);
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $qliroOrderManagementStatus->getOrderId(),
+ 'payment_id' => $payment->getId(),
+ ],
+ ]
+ );
+ // Silent, since this code is not required, just nice to haves
+ }
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleCancelled($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'Cancelled');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleError($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'Error');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleInProcess($qliroOrderManagementStatus, $omStatus)
+ {
+ // Nothing to do
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleOnHold($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'OnHold');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleUserInteraction($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'UserInteraction');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleCreated($qliroOrderManagementStatus, $omStatus)
+ {
+ // Nothing to do
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @return \Magento\Sales\Model\Order\Payment $payment
+ */
+ private function getPayment($omStatus)
+ {
+ $payment = $this->paymentRepository->get($omStatus->getRecordId());
+
+ return $payment;
+ }
+
+ /**
+ * Get payment transaction with the same transaction number as was part of this notification
+ *
+ * @param int $transactionId
+ * @param int $paymentId
+ * @param int $orderId
+ * @return \Magento\Sales\Model\Order\Payment $payment
+ */
+ private function getPaymentTransaction($transactionId, $paymentId, $orderId)
+ {
+ $paymentTransaction = $this->paymentTransactionRepository->getByTransactionId(
+ $transactionId,
+ $paymentId,
+ $orderId
+ );
+
+ return $paymentTransaction;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @param string $contextMessage
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ private function setOnHold($qliroOrderManagementStatus, $omStatus, $contextMessage)
+ {
+ try {
+ $payment = $this->getPayment($omStatus);
+ $order = $payment->getOrder();
+ $order->hold();
+ $order->addStatusHistoryComment(
+ __('Order set on hold because Qliro One reported an error with the capture: %1', $contextMessage)
+ );
+ $this->orderRepository->save($order);
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $qliroOrderManagementStatus->getOrderId(),
+ ],
+ ]
+ );
+
+ throw new TerminalException($exception->getMessage(), $exception->getCode(), $exception);
+ }
+ }
+}
diff --git a/Model/OrderManagementStatus/Update/Handler/Shipment.php b/Model/OrderManagementStatus/Update/Handler/Shipment.php
new file mode 100644
index 0000000..e4773eb
--- /dev/null
+++ b/Model/OrderManagementStatus/Update/Handler/Shipment.php
@@ -0,0 +1,255 @@
+shipmentRepository = $shipmentRepository;
+ $this->orderRepository = $orderRepository;
+ $this->invoiceRepository = $invoiceRepository;
+ $this->transactionBuilder = $transactionBuilder;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleSuccess($qliroOrderManagementStatus, $omStatus)
+ {
+
+ try {
+ $shipment = $this->getShipment($omStatus);
+ $order = $shipment->getOrder();
+ $payment = $order->getPayment();
+
+ /*
+ * Update Order
+ */
+ if ($order->getState() == Order::STATE_HOLDED) {
+ $order->unhold();
+ $this->orderRepository->save($order);
+ }
+
+ /*
+ * Create Invoice
+ */
+ $invoiceItems = [];
+ $shipmentItems = $shipment->getAllItems();
+
+ /** @var \Magento\Sales\Model\Order\Shipment\Item $shipmentItem */
+ foreach ($shipmentItems as $shipmentItem) {
+ $qty = (int)$shipmentItem->getQty();
+
+ /** @var \Magento\Sales\Model\Order\Item $item */
+ $item = $order->getItemById($shipmentItem->getOrderItemId());
+
+ /*
+ * This is the same test for invoice made earlier, as seen in:
+ * \Qliro\QliroOne\Model\QliroOrder\Admin\Builder\ShipmentOrderItemsBuilder::create
+ */
+ if ($item->getQtyInvoiced() > 0) {
+ $remaining = $item->getQtyOrdered() - $item->getQtyInvoiced();
+ if ($remaining < $qty) {
+ $qty = $remaining;
+ }
+ }
+
+ $invoiceItems[$shipmentItem->getOrderItemId()] = $qty;
+ }
+
+ /*
+ * Capture online is selected, to make use of all the functions that it runs (payment
+ * transactions etc). "qliro_skip_actual_capture" is set to avoid doing the capture
+ * inside, since it was already done by the shipment
+ */
+ if ($order->canInvoice()) {
+ $invoice = $order->prepareInvoice($invoiceItems);
+ $invoice->setRequestedCaptureCase(Invoice::CAPTURE_ONLINE);
+ $payment->setTransactionId($qliroOrderManagementStatus->getPaymentTransactionId());
+ $payment->setData(\Qliro\QliroOne\Model\Management::QLIRO_SKIP_ACTUAL_CAPTURE, 1);
+ $invoice->register()->pay();
+ $this->invoiceRepository->save($invoice);
+ } else {
+ throw new \Magento\Framework\Exception\LocalizedException(
+ __('Order does not allow to capture')
+ );
+ }
+
+ $formattedPrice = $order->getBaseCurrency()->formatTxt(
+ $qliroOrderManagementStatus->getAmount()
+ );
+
+ $order->addStatusHistoryComment(__('Capture of %1 confirmed successful', $formattedPrice));
+
+ $this->orderRepository->save($order);
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $qliroOrderManagementStatus->getOrderId(),
+ 'shipment_id' => isset($shipment) ? $shipment->getId() : null,
+ ],
+ ]
+ );
+ throw new TerminalException('Could not handle Shipment Success', null, $exception);
+ }
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleCancelled($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'Cancelled');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleError($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'Error');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleInProcess($qliroOrderManagementStatus, $omStatus)
+ {
+ // Nothing to do
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleOnHold($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'OnHold');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ public function handleUserInteraction($qliroOrderManagementStatus, $omStatus)
+ {
+ $this->setOnHold($qliroOrderManagementStatus, $omStatus, 'UserInteraction');
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ */
+ public function handleCreated($qliroOrderManagementStatus, $omStatus)
+ {
+ // Nothing to do
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @return \Magento\Sales\Model\Order\Shipment $shipment
+ */
+ private function getShipment($omStatus)
+ {
+ $shipment = $this->shipmentRepository->get($omStatus->getRecordId());
+
+ return $shipment;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @param string $contextMessage
+ * @throws \Qliro\QliroOne\Model\Exception\TerminalException
+ */
+ private function setOnHold($qliroOrderManagementStatus, $omStatus, $contextMessage)
+ {
+ try {
+ $shipment = $this->getShipment($omStatus);
+ $order = $shipment->getOrder();
+ $order->hold();
+
+ $order->addStatusHistoryComment(
+ __('Order set on hold because Qliro One reported an error with the capture: %1', $contextMessage)
+ );
+
+ $this->orderRepository->save($order);
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_order_id' => $qliroOrderManagementStatus->getOrderId(),
+ ],
+ ]
+ );
+
+ throw new TerminalException($exception->getMessage(), $exception->getCode(), $exception);
+ }
+ }
+}
diff --git a/Model/OrderManagementStatus/Update/HandlerPool.php b/Model/OrderManagementStatus/Update/HandlerPool.php
new file mode 100644
index 0000000..edb3f08
--- /dev/null
+++ b/Model/OrderManagementStatus/Update/HandlerPool.php
@@ -0,0 +1,96 @@
+ 'handleSuccess',
+ QliroOrderManagementStatusInterface::STATUS_CANCELLED => 'handleCancelled',
+ QliroOrderManagementStatusInterface::STATUS_ERROR => 'handleError',
+ QliroOrderManagementStatusInterface::STATUS_INPROCESS => 'handleInProcess',
+ QliroOrderManagementStatusInterface::STATUS_ONHOLD => 'handleOnHold',
+ QliroOrderManagementStatusInterface::STATUS_USER_INTERACTION => 'handleUserInteraction',
+ QliroOrderManagementStatusInterface::STATUS_CREATED => 'handleCreated',
+ ];
+
+ /**
+ * HandlerPool constructor.
+ *
+ * @param array $handlerPool
+ * @param \Qliro\QliroOne\Model\Logger\Manager $logManager
+ */
+ public function __construct(
+ LogManager $logManager,
+ $handlerPool = []
+ ) {
+ $this->handlerPool = $handlerPool;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * If a handler is found, figure out what status it is and call the selected handler for it
+ * Returns true if it was handled, otherwise it returns false
+ *
+ * @param \Qliro\QliroOne\Model\Notification\QliroOrderManagementStatus $qliroOrderManagementStatus
+ * @param \Qliro\QliroOne\Model\OrderManagementStatus $omStatus
+ * @return bool
+ */
+ public function handle($qliroOrderManagementStatus, $omStatus)
+ {
+ try {
+ $type = $omStatus->getRecordType();
+ // Null means it used to throw an exception and log it. Type is always null initially, no point in logging
+ if ($type === null) {
+ return false;
+ }
+ $handler = $this->handlerPool[$type] ?? null;
+ if ($handler instanceof OrderManagementStatusUpdateHandlerInterface) {
+ $handlerFunction = $this->handlerStatusMap[$qliroOrderManagementStatus->getStatus()];
+ if ($handlerFunction) {
+ $handler->$handlerFunction($qliroOrderManagementStatus, $omStatus);
+ } else {
+ throw new \LogicException('No status function for OrderManagementStatus handler available');
+ }
+ } else {
+ throw new \LogicException('No Handler for OrderManagementStatus available');
+ }
+
+ } catch (\Exception $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'type' => $type,
+ 'status' => $qliroOrderManagementStatus->getStatus(),
+ ],
+ ]
+ );
+
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Model/Product/ProductPool.php b/Model/Product/ProductPool.php
new file mode 100644
index 0000000..2a4dcec
--- /dev/null
+++ b/Model/Product/ProductPool.php
@@ -0,0 +1,53 @@
+productRepository = $productRepository;
+ }
+
+ /**
+ * Get a product by SKU
+ *
+ * @param string $sku
+ * @param int|null $storeId
+ * @return \Magento\Catalog\Model\Product
+ * @throws \Magento\Framework\Exception\NoSuchEntityException
+ */
+ public function getProduct($sku, $storeId = null)
+ {
+ if (!isset($this->products[$sku])) {
+ $this->products[$sku] = $this->productRepository->get($sku, false, $storeId);
+ }
+
+ return $this->products[$sku];
+ }
+}
diff --git a/Model/Product/Type/Handler/ConfigurableHandler.php b/Model/Product/Type/Handler/ConfigurableHandler.php
new file mode 100644
index 0000000..eb28f6f
--- /dev/null
+++ b/Model/Product/Type/Handler/ConfigurableHandler.php
@@ -0,0 +1,53 @@
+getParent();
+
+ return $taxIncluded ? $parent->getPriceInclTax() : $parent->getPriceExclTax();
+ }
+
+ /**
+ * Prepare QliroOne order item's quantity
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @return int
+ */
+ public function prepareQuantity(TypeSourceItemInterface $item)
+ {
+ $parent = $item->getParent();
+
+ return $parent->getQty();
+ }
+
+ /**
+ * Prepare QliroOne order item's description
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @return string
+ */
+ public function prepareDescription(TypeSourceItemInterface $item)
+ {
+ return $item->getName();
+ }
+}
diff --git a/Model/Product/Type/Handler/DefaultHandler.php b/Model/Product/Type/Handler/DefaultHandler.php
new file mode 100644
index 0000000..5bdb516
--- /dev/null
+++ b/Model/Product/Type/Handler/DefaultHandler.php
@@ -0,0 +1,159 @@
+qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->typeResolver = $typeResolver;
+ $this->productPool = $productPool;
+ $this->qliroHelper = $qliroHelper;
+ }
+
+ /**
+ * Get QliroOne order item out of a source item, or null if not applicable
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface|null
+ */
+ public function getQliroOrderItem(TypeSourceItemInterface $item)
+ {
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem */
+ $qliroOrderItem = $this->qliroOrderItemFactory->create();
+
+ $pricePerItemIncVat = $this->preparePrice($item, true);
+ $pricePerItemExVat = $this->preparePrice($item, false);
+
+ $qliroOrderItem->setMerchantReference($this->prepareMerchantReference($item));
+ $qliroOrderItem->setType(QliroOrderItemInterface::TYPE_PRODUCT);
+ $qliroOrderItem->setQuantity($this->prepareQuantity($item));
+ $qliroOrderItem->setPricePerItemIncVat($this->qliroHelper->formatPrice($pricePerItemIncVat));
+ $qliroOrderItem->setPricePerItemExVat($this->qliroHelper->formatPrice($pricePerItemExVat));
+ $qliroOrderItem->setDescription($this->prepareDescription($item));
+ $qliroOrderItem->setMetaData($this->prepareMetaData($item));
+
+ return $qliroOrderItem;
+ }
+
+ /**
+ * Get a reference to source item out of QliroOne order item, or null if not applicable
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceProviderInterface $typeSourceProvider
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface|null
+ */
+ public function getItem(QliroOrderItemInterface $qliroOrderItem, TypeSourceProviderInterface $typeSourceProvider)
+ {
+ if ($qliroOrderItem->getType() !== QliroOrderItemInterface::TYPE_PRODUCT) {
+ return null;
+ }
+
+ return $typeSourceProvider->getSourceItemByMerchantReference($qliroOrderItem->getMerchantReference());
+ }
+
+ /**
+ * Prepare QliroOne order item's merchant reference
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @return string
+ */
+ public function prepareMerchantReference(TypeSourceItemInterface $item)
+ {
+ return sprintf('%s:%s', $item->getId(), $item->getSku());
+ }
+
+ /**
+ * Prepare QliroOne order item's price
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @param bool $taxIncluded
+ * @return float
+ */
+ public function preparePrice(TypeSourceItemInterface $item, $taxIncluded = true)
+ {
+ return $taxIncluded ? $item->getPriceInclTax() : $item->getPriceExclTax();
+ }
+
+ /**
+ * Prepare QliroOne order item's quantity
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @return int
+ */
+ public function prepareQuantity(TypeSourceItemInterface $item)
+ {
+ return $item->getQty();
+ }
+
+ /**
+ * Prepare QliroOne order item's description
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @return string
+ */
+ public function prepareDescription(TypeSourceItemInterface $item)
+ {
+ return $item->getName();
+ }
+
+ /**
+ * Prepare QliroOne order item's metadata
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $item
+ * @return array|null
+ */
+ public function prepareMetaData(TypeSourceItemInterface $item)
+ {
+ return null;
+ }
+}
diff --git a/Model/Product/Type/OrderSourceProvider.php b/Model/Product/Type/OrderSourceProvider.php
new file mode 100644
index 0000000..ca13d3a
--- /dev/null
+++ b/Model/Product/Type/OrderSourceProvider.php
@@ -0,0 +1,153 @@
+productPool = $productPool;
+ $this->typeSourceItemFactory = $typeSourceItemFactory;
+ }
+
+ /**
+ * @return int
+ */
+ public function getStoreId()
+ {
+ return $this->getStoreId();
+ }
+
+ /**
+ * @param string $reference
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface
+ */
+ public function getSourceItemByMerchantReference($reference)
+ {
+ if (strpos($reference, ':') !== false) {
+ list($quoteItemId, $sku) = explode(':', $reference);
+ } else {
+ $quoteItemId = null;
+ $sku = $reference;
+ }
+
+ try {
+ $orderItem = $this->order->getItemByQuoteItemId($quoteItemId);
+
+ if (!$orderItem) {
+ if ($sku) {
+ $product = $this->productPool->getProduct($sku, $this->getStoreId());
+
+ $orderItem = $this->order->getItemById($product);
+ } else {
+ $orderItem = null;
+ }
+ }
+
+ if ($orderItem) {
+ return $this->generateSourceItem($orderItem, $orderItem->getQty());
+ }
+
+ return null;
+ } catch (\Exception $exception) {
+ return null;
+ }
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface[]
+ */
+ public function getSourceItems()
+ {
+ $result = [];
+
+ /** @var \Magento\Sales\Model\Order\Item $item */
+ foreach ($this->order->getAllVisibleItems() as $item) {
+ $result[] = $this->generateSourceItem($item, $item->getQtyOrdered());
+ }
+
+ return $result;
+ }
+
+ /**
+ * Set order
+ *
+ * @param \Magento\Sales\Model\Order $order
+ */
+ public function setOrder($order)
+ {
+ $this->order = $order;
+ }
+
+ /**
+ * @param \Magento\Sales\Model\Order\Item $item
+ * @param float $quantity
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface
+ */
+ public function generateSourceItem($item, $quantity)
+ {
+ if (!isset($this->sourceItems[$item->getQuoteItemId()])) {
+ /** @var \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $sourceItem */
+ $sourceItem = $this->typeSourceItemFactory->create();
+
+ $sourceItem->setId($item->getQuoteItemId());
+ $sourceItem->setName($item->getName());
+ $sourceItem->setPriceInclTax($item->getRowTotalInclTax() / $quantity); // $item->getPriceInclTax()
+ $sourceItem->setPriceExclTax($item->getRowTotal() / $quantity); // $item->getPrice()
+ $sourceItem->setQty($item->getQtyOrdered());
+ $sourceItem->setSku($item->getSku());
+ $sourceItem->setType($item->getProductType());
+ $sourceItem->setProduct($item->getProduct());
+ $sourceItem->setItem($item);
+
+ $this->sourceItems[$item->getQuoteItemId()] = $sourceItem;
+
+ if ($parentItem = $item->getParentItem()) {
+ $sourceItem->setParent($this->generateSourceItem($parentItem, $quantity));
+ }
+ }
+
+ return $this->sourceItems[$item->getQuoteItemId()];
+ }
+}
diff --git a/Model/Product/Type/QuoteSourceProvider.php b/Model/Product/Type/QuoteSourceProvider.php
new file mode 100644
index 0000000..4e9213d
--- /dev/null
+++ b/Model/Product/Type/QuoteSourceProvider.php
@@ -0,0 +1,155 @@
+productPool = $productPool;
+ $this->typeSourceItemFactory = $typeSourceItemFactory;
+ }
+
+ /**
+ * @return int
+ */
+ public function getStoreId()
+ {
+ return $this->getStoreId();
+ }
+
+ /**
+ * @param string $reference
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface
+ */
+ public function getSourceItemByMerchantReference($reference)
+ {
+ if (strpos($reference, ':') !== false) {
+ list($quoteItemId, $sku) = explode(':', $reference);
+ } else {
+ $quoteItemId = null;
+ $sku = $reference;
+ }
+
+ try {
+ $quoteItem = $this->quote->getItemById($quoteItemId);
+
+ if (!$quoteItem) {
+ if ($sku) {
+ $product = $this->productPool->getProduct($sku, $this->getStoreId());
+
+ $quoteItem = $this->quote->getItemByProduct($product);
+ } else {
+ $quoteItem = null;
+ }
+ }
+
+ if ($quoteItem) {
+ // Basically, at this point we do not update quote items
+
+ return $this->generateSourceItem($quoteItem, $quoteItem->getQty());
+ }
+
+ return null;
+ } catch (\Exception $exception) {
+ return null;
+ }
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface[]
+ */
+ public function getSourceItems()
+ {
+ $result = [];
+
+ /** @var \Magento\Quote\Model\Quote\Item $item */
+ foreach ($this->quote->getAllVisibleItems() as $item) {
+ $result[] = $this->generateSourceItem($item, $item->getQty());
+ }
+
+ return $result;
+ }
+
+ /**
+ * Set quote
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ */
+ public function setQuote($quote)
+ {
+ $this->quote = $quote;
+ }
+
+ /**
+ * @param \Magento\Quote\Model\Quote\Item $item
+ * @param float $quantity
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface
+ */
+ public function generateSourceItem($item, $quantity)
+ {
+ if (!isset($this->sourceItems[$item->getItemId()])) {
+ /** @var \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $sourceItem */
+ $sourceItem = $this->typeSourceItemFactory->create();
+
+ $sourceItem->setId($item->getItemId());
+ $sourceItem->setName($item->getName());
+ $sourceItem->setPriceInclTax($item->getRowTotalInclTax() / $quantity); // $item->getPriceInclTax()
+ $sourceItem->setPriceExclTax($item->getRowTotal() / $quantity); // $item->getPrice()
+ $sourceItem->setQty($item->getQty());
+ $sourceItem->setSku($item->getSku());
+ $sourceItem->setType($item->getProductType());
+ $sourceItem->setProduct($item->getProduct());
+ $sourceItem->setItem($item);
+
+ $this->sourceItems[$item->getItemId()] = $sourceItem;
+
+ if ($parentItem = $item->getParentItem()) {
+ $sourceItem->setParent($this->generateSourceItem($parentItem, $quantity));
+ }
+ }
+
+ return $this->sourceItems[$item->getItemId()];
+ }
+}
diff --git a/Model/Product/Type/TypePoolHandler.php b/Model/Product/Type/TypePoolHandler.php
new file mode 100644
index 0000000..8a18318
--- /dev/null
+++ b/Model/Product/Type/TypePoolHandler.php
@@ -0,0 +1,100 @@
+pool = $pool;
+ $this->typeResolver = $typeResolver;
+ }
+
+ /**
+ * Resolve a QliroOne order item out of given source item
+ *
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $sourceItem
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceProviderInterface $typeSourceProvider
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface|null
+ */
+ public function resolveQliroOrderItem(
+ TypeSourceItemInterface $sourceItem,
+ TypeSourceProviderInterface $typeSourceProvider
+ ) {
+ $typeHash = [$sourceItem->getProduct()->getTypeId()];
+
+ if ($parentItem = $sourceItem->getParent()) {
+ $typeHash[] = $parentItem->getProduct()->getTypeId();
+ }
+
+ $handler = $this->resolveHandler(implode(':', $typeHash));
+
+ if ($handler) {
+ return $handler->getQliroOrderItem($sourceItem);
+ }
+
+ return null;
+ }
+
+ /**
+ * Resolve a source item out of given QliroOne order item
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceProviderInterface $typeSourceProvider
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface|null
+ */
+ public function resolveQuoteItem(
+ QliroOrderItemInterface $qliroOrderItem,
+ TypeSourceProviderInterface $typeSourceProvider
+ ) {
+ $handler = $this->resolveHandler($this->typeResolver->resolve($qliroOrderItem, $typeSourceProvider));
+
+ if ($handler) {
+ return $handler->getItem($qliroOrderItem, $typeSourceProvider)->getItem();
+ }
+
+ return null;
+ }
+
+ /**
+ * Resolve handler class from a type
+ *
+ * @param string $type
+ * @return \Qliro\QliroOne\Api\Product\TypeHandlerInterface|null
+ */
+ private function resolveHandler($type)
+ {
+ return $this->pool[$type] ?? null;
+ }
+}
diff --git a/Model/Product/Type/TypeResolver.php b/Model/Product/Type/TypeResolver.php
new file mode 100644
index 0000000..797d60c
--- /dev/null
+++ b/Model/Product/Type/TypeResolver.php
@@ -0,0 +1,61 @@
+productPool = $productPool;
+ }
+
+ /**
+ * Resolve product type from a QliroOne order item
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceProviderInterface $typeSourceProvider
+ * @return string|null
+ */
+ public function resolve(QliroOrderItemInterface $qliroOrderItem, TypeSourceProviderInterface $typeSourceProvider)
+ {
+ if ($qliroOrderItem->getType() !== QliroOrderItemInterface::TYPE_PRODUCT) {
+ return null;
+ }
+
+ $sourceItem = $typeSourceProvider->getSourceItemByMerchantReference($qliroOrderItem->getMerchantReference());
+
+ if ($sourceItem) {
+ $typeHash = [$sourceItem->getProduct()->getTypeId()];
+
+ if ($parentItem = $sourceItem->getParent()) {
+ $typeHash[] = $parentItem->getProduct()->getTypeId();
+ }
+
+ return implode(':', $typeHash);
+ }
+
+ return null;
+ }
+}
diff --git a/Model/Product/Type/TypeSourceItem.php b/Model/Product/Type/TypeSourceItem.php
new file mode 100644
index 0000000..a503d4c
--- /dev/null
+++ b/Model/Product/Type/TypeSourceItem.php
@@ -0,0 +1,274 @@
+id;
+ }
+
+ /**
+ * @param int $id
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getSku()
+ {
+ return $this->sku;
+ }
+
+ /**
+ * @param string $sku
+ * @return $this
+ */
+ public function setSku($sku)
+ {
+ $this->sku = $sku;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @param string $type
+ * @return $this
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * @param string $name
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Magento\Catalog\Model\Product
+ */
+ public function getProduct()
+ {
+ return $this->product;
+ }
+
+ /**
+ * @param \Magento\Catalog\Model\Product $product
+ * @return $this
+ */
+ public function setProduct($product)
+ {
+ $this->product = $product;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getQty()
+ {
+ return $this->qty;
+ }
+
+ /**
+ * @param float $qty
+ * @return $this
+ */
+ public function setQty($qty)
+ {
+ $this->qty = $qty;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getPriceInclTax()
+ {
+ return $this->priceInclTax;
+ }
+
+ /**
+ * @param float $priceInclTax
+ * @return $this
+ */
+ public function setPriceInclTax($priceInclTax)
+ {
+ $this->priceInclTax = $priceInclTax;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getPriceExclTax()
+ {
+ return $this->priceExclTax;
+ }
+
+ /**
+ * @param float $priceExclTax
+ * @return $this
+ */
+ public function setPriceExclTax($priceExclTax)
+ {
+ $this->priceExclTax = $priceExclTax;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return mixed
+ */
+ public function getItem()
+ {
+ return $this->item;
+ }
+
+ /**
+ * @param mixed $item
+ * @return $this
+ */
+ public function setItem($item)
+ {
+ $this->item = $item;
+
+ return $this;
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Product\TypeSourceItemInterface|null
+ */
+ public function getParent()
+ {
+ return $this->parent;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Product\TypeSourceItemInterface $value
+ * @return $this
+ */
+ public function setParent($value)
+ {
+ $this->parent = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder.php b/Model/QliroOrder.php
new file mode 100644
index 0000000..5cc8b93
--- /dev/null
+++ b/Model/QliroOrder.php
@@ -0,0 +1,429 @@
+orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return QliroOrder
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantReference()
+ {
+ return $this->merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return QliroOrder
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getTotalPrice()
+ {
+ return $this->totalPrice;
+ }
+
+ /**
+ * @param float $totalPrice
+ * @return QliroOrder
+ */
+ public function setTotalPrice($totalPrice)
+ {
+ $this->totalPrice = $totalPrice;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCountry()
+ {
+ return $this->country;
+ }
+
+ /**
+ * @param string $country
+ * @return QliroOrder
+ */
+ public function setCountry($country)
+ {
+ $this->country = $country;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return QliroOrder
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getLanguage()
+ {
+ return $this->language;
+ }
+
+ /**
+ * @param string $language
+ * @return QliroOrder
+ */
+ public function setLanguage($language)
+ {
+ $this->language = $language;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getOrderHtmlSnippet()
+ {
+ return $this->orderHtmlSnippet;
+ }
+
+ /**
+ * @param string $orderHtmlSnippet
+ * @return QliroOrder
+ */
+ public function setOrderHtmlSnippet($orderHtmlSnippet)
+ {
+ $this->orderHtmlSnippet = $orderHtmlSnippet;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCustomerCheckoutStatus()
+ {
+ return $this->customerCheckoutStatus;
+ }
+
+ /**
+ * @param string $customerCheckoutStatus
+ * @return QliroOrder
+ */
+ public function setCustomerCheckoutStatus($customerCheckoutStatus)
+ {
+ $this->customerCheckoutStatus = $customerCheckoutStatus;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getSignupForNewsletter()
+ {
+ return $this->signupForNewsletter;
+ }
+
+ /**
+ * @param bool $signupForNewsletter
+ * @return QliroOrder
+ */
+ public function setSignupForNewsletter($signupForNewsletter)
+ {
+ $this->signupForNewsletter = $signupForNewsletter;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderIdentityVerificationInterface
+ */
+ public function getIdentityVerification()
+ {
+ return $this->identityVerification;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderIdentityVerificationInterface $identityVerification
+ * @return QliroOrder
+ */
+ public function setIdentityVerification($identityVerification)
+ {
+ $this->identityVerification = $identityVerification;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface
+ */
+ public function getCustomer()
+ {
+ return $this->customer;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $customer
+ * @return QliroOrder
+ */
+ public function setCustomer($customer)
+ {
+ $this->customer = $customer;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function getBillingAddress()
+ {
+ return $this->billingAddress;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $billingAddress
+ * @return QliroOrder
+ */
+ public function setBillingAddress($billingAddress)
+ {
+ $this->billingAddress = $billingAddress;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function getShippingAddress()
+ {
+ return $this->shippingAddress;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $shippingAddress
+ * @return QliroOrder
+ */
+ public function setShippingAddress($shippingAddress)
+ {
+ $this->shippingAddress = $shippingAddress;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getOrderItems()
+ {
+ return $this->orderItems;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @return QliroOrder
+ */
+ public function setOrderItems($orderItems)
+ {
+ $this->orderItems = $orderItems;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderPaymentMethodInterface
+ */
+ public function getPaymentMethod()
+ {
+ return $this->paymentMethod;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderPaymentMethodInterface $paymentMethod
+ * @return QliroOrder
+ */
+ public function setPaymentMethod($paymentMethod)
+ {
+ $this->paymentMethod = $paymentMethod;
+
+ return $this;
+ }
+
+ /**
+ * Check if QliroOne order was already placed
+ *
+ * @return bool
+ */
+ public function isPlaced()
+ {
+ return !in_array(
+ $this->getCustomerCheckoutStatus(),
+ [
+ CheckoutStatus::STATUS_IN_PROCESS,
+ CheckoutStatus::STATUS_ONHOLD
+ ]
+ );
+ }
+}
diff --git a/Model/QliroOrder/Address/Address.php b/Model/QliroOrder/Address/Address.php
new file mode 100644
index 0000000..38b7afc
--- /dev/null
+++ b/Model/QliroOrder/Address/Address.php
@@ -0,0 +1,197 @@
+firstName;
+ }
+
+ /**
+ * @param string $firstName
+ * @return Address
+ */
+ public function setFirstName($firstName)
+ {
+ $this->firstName = $firstName;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getLastName()
+ {
+ return $this->lastName;
+ }
+
+ /**
+ * @param string $lastName
+ * @return Address
+ */
+ public function setLastName($lastName)
+ {
+ $this->lastName = $lastName;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCareOf()
+ {
+ return $this->careOf;
+ }
+
+ /**
+ * @param string $careOf
+ * @return Address
+ */
+ public function setCareOf($careOf)
+ {
+ $this->careOf = $careOf;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCompanyName()
+ {
+ return $this->companyName;
+ }
+
+ /**
+ * @param string $companyName
+ * @return Address
+ */
+ public function setCompanyName($companyName)
+ {
+ $this->companyName = $companyName;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getStreet()
+ {
+ return $this->street;
+ }
+
+ /**
+ * @param string $street
+ * @return Address
+ */
+ public function setStreet($street)
+ {
+ $this->street = $street;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getPostalCode()
+ {
+ return $this->postalCode;
+ }
+
+ /**
+ * @param string $postalCode
+ * @return Address
+ */
+ public function setPostalCode($postalCode)
+ {
+ $this->postalCode = $postalCode;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCity()
+ {
+ return $this->city;
+ }
+
+ /**
+ * @param string $city
+ * @return Address
+ */
+ public function setCity($city)
+ {
+ $this->city = $city;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Admin/AdminOrder.php b/Model/QliroOrder/Admin/AdminOrder.php
new file mode 100644
index 0000000..0dbfd6d
--- /dev/null
+++ b/Model/QliroOrder/Admin/AdminOrder.php
@@ -0,0 +1,353 @@
+orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return AdminOrder
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantReference()
+ {
+ return $this->merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return AdminOrder
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getTotalPrice()
+ {
+ return $this->totalPrice;
+ }
+
+ /**
+ * @param float $totalPrice
+ * @return AdminOrder
+ */
+ public function setTotalPrice($totalPrice)
+ {
+ $this->totalPrice = $totalPrice;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCountry()
+ {
+ return $this->country;
+ }
+
+ /**
+ * @param string $country
+ * @return AdminOrder
+ */
+ public function setCountry($country)
+ {
+ $this->country = $country;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return AdminOrder
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getLanguage()
+ {
+ return $this->language;
+ }
+
+ /**
+ * @param string $language
+ * @return AdminOrder
+ */
+ public function setLanguage($language)
+ {
+ $this->language = $language;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getSignupForNewsletter()
+ {
+ return $this->signupForNewsletter;
+ }
+
+ /**
+ * @param bool $signupForNewsletter
+ * @return AdminOrder
+ */
+ public function setSignupForNewsletter($signupForNewsletter)
+ {
+ $this->signupForNewsletter = $signupForNewsletter;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderIdentityVerificationInterface
+ */
+ public function getIdentityVerification()
+ {
+ return $this->identityVerification;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderIdentityVerificationInterface $identityVerification
+ * @return AdminOrder
+ */
+ public function setIdentityVerification($identityVerification)
+ {
+ $this->identityVerification = $identityVerification;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface
+ */
+ public function getCustomer()
+ {
+ return $this->customer;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $customer
+ * @return AdminOrder
+ */
+ public function setCustomer($customer)
+ {
+ $this->customer = $customer;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function getBillingAddress()
+ {
+ return $this->billingAddress;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $billingAddress
+ * @return AdminOrder
+ */
+ public function setBillingAddress($billingAddress)
+ {
+ $this->billingAddress = $billingAddress;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function getShippingAddress()
+ {
+ return $this->shippingAddress;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $shippingAddress
+ * @return AdminOrder
+ */
+ public function setShippingAddress($shippingAddress)
+ {
+ $this->shippingAddress = $shippingAddress;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\AdminOrderItemActionInterface[]
+ */
+ public function getOrderItemActions()
+ {
+ return $this->orderItemActions;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\AdminOrderItemActionInterface[] $orderItemActions
+ * @return AdminOrder
+ */
+ public function setOrderItemActions($orderItemActions)
+ {
+ $this->orderItemActions = $orderItemActions;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\AdminOrderPaymentTransactionInterface[]
+ */
+ public function getPaymentTransactions()
+ {
+ return $this->paymentTransactions;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\AdminOrderPaymentTransactionInterface[] $paymentTransactions
+ * @return AdminOrder
+ */
+ public function setPaymentTransactions($paymentTransactions)
+ {
+ $this->paymentTransactions = $paymentTransactions;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Admin/Builder/Handler/AppliedRulesHandler.php b/Model/QliroOrder/Admin/Builder/Handler/AppliedRulesHandler.php
new file mode 100644
index 0000000..69d4339
--- /dev/null
+++ b/Model/QliroOrder/Admin/Builder/Handler/AppliedRulesHandler.php
@@ -0,0 +1,78 @@
+qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->qliroHelper = $qliroHelper;
+ }
+
+ /**
+ * Handle specific type of order items and add them to the QliroOne order items list
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @param \Magento\Sales\Model\Order $order
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function handle($orderItems, $order)
+ {
+ if (!$order->getFirstCaptureFlag()) {
+ return $orderItems;
+ }
+ $arrayAppliedRules = sprintf('DSC_%s', \str_replace(',', '_', $order->getAppliedRuleIds()));
+ // @todo might not be the exact same amount as quote calculation: $quote->getSubtotalWithDiscount() - $quote->getSubtotal();
+ $discountAmount = (float)$order->getDiscountAmount();
+
+ $formattedAmount = $this->qliroHelper->formatPrice($discountAmount);
+
+ if ($discountAmount) {
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem */
+ $qliroOrderItem = $this->qliroOrderItemFactory->create();
+
+ $qliroOrderItem->setMerchantReference($arrayAppliedRules);
+ $qliroOrderItem->setDescription($arrayAppliedRules);
+ $qliroOrderItem->setType(QliroOrderItemInterface::TYPE_DISCOUNT);
+ $qliroOrderItem->setQuantity(1);
+ $qliroOrderItem->setPricePerItemIncVat(\abs($formattedAmount));
+ $qliroOrderItem->setPricePerItemExVat(\abs($formattedAmount));
+
+ $orderItems[] = $qliroOrderItem;
+ }
+
+ return $orderItems;
+ }
+}
diff --git a/Model/QliroOrder/Admin/Builder/Handler/InvoiceFeeHandler.php b/Model/QliroOrder/Admin/Builder/Handler/InvoiceFeeHandler.php
new file mode 100644
index 0000000..2663a11
--- /dev/null
+++ b/Model/QliroOrder/Admin/Builder/Handler/InvoiceFeeHandler.php
@@ -0,0 +1,84 @@
+qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->qliroHelper = $qliroHelper;
+ }
+
+ /**
+ * Handle specific type of order items and add them to the QliroOne order items list
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @param \Magento\Sales\Model\Order $order
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function handle($orderItems, $order)
+ {
+ // @todo Handle invoiced and refunded fee
+ if (!$order->getFirstCaptureFlag()) {
+ return $orderItems;
+ }
+ $merchantReference = $order->getPayment()->getAdditionalInformation(self::MERCHANT_REFERENCE_CODE_FIELD);
+ $description = $order->getPayment()->getAdditionalInformation(self::MERCHANT_REFERENCE_DESCRIPTION_FIELD);
+ $inclTax = (float)$order->getQlirooneFee();
+ $exclTax = $inclTax - (float)$order->getQlirooneFeeTax();
+
+ $formattedInclAmount = $this->qliroHelper->formatPrice($inclTax);
+ $formattedExclAmount = $this->qliroHelper->formatPrice($exclTax);
+
+ if ($inclTax > 0 && $merchantReference) {
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem */
+ $qliroOrderItem = $this->qliroOrderItemFactory->create();
+
+ $qliroOrderItem->setMerchantReference($merchantReference);
+ $qliroOrderItem->setDescription($description);
+ $qliroOrderItem->setType(QliroOrderItemInterface::TYPE_FEE);
+ $qliroOrderItem->setQuantity(1);
+ $qliroOrderItem->setPricePerItemIncVat($formattedInclAmount);
+ $qliroOrderItem->setPricePerItemExVat($formattedExclAmount);
+
+ $orderItems[] = $qliroOrderItem;
+ }
+
+ return $orderItems;
+ }
+}
diff --git a/Model/QliroOrder/Admin/Builder/Handler/ShippingFeeHandler.php b/Model/QliroOrder/Admin/Builder/Handler/ShippingFeeHandler.php
new file mode 100644
index 0000000..3e8db63
--- /dev/null
+++ b/Model/QliroOrder/Admin/Builder/Handler/ShippingFeeHandler.php
@@ -0,0 +1,80 @@
+qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->qliroHelper = $qliroHelper;
+ }
+
+ /**
+ * Handle specific type of order items and add them to the QliroOne order items list
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @param \Magento\Sales\Model\Order $order
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function handle($orderItems, $order)
+ {
+ // @todo Handle invoiced and refunded shipping
+ if (!$order->getFirstCaptureFlag()) {
+ return $orderItems;
+ }
+ $merchantReference = $order->getShippingMethod();
+ $inclTax = (float)$order->getShippingAmount() + $order->getShippingTaxAmount();
+ $exclTax = (float)$order->getShippingAmount();
+
+ $formattedInclAmount = $this->qliroHelper->formatPrice($inclTax);
+ $formattedExclAmount = $this->qliroHelper->formatPrice($exclTax);
+
+ if ($inclTax > 0 && $merchantReference) {
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem */
+ $qliroOrderItem = $this->qliroOrderItemFactory->create();
+
+ $qliroOrderItem->setMerchantReference($merchantReference);
+ $qliroOrderItem->setDescription($merchantReference);
+ $qliroOrderItem->setType(QliroOrderItemInterface::TYPE_SHIPPING);
+ $qliroOrderItem->setQuantity(1);
+ $qliroOrderItem->setPricePerItemIncVat($formattedInclAmount);
+ $qliroOrderItem->setPricePerItemExVat($formattedExclAmount);
+
+ $orderItems[] = $qliroOrderItem;
+ }
+
+ return $orderItems;
+ }
+}
diff --git a/Model/QliroOrder/Admin/Builder/InvoiceMarkItemsAsShippedRequestBuilder.php b/Model/QliroOrder/Admin/Builder/InvoiceMarkItemsAsShippedRequestBuilder.php
new file mode 100644
index 0000000..42520bc
--- /dev/null
+++ b/Model/QliroOrder/Admin/Builder/InvoiceMarkItemsAsShippedRequestBuilder.php
@@ -0,0 +1,168 @@
+requestFactory = $requestFactory;
+ $this->linkRepository = $linkRepository;
+ $this->logManager = $logManager;
+ $this->orderItemsBuilder = $orderItemsBuilder;
+ $this->qliroConfig = $qliroConfig;
+ }
+
+ /**
+ * @param \Magento\Sales\Model\Order\Payment $payment
+ */
+ public function setPayment($payment)
+ {
+ $this->payment = $payment;
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $this->order = $this->payment->getOrder();
+
+ /** @var \Magento\Sales\Model\Order\Invoice $invoice */
+ $this->invoice = $this->payment->getInvoice();
+ }
+
+ /**
+ * Amount from Magento Capture call, is not actually used, but could be used for double checking...
+ *
+ * @param float $amount
+ */
+ public function setAmount($amount)
+ {
+ $this->amount = $amount;
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Data\AdminMarkItemsAsShippedRequestInterface
+ */
+ public function create()
+ {
+ if (empty($this->order)) {
+ throw new \LogicException('Order entity is not set.');
+ }
+
+ $request = $this->prepareRequest();
+
+ $this->payment = null;
+ $this->order = null;
+ $this->invoice = null;
+
+ return $request;
+ }
+
+ /**
+ * Prepare a new request
+ *
+ * @return \Qliro\QliroOne\Api\Data\AdminMarkItemsAsShippedRequestInterface
+ */
+ private function prepareRequest()
+ {
+ /** @var \Qliro\QliroOne\Api\Data\AdminMarkItemsAsShippedRequestInterface $request */
+ $request = $this->requestFactory->create();
+
+ try {
+ $link = $this->linkRepository->getByOrderId($this->order->getId());
+
+ $request->setMerchantApiKey($this->qliroConfig->getMerchantApiKey($this->order->getStoreId()));
+ $request->setCurrency($this->order->getOrderCurrencyCode());
+ $request->setOrderId($link->getQliroOrderId());
+
+ $this->orderItemsBuilder->setPayment($this->payment);
+ $orderItems = $this->orderItemsBuilder->create();
+
+ $request->setOrderItems($orderItems);
+
+ } catch (NoSuchEntityException $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'link_id' => $link->getId(),
+ 'quote_id' => $link->getQuoteId(),
+ 'qliro_order_id' => $link->getQliroOrderId(),
+ ],
+ ]
+ );
+
+ }
+
+ return $request;
+ }
+}
diff --git a/Model/QliroOrder/Admin/Builder/InvoiceOrderItemsBuilder.php b/Model/QliroOrder/Admin/Builder/InvoiceOrderItemsBuilder.php
new file mode 100644
index 0000000..1e39041
--- /dev/null
+++ b/Model/QliroOrder/Admin/Builder/InvoiceOrderItemsBuilder.php
@@ -0,0 +1,200 @@
+taxHelper = $taxHelper;
+ $this->typeResolver = $typeResolver;
+ $this->qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->taxCalculation = $taxCalculation;
+ $this->qliroHelper = $qliroHelper;
+ $this->handlers = $handlers;
+ $this->orderSourceProvider = $orderSourceProvider;
+ }
+
+ /**
+ * @param \Magento\Sales\Model\Order\Payment $payment
+ */
+ public function setPayment($payment)
+ {
+ $this->payment = $payment;
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $this->order = $this->payment->getOrder();
+
+ /** @var \Magento\Sales\Model\Order\Invoice $invoice */
+ $this->invoice = $this->payment->getInvoice();
+ }
+
+ /**
+ * Create an array of containers
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function create()
+ {
+ if (empty($this->order)) {
+ throw new \LogicException('Order entity is not set.');
+ }
+
+ $result = [];
+
+ /*
+ * Contains the order item id of each valid configurable about to be invoiced in this format:
+ * $configurableProducts['order item id of configurable'] = quantity about to be captured
+ */
+ $configurableProducts = [];
+
+ /** @var \Magento\Sales\Model\Order\Invoice\Item $invoiceItem */
+ foreach ($this->invoice->getAllItems() as $invoiceItem) {
+ /** @var \Magento\Sales\Model\Order\Item $orderItem */
+ $orderItem = $this->order->getItemById($invoiceItem->getOrderItemId());
+ $invoiceQty = (int)$invoiceItem->getQty();
+
+ if ($orderItem->getProductType() == 'configurable') {
+ $configurableProducts[$orderItem->getId()] = $invoiceQty;
+ }
+
+ if ($orderItem->getParentItemId()) {
+ if (!isset($configurableProducts[$orderItem->getParentItemId()])) {
+ continue;
+ }
+ $invoiceQty = $configurableProducts[$orderItem->getParentItemId()];
+ }
+
+ if (!$invoiceQty) {
+ continue;
+ }
+
+ $qliroOrderItem = $this->typeResolver->resolveQliroOrderItem(
+ $this->orderSourceProvider->generateSourceItem($orderItem, $invoiceQty),
+ $this->orderSourceProvider
+ );
+
+ if ($qliroOrderItem) {
+ $qliroOrderItem->setQuantity($invoiceQty);
+ $result[] = $qliroOrderItem;
+ }
+ }
+
+ if ($this->isFirstInvoice()) {
+ $this->order->setFirstCaptureFlag(true);
+ }
+
+ foreach ($this->handlers as $handler) {
+ if ($handler instanceof OrderItemHandlerInterface) {
+ $result = $handler->handle($result, $this->order);
+ }
+ }
+
+ $this->payment = null;
+ $this->order = null;
+ $this->invoice = null;
+ $this->orderSourceProvider->setOrder($this->order);
+
+ return $result;
+ }
+
+ /**
+ * @return bool
+ */
+ private function isFirstInvoice()
+ {
+ $invoiceCollection = $this->order->getInvoiceCollection();
+ foreach ($invoiceCollection as $invoice) {
+ if ($invoice->getId() == $this->invoice->getId()) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+}
+
diff --git a/Model/QliroOrder/Admin/Builder/ShipmentMarkItemsAsShippedRequestBuilder.php b/Model/QliroOrder/Admin/Builder/ShipmentMarkItemsAsShippedRequestBuilder.php
new file mode 100644
index 0000000..dde1a34
--- /dev/null
+++ b/Model/QliroOrder/Admin/Builder/ShipmentMarkItemsAsShippedRequestBuilder.php
@@ -0,0 +1,153 @@
+requestFactory = $requestFactory;
+ $this->linkRepository = $linkRepository;
+ $this->logManager = $logManager;
+ $this->orderItemsBuilder = $orderItemsBuilder;
+ $this->qliroConfig = $qliroConfig;
+ }
+
+ /**
+ * @param \Magento\Sales\Model\Order\Shipment $shipment
+ */
+ public function setShipment($shipment)
+ {
+ $this->shipment = $shipment;
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $this->order = $this->shipment->getOrder();
+
+ /** @var \Magento\Sales\Model\Order\Payment $payment */
+ $this->payment = $this->order->getPayment();
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Data\AdminMarkItemsAsShippedRequestInterface
+ */
+ public function create()
+ {
+ if (empty($this->order)) {
+ throw new \LogicException('Order entity is not set.');
+ }
+
+ $request = $this->prepareRequest();
+
+ $this->payment = null;
+ $this->order = null;
+ $this->shipment = null;
+
+ return $request;
+ }
+
+ /**
+ * Prepare a new request
+ *
+ * @return \Qliro\QliroOne\Api\Data\AdminMarkItemsAsShippedRequestInterface
+ */
+ private function prepareRequest()
+ {
+ /** @var \Qliro\QliroOne\Api\Data\AdminMarkItemsAsShippedRequestInterface $request */
+ $request = $this->requestFactory->create();
+
+ try {
+ $link = $this->linkRepository->getByOrderId($this->order->getId());
+
+ $request->setMerchantApiKey($this->qliroConfig->getMerchantApiKey($this->order->getStoreId()));
+ $request->setCurrency($this->order->getOrderCurrencyCode());
+ $request->setOrderId($link->getQliroOrderId());
+
+ $this->orderItemsBuilder->setShipment($this->shipment);
+ $orderItems = $this->orderItemsBuilder->create();
+
+ $request->setOrderItems($orderItems);
+
+ } catch (NoSuchEntityException $exception) {
+ $this->logManager->debug(
+ $exception,
+ [
+ 'extra' => [
+ 'link_id' => $link->getId(),
+ 'quote_id' => $link->getQuoteId(),
+ 'qliro_order_id' => $link->getQliroOrderId(),
+ ],
+ ]
+ );
+
+ }
+
+ return $request;
+ }
+}
diff --git a/Model/QliroOrder/Admin/Builder/ShipmentOrderItemsBuilder.php b/Model/QliroOrder/Admin/Builder/ShipmentOrderItemsBuilder.php
new file mode 100644
index 0000000..e77c0b5
--- /dev/null
+++ b/Model/QliroOrder/Admin/Builder/ShipmentOrderItemsBuilder.php
@@ -0,0 +1,213 @@
+taxHelper = $taxHelper;
+ $this->typeResolver = $typeResolver;
+ $this->qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->taxCalculation = $taxCalculation;
+ $this->qliroHelper = $qliroHelper;
+ $this->handlers = $handlers;
+ $this->orderSourceProvider = $orderSourceProvider;
+ }
+
+ /**
+ * @param \Magento\Sales\Model\Order\Shipment $shipment
+ */
+ public function setShipment($shipment)
+ {
+ $this->shipment = $shipment;
+
+ /** @var \Magento\Sales\Model\Order $order */
+ $this->order = $this->shipment->getOrder();
+
+ /** @var \Magento\Sales\Model\Order\Payment $payment */
+ $this->payment = $this->order->getPayment();
+ }
+
+ /**
+ * Create an array of containers
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function create()
+ {
+ if (empty($this->order)) {
+ throw new \LogicException('Order entity is not set.');
+ }
+
+ $result = [];
+
+ /*
+ * Contains the order item id of each valid configurable about to be shipped in this format:
+ * $configurableProducts['order item id of configurable'] = quantity about to be captured
+ */
+ $configurableProducts = [];
+
+ /** @var \Magento\Sales\Model\Order\Shipment\Item $shipmentItem */
+ foreach ($this->shipment->getItemsCollection() as $shipmentItem) {
+ /** @var \Magento\Sales\Model\Order\Item $orderItem */
+ $orderItem = $this->order->getItemById($shipmentItem->getOrderItemId());
+ $shipmentQty = (int)$shipmentItem->getQty();
+
+ if ($orderItem->getProductType() == 'configurable') {
+ /**
+ * This calculates how many items to ship, in case invoice was created Before shipment
+ */
+ if ($orderItem->getQtyInvoiced() > 0) {
+ $remaining = $orderItem->getQtyOrdered() - $orderItem->getQtyInvoiced();
+ if ($remaining < $shipmentQty) {
+ $shipmentQty = $remaining;
+ }
+ }
+ $configurableProducts[$orderItem->getId()] = $shipmentQty;
+ }
+
+ if ($orderItem->getParentItemId()) {
+ if (!isset($configurableProducts[$orderItem->getParentItemId()])) {
+ continue;
+ }
+ $shipmentQty = $configurableProducts[$orderItem->getParentItemId()];
+ }
+
+ if (!$shipmentQty) {
+ continue;
+ }
+
+ $qliroOrderItem = $this->typeResolver->resolveQliroOrderItem(
+ $this->orderSourceProvider->generateSourceItem($orderItem, $shipmentQty),
+ $this->orderSourceProvider
+ );
+
+ if ($qliroOrderItem) {
+ $qliroOrderItem->setQuantity($shipmentQty);
+ $result[] = $qliroOrderItem;
+ }
+ }
+
+ if ($this->isFirstShipment()) {
+ $this->order->setFirstCaptureFlag(true);
+ }
+
+ foreach ($this->handlers as $handler) {
+ if ($handler instanceof OrderItemHandlerInterface) {
+ $result = $handler->handle($result, $this->order);
+ }
+ }
+
+ $this->payment = null;
+ $this->order = null;
+ $this->shipment = null;
+ $this->orderSourceProvider->setOrder($this->order);
+
+ return $result;
+ }
+
+ /**
+ * @return bool
+ */
+ private function isFirstShipment()
+ {
+ $invoiceCollection = $this->order->getInvoiceCollection();
+ foreach ($invoiceCollection as $invoice) {
+ return false;
+ }
+ $shipmentCollection = $this->order->getShipmentsCollection();
+ foreach ($shipmentCollection as $shipment) {
+ if ($shipment->getId() == $this->shipment->getId()) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+}
+
diff --git a/Model/QliroOrder/Admin/CancelOrderRequest.php b/Model/QliroOrder/Admin/CancelOrderRequest.php
new file mode 100644
index 0000000..05c7e3b
--- /dev/null
+++ b/Model/QliroOrder/Admin/CancelOrderRequest.php
@@ -0,0 +1,93 @@
+merchantApiKey;
+ }
+
+ /**
+ * @param string $merchantApiKey
+ * @return CancelOrderRequest
+ */
+ public function setMerchantApiKey($merchantApiKey)
+ {
+ $this->merchantApiKey = $merchantApiKey;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getOrderId()
+ {
+ return $this->orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return CancelOrderRequest
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getRequestId()
+ {
+ return $this->requestId;
+ }
+
+ /**
+ * @param string $requestId
+ * @return CancelOrderRequest
+ */
+ public function setRequestId($requestId)
+ {
+ $this->requestId = $requestId;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Admin/MarkItemsAsShippedRequest.php b/Model/QliroOrder/Admin/MarkItemsAsShippedRequest.php
new file mode 100644
index 0000000..1400ee0
--- /dev/null
+++ b/Model/QliroOrder/Admin/MarkItemsAsShippedRequest.php
@@ -0,0 +1,145 @@
+merchantApiKey;
+ }
+
+ /**
+ * @param string $merchantApiKey
+ * @return MarkItemsAsShippedRequest
+ */
+ public function setMerchantApiKey($merchantApiKey)
+ {
+ $this->merchantApiKey = $merchantApiKey;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getOrderId()
+ {
+ return $this->orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return MarkItemsAsShippedRequest
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return MarkItemsAsShippedRequest
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getOrderItems()
+ {
+ return $this->orderItems;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @return MarkItemsAsShippedRequest
+ */
+ public function setOrderItems($orderItems)
+ {
+ $this->orderItems = $orderItems;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getRequestId()
+ {
+ return $this->requestId;
+ }
+
+ /**
+ * @param string $requestId
+ * @return MarkItemsAsShippedRequest
+ */
+ public function setRequestId($requestId)
+ {
+ $this->requestId = $requestId;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Admin/OrderPaymentTransaction.php b/Model/QliroOrder/Admin/OrderPaymentTransaction.php
new file mode 100644
index 0000000..f310e26
--- /dev/null
+++ b/Model/QliroOrder/Admin/OrderPaymentTransaction.php
@@ -0,0 +1,171 @@
+paymentTransactionId;
+ }
+
+ /**
+ * @param int $paymentTransactionId
+ * @return OrderPaymentTransaction
+ */
+ public function setPaymentTransactionId($paymentTransactionId)
+ {
+ $this->paymentTransactionId = $paymentTransactionId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @param string $type
+ * @return OrderPaymentTransaction
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getAmount()
+ {
+ return $this->amount;
+ }
+
+ /**
+ * @param float $amount
+ * @return OrderPaymentTransaction
+ */
+ public function setAmount($amount)
+ {
+ $this->amount = $amount;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return OrderPaymentTransaction
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ /**
+ * @param string $status
+ * @return OrderPaymentTransaction
+ */
+ public function setStatus($status)
+ {
+ $this->status = $status;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getPaymentMethodName()
+ {
+ return $this->paymentMethodName;
+ }
+
+ /**
+ * @param string $paymentMethodName
+ * @return OrderPaymentTransaction
+ */
+ public function setPaymentMethodName($paymentMethodName)
+ {
+ $this->paymentMethodName = $paymentMethodName;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Admin/ReturnWithItemsRequest.php b/Model/QliroOrder/Admin/ReturnWithItemsRequest.php
new file mode 100644
index 0000000..63d269f
--- /dev/null
+++ b/Model/QliroOrder/Admin/ReturnWithItemsRequest.php
@@ -0,0 +1,197 @@
+merchantApiKey;
+ }
+
+ /**
+ * @param string $merchantApiKey
+ * @return ReturnWithItemsRequest
+ */
+ public function setMerchantApiKey($merchantApiKey)
+ {
+ $this->merchantApiKey = $merchantApiKey;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getPaymentReference()
+ {
+ return $this->paymentReference;
+ }
+
+ /**
+ * @param int $paymentReference
+ * @return ReturnWithItemsRequest
+ */
+ public function setPaymentReference($paymentReference)
+ {
+ $this->paymentReference = $paymentReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getRequestId()
+ {
+ return $this->requestId;
+ }
+
+ /**
+ * @param string $requestId
+ * @return ReturnWithItemsRequest
+ */
+ public function setRequestId($requestId)
+ {
+ $this->requestId = $requestId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return ReturnWithItemsRequest
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getOrderItems()
+ {
+ return $this->orderItems;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @return ReturnWithItemsRequest
+ */
+ public function setOrderItems($orderItems)
+ {
+ $this->orderItems = $orderItems;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getFees()
+ {
+ return $this->fees;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $fees
+ * @return ReturnWithItemsRequest
+ */
+ public function setFees($fees)
+ {
+ $this->fees = $fees;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getDiscounts()
+ {
+ return $this->discounts;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $discounts
+ * @return ReturnWithItemsRequest
+ */
+ public function setDiscounts($discounts)
+ {
+ $this->discounts = $discounts;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Admin/TransactionResponse.php b/Model/QliroOrder/Admin/TransactionResponse.php
new file mode 100644
index 0000000..dcf529b
--- /dev/null
+++ b/Model/QliroOrder/Admin/TransactionResponse.php
@@ -0,0 +1,108 @@
+paymentTransactionId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getStatus()
+ {
+ return $this->status;
+ }
+
+ /**
+ * @return string
+ */
+ public function getType()
+ {
+ $this->type;
+ }
+
+ /**
+ * @return int
+ */
+ public function getReversalPaymentTransactionId()
+ {
+ $this->reversalPaymentTransactionId;
+ }
+
+ /**
+ * @return string
+ */
+ public function getReversalPaymentTransactionStatus()
+ {
+ $this->reversalPaymentTransactionStatus;
+ }
+
+ /**
+ * @param int $value
+ * @return $this
+ */
+ public function setPaymentTransactionId($value)
+ {
+ $this->paymentTransactionId = $value;
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setStatus($value)
+ {
+ $this->status = $value;
+ return $this;
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setType($value)
+ {
+ // TODO: Implement setType() method.
+ }
+
+ /**
+ * @param int $value
+ * @return $this
+ */
+ public function setReversalPaymentTransactionId($value)
+ {
+ // TODO: Implement setReversalPaymentTransactionId() method.
+ }
+
+ /**
+ * @param string $value
+ * @return $this
+ */
+ public function setReversalPaymentTransactionStatus($value)
+ {
+ // TODO: Implement setReversalPaymentTransactionStatus() method.
+ }
+}
diff --git a/Model/QliroOrder/Admin/UpdateItemsRequest.php b/Model/QliroOrder/Admin/UpdateItemsRequest.php
new file mode 100644
index 0000000..a6a59e3
--- /dev/null
+++ b/Model/QliroOrder/Admin/UpdateItemsRequest.php
@@ -0,0 +1,145 @@
+merchantApiKey;
+ }
+
+ /**
+ * @param string $merchantApiKey
+ * @return UpdateItemsRequest
+ */
+ public function setMerchantApiKey($merchantApiKey)
+ {
+ $this->merchantApiKey = $merchantApiKey;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getOrderId()
+ {
+ return $this->orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return UpdateItemsRequest
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return UpdateItemsRequest
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getOrderItems()
+ {
+ return $this->orderItems;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @return UpdateItemsRequest
+ */
+ public function setOrderItems($orderItems)
+ {
+ $this->orderItems = $orderItems;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getRequestId()
+ {
+ return $this->requestId;
+ }
+
+ /**
+ * @param string $requestId
+ * @return UpdateItemsRequest
+ */
+ public function setRequestId($requestId)
+ {
+ $this->requestId = $requestId;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Admin/UpdateMerchantReferenceRequest.php b/Model/QliroOrder/Admin/UpdateMerchantReferenceRequest.php
new file mode 100644
index 0000000..475b144
--- /dev/null
+++ b/Model/QliroOrder/Admin/UpdateMerchantReferenceRequest.php
@@ -0,0 +1,118 @@
+merchantApiKey;
+ }
+
+ /**
+ * @param string $merchantApiKey
+ * @return UpdateMerchantReferenceRequest
+ */
+ public function setMerchantApiKey($merchantApiKey)
+ {
+ $this->merchantApiKey = $merchantApiKey;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getOrderId()
+ {
+ return $this->orderId;
+ }
+
+ /**
+ * @param int $orderId
+ * @return UpdateMerchantReferenceRequest
+ */
+ public function setOrderId($orderId)
+ {
+ $this->orderId = $orderId;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getRequestId()
+ {
+ return $this->requestId;
+ }
+
+ /**
+ * @param string $requestId
+ * @return UpdateMerchantReferenceRequest
+ */
+ public function setRequestId($requestId)
+ {
+ $this->requestId = $requestId;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getNewMerchantReference()
+ {
+ return $this->newMerchantReference;
+ }
+
+ /**
+ * @param string $value
+ * @return UpdateMerchantReferenceRequest
+ */
+ public function setNewMerchantReference($value)
+ {
+ $this->newMerchantReference = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Builder/CreateRequestBuilder.php b/Model/QliroOrder/Builder/CreateRequestBuilder.php
new file mode 100644
index 0000000..e494962
--- /dev/null
+++ b/Model/QliroOrder/Builder/CreateRequestBuilder.php
@@ -0,0 +1,413 @@
+createRequestFactory = $createRequestFactory;
+ $this->languageMapper = $languageMapper;
+ $this->qliroConfig = $qliroConfig;
+ $this->scopeConfig = $scopeConfig;
+ $this->shippingMethodFactory = $shippingMethodFactory;
+ $this->customerBuilder = $customerBuilderm;
+ $this->orderItemsBuilder = $orderItemsBuilder;
+ $this->session = $session;
+ $this->storeManager = $storeManager;
+ $this->geoIpResolver = $geoIpResolver;
+ $this->callbackToken = $callbackToken;
+ $this->queryParamsResolver = $queryParamsResolver;
+ $this->shippingMethodsBuilder = $shippingMethodsBuilder;
+ $this->information = $information;
+ $this->eventManager = $eventManager;
+ $this->shippingConfigBuilder = $shippingConfigBuilder;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Api\Data\CartInterface $quote
+ * @return $this
+ */
+ public function setQuote(CartInterface $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Generate a QliroOne order create request object
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCreateRequestInterface
+ * @throws \Exception
+ * @todo: should we always supply shipping methods, or should it be a configuration?
+ * @todo: what about virtual quotes, they should not have any shipping methods or what?
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ $createRequest = $this->prepareCreateRequest();
+
+ $orderItems = $this->orderItemsBuilder->setQuote($this->quote)->create();
+
+ $createRequest->setOrderItems($orderItems);
+ $presetAddress = $this->qliroConfig->presetAddress();
+ $shippingAddress = $this->quote->getShippingAddress();
+ if ($presetAddress && empty($shippingAddress->getPostcode())) {
+ /* set a fake address since we don't have the real one yet */
+ $storeInfo = $this->information->getStoreInformationObject($this->quote->getStore());
+ if (!empty($storeInfo)) {
+ $shippingAddress->addData([
+ 'company' => $storeInfo->getData('name'),
+ 'telephone' => $storeInfo->getData('phone'),
+ 'street' => sprintf(
+ "%s\n%s",
+ $storeInfo->getData('street_line1'),
+ $storeInfo->getData('street_line2')
+ ),
+ 'city' => $storeInfo->getData('city'),
+ 'postcode' => str_replace(' ', '', $storeInfo->getData('postcode')),
+ 'region_id' => $storeInfo->getData('region_id'),
+ 'country_id' => $storeInfo->getData('country_id'),
+ 'region' => $storeInfo->getData('region'),
+ ]);
+ }
+ }
+ $shippingAddress->setCollectShippingRates(true)->collectShippingRates()->save();
+ $shippingMethods = $this->shippingMethodsBuilder->setQuote($this->quote)->create();
+ $availableShippingMethods = $shippingMethods->getAvailableShippingMethods();
+ if (!empty($storeInfo)) {
+ $shippingAddress->clearInstance()->save();
+ }
+ $createRequest->setAvailableShippingMethods($availableShippingMethods);
+
+ $shippingConfig = $this->shippingConfigBuilder->setQuote($this->quote)->create();
+ if ($shippingConfig) {
+ $createRequest->setShippingConfiguration($shippingConfig);
+ }
+
+ if ($this->session->isLoggedIn()) {
+ $customerInfo = $this->customerBuilder->setCustomer($this->quote->getCustomer())->create();
+ $createRequest->setCustomerInformation($customerInfo);
+ }
+
+ $this->quote->getBillingAddress()->setCountryId($createRequest->getCountry());
+ $this->quote->getShippingAddress()->setCountryId($createRequest->getCountry());
+ $this->quote->save();
+
+ $this->eventManager->dispatch(
+ 'qliroone_order_create_request_build_after',
+ [
+ 'quote' => $this->quote,
+ 'container' => $createRequest,
+ ]
+ );
+
+ $this->quote = null;
+
+ return $createRequest;
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCreateRequestInterface
+ */
+ private function prepareCreateRequest()
+ {
+ /** @var \Magento\Quote\Api\Data\CurrencyInterface $currencies */
+ $currencies = $this->quote->getCurrency();
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderCreateRequestInterface $createRequest */
+ $createRequest = $this->createRequestFactory->create();
+
+ $createRequest->setCurrency($currencies->getQuoteCurrencyCode());
+ $createRequest->setLanguage($this->languageMapper->getLanguage());
+ $createRequest->setCountry($this->getCountry());
+
+ $termsUrl = $this->qliroConfig->getTermsUrl();
+ $createRequest->setMerchantTermsUrl($termsUrl ? $termsUrl : $this->getUrl('/'));
+ $createRequest->setMerchantIntegrityPolicyUrl($this->qliroConfig->getIntegrityPolicyUrl());
+
+ $createRequest->setMerchantConfirmationUrl($this->getUrl('checkout/qliro/pending'));
+
+ $createRequest->setMerchantCheckoutStatusPushUrl(
+ $this->getCallbackUrl('checkout/qliro_callback/checkoutStatus')
+ );
+
+ $createRequest->setMerchantOrderManagementStatusPushUrl(
+ $this->getCallbackUrl('checkout/qliro_callback/transactionStatus')
+ );
+
+ $createRequest->setMerchantOrderValidationUrl($this->getCallbackUrl('checkout/qliro_callback/validate'));
+
+ $createRequest->setMerchantOrderAvailableShippingMethodsUrl(
+ $this->getCallbackUrl('checkout/qliro_callback/shippingMethods')
+ );
+
+ $createRequest->setBackgroundColor($this->qliroConfig->getStylingBackgroundColor());
+ $createRequest->setPrimaryColor($this->qliroConfig->getStylingPrimaryColor());
+ $createRequest->setCallToActionColor($this->qliroConfig->getStylingCallToActionColor());
+ $createRequest->setCallToActionHoverColor($this->qliroConfig->getStylingHoverColor());
+ $createRequest->setCornerRadius($this->qliroConfig->getStylingRadius());
+ $createRequest->setButtonCornerRadius($this->qliroConfig->getStylingButtonCurnerRadius());
+ $createRequest->setMinimumCustomerAge(
+ $this->qliroConfig->getMinimumCustomerAge() > 0 ? $this->qliroConfig->getMinimumCustomerAge() : null
+ );
+ $createRequest->setAskForNewsletterSignup($this->qliroConfig->shouldAskForNewsletterSignup());
+ $createRequest->setRequireIdentityVerification($this->qliroConfig->requireIdentityVerification());
+ foreach ($this->quote->getItems() as $item) {
+ if ($item->getProductType() == Type::TYPE_VIRTUAL && !$item->getParentItemId()) {
+ $createRequest->setRequireIdentityVerification(1);
+ }
+ }
+
+ return $createRequest;
+ }
+
+ /**
+ * Get a country code, either from default config setting, or from a GeoIP resolver
+ *
+ * @return string
+ */
+ private function getCountry()
+ {
+ $countryCode = null;
+
+ if ($this->qliroConfig->isUseGeoIp()) {
+ $countryCode = $this->geoIpResolver->getCountryCode($this->quote->getRemoteIp());
+ }
+
+ if (empty($countryCode)) {
+ $countryCode = $this->scopeConfig->getValue(
+ \Magento\Directory\Helper\Data::XML_PATH_DEFAULT_COUNTRY,
+ ScopeInterface::SCOPE_STORE
+ );
+ }
+
+ return $countryCode;
+ }
+
+ /**
+ * Get a callback URL with provided path and generated token
+ *
+ * @param string $path
+ * @return string
+ */
+ private function getCallbackUrl($path)
+ {
+ $params['_query']['token'] = $this->generateCallbackToken();
+
+ if ($this->qliroConfig->isDebugMode()) {
+ $params['_query']['XDEBUG_SESSION_START'] = $this->qliroConfig->getCallbackXdebugSessionFlagName();
+ }
+
+ if ($this->qliroConfig->redirectCallbacks() && ($baseUri = $this->qliroConfig->getCallbackUri())) {
+ $url = implode('/', [rtrim($baseUri, '/'), ltrim($path, '/')]);
+
+ $this->queryParamsResolver->addQueryParams($params['_query']);
+ $queryString = $this->queryParamsResolver->getQuery();
+ $url .= '?' . $queryString;
+
+ return $this->applyHttpAuth($url);
+ }
+
+ return $this->applyHttpAuth($this->getUrl($path, $params));
+ }
+
+ /**
+ * Apply HTTP authentication credentials if specified
+ *
+ * @param string $url
+ * @return string
+ */
+ private function applyHttpAuth($url)
+ {
+ if ($this->qliroConfig->isHttpAuthEnabled() && preg_match('#^(https?://)(.+)$#', $url, $match)) {
+ $authUsername = $this->qliroConfig->getCallbackHttpAuthUsername();
+ $authPassword = $this->qliroConfig->getCallbackHttpAuthPassword();
+
+ $url = sprintf('%s%s:%s@%s', $match[1], \urlencode($authUsername), \urlencode($authPassword), $match[2]);
+ }
+
+ return $url;
+ }
+
+ /**
+ * Get a store-specific URL with provided path and optional parameters
+ *
+ * @param string $path
+ * @param array $params
+ * @return string
+ */
+ private function getUrl($path, $params = [])
+ {
+ /** @var \Magento\Store\Model\Store $store */
+ $store = $this->storeManager->getStore();
+
+ return $store->getUrl($path, $params);
+ }
+
+ /**
+ * @return string
+ */
+ private function generateCallbackToken()
+ {
+ if (!$this->generatedToken) {
+ $this->generatedToken = $this->callbackToken->getToken();
+ }
+
+ return $this->generatedToken;
+ }
+}
diff --git a/Model/QliroOrder/Builder/CustomerAddressBuilder.php b/Model/QliroOrder/Builder/CustomerAddressBuilder.php
new file mode 100644
index 0000000..63a6c1b
--- /dev/null
+++ b/Model/QliroOrder/Builder/CustomerAddressBuilder.php
@@ -0,0 +1,80 @@
+orderCustomerAddressFactory = $orderCustomerAddressFactory;
+ }
+
+ /**
+ * Set a customer to extract data
+ *
+ * @param \Magento\Customer\Model\Address $address
+ * @return $this
+ */
+ public function setAddress(Address $address)
+ {
+ $this->address = $address;
+
+ return $this;
+ }
+
+ /**
+ * Create a container
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function create()
+ {
+ if (empty($this->address)) {
+ throw new \LogicException('Customer address entity is not set.');
+ }
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $qliroOrderCustomerAddress */
+ $qliroOrderCustomerAddress = $this->orderCustomerAddressFactory->create();
+
+ $streetAddress = trim(implode(self::STREET_ADDRESS_SEPARATOR, $this->address->getStreet()));
+
+ $qliroOrderCustomerAddress->setFirstName($this->address->getFirstname());
+ $qliroOrderCustomerAddress->setLastName($this->address->getLastname());
+ $qliroOrderCustomerAddress->setCompanyName($this->address->getCompany());
+ $qliroOrderCustomerAddress->setStreet($streetAddress);
+ $qliroOrderCustomerAddress->setPostalCode(str_replace(' ', '', $this->address->getPostcode()));
+ $qliroOrderCustomerAddress->setCity($this->address->getCity());
+
+ $this->address = null;
+
+ return $qliroOrderCustomerAddress;
+ }
+
+}
diff --git a/Model/QliroOrder/Builder/CustomerBuilder.php b/Model/QliroOrder/Builder/CustomerBuilder.php
new file mode 100644
index 0000000..e5d59ab
--- /dev/null
+++ b/Model/QliroOrder/Builder/CustomerBuilder.php
@@ -0,0 +1,101 @@
+orderCustomerFactory = $orderCustomerFactory;
+ $this->customerAddressBuilder = $customerAddressBuilder;
+ $this->addressFactory = $addressFactory;
+ }
+
+ /**
+ * Set a customer to extract data
+ *
+ * @param \Magento\Customer\Api\Data\CustomerInterface $customer
+ * @return $this
+ */
+ public function setCustomer(CustomerInterface $customer)
+ {
+ $this->customer = $customer;
+
+ return $this;
+ }
+
+ /**
+ * Create a container
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface
+ */
+ public function create()
+ {
+ if (empty($this->customer)) {
+ throw new \LogicException('Customer entity is not set.');
+ }
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $qliroOrderCustomer */
+ $qliroOrderCustomer = $this->orderCustomerFactory->create();
+
+ $addressId = $this->customer->getDefaultBilling();
+
+
+ $address = $this->addressFactory->create()->load($addressId);
+
+ $qliroOrderCustomerAddress = $this->customerAddressBuilder->setAddress($address)->create();
+
+ $qliroOrderCustomer->setEmail($this->customer->getEmail());
+ $qliroOrderCustomer->setMobileNumber(null);
+ $qliroOrderCustomer->setAddress($qliroOrderCustomerAddress);
+ $qliroOrderCustomer->setLockCustomerInformation(true);
+ $qliroOrderCustomer->setLockCustomerEmail(false);
+ $qliroOrderCustomer->setLockCustomerMobileNumber(false);
+ $qliroOrderCustomer->setLockCustomerAddress(false);
+
+ $this->customer = null;
+
+ return $qliroOrderCustomer;
+ }
+}
diff --git a/Model/QliroOrder/Builder/FeeBuilder.php b/Model/QliroOrder/Builder/FeeBuilder.php
new file mode 100644
index 0000000..878fc87
--- /dev/null
+++ b/Model/QliroOrder/Builder/FeeBuilder.php
@@ -0,0 +1,115 @@
+qliroConfig = $qliroConfig;
+ $this->qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->fee = $fee;
+ $this->eventManager = $eventManager;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return $this
+ */
+ public function setQuote(Quote $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Create a QliroOne order fee container
+ *
+ * Is this class used?
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $container */
+ $container = $this->qliroOrderItemFactory->create();
+
+ $priceExVat = $this->fee->getQlirooneFeeInclTax($this->quote);
+ $priceIncVat = $this->fee->getQlirooneFeeExclTax($this->quote);
+
+ $container->setMerchantReference($this->qliroConfig->getFeeMerchantReference());
+ $container->setDescription($this->qliroConfig->getFeeMerchantReference());
+ $container->setPricePerItemIncVat($priceIncVat);
+ $container->setPricePerItemExVat($priceExVat);
+ $container->setQuantity(1);
+ $container->setType(QliroOrderItemInterface::TYPE_FEE);
+
+ $this->eventManager->dispatch(
+ 'qliroone_order_item_build_after',
+ [
+ 'quote' => $this->quote,
+ 'container' => $container,
+ ]
+ );
+
+ $this->quote = null;
+
+ return $container;
+ }
+}
diff --git a/Model/QliroOrder/Builder/Handler/AppliedRulesHandler.php b/Model/QliroOrder/Builder/Handler/AppliedRulesHandler.php
new file mode 100644
index 0000000..160382f
--- /dev/null
+++ b/Model/QliroOrder/Builder/Handler/AppliedRulesHandler.php
@@ -0,0 +1,93 @@
+qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->qliroHelper = $qliroHelper;
+ $this->eventManager = $eventManager;
+ }
+
+ /**
+ * Handle specific type of order items and add them to the QliroOne order items list
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function handle($orderItems, $quote)
+ {
+ $arrayAppliedRules = sprintf('DSC_%s', \str_replace(',', '_', $quote->getAppliedRuleIds()));
+ $discountAmount = $quote->getSubtotalWithDiscount() - $quote->getSubtotal();
+ $formattedAmount = $this->qliroHelper->formatPrice($discountAmount);
+
+ if ($discountAmount) {
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $qliroOrderItem */
+ $qliroOrderItem = $this->qliroOrderItemFactory->create();
+
+ $qliroOrderItem->setMerchantReference($arrayAppliedRules);
+ $qliroOrderItem->setDescription($arrayAppliedRules);
+ $qliroOrderItem->setType(QliroOrderItemInterface::TYPE_DISCOUNT);
+ $qliroOrderItem->setQuantity(1);
+ $qliroOrderItem->setPricePerItemIncVat(\abs($formattedAmount));
+ $qliroOrderItem->setPricePerItemExVat(\abs($formattedAmount));
+
+ // Note that this event dispatch must be done for every implemented Handler
+ $this->eventManager->dispatch(
+ 'qliroone_order_item_build_after',
+ [
+ 'quote' => $quote,
+ 'container' => $qliroOrderItem,
+ ]
+ );
+
+ if ($qliroOrderItem->getMerchantReference()) {
+ $orderItems[] = $qliroOrderItem;
+ }
+ }
+
+ return $orderItems;
+ }
+}
diff --git a/Model/QliroOrder/Builder/OrderItemsBuilder.php b/Model/QliroOrder/Builder/OrderItemsBuilder.php
new file mode 100644
index 0000000..21fbe80
--- /dev/null
+++ b/Model/QliroOrder/Builder/OrderItemsBuilder.php
@@ -0,0 +1,161 @@
+taxHelper = $taxHelper;
+ $this->typeResolver = $typeResolver;
+ $this->qliroOrderItemFactory = $qliroOrderItemFactory;
+ $this->taxCalculation = $taxCalculation;
+ $this->qliroHelper = $qliroHelper;
+ $this->quoteSourceProvider = $quoteSourceProvider;
+ $this->eventManager = $eventManager;
+ $this->handlers = $handlers;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Api\Data\CartInterface $quote
+ * @return $this
+ */
+ public function setQuote(CartInterface $quote)
+ {
+ $this->quote = $quote;
+ $this->quoteSourceProvider->setQuote($this->quote);
+
+ return $this;
+ }
+
+ /**
+ * Create an array of containers
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ $result = [];
+
+ /** @var \Magento\Quote\Model\Quote\Item $item */
+ foreach ($this->quote->getAllItems() as $item) {
+ $qliroOrderItem = $this->typeResolver->resolveQliroOrderItem(
+ $this->quoteSourceProvider->generateSourceItem($item, $item->getQty()),
+ $this->quoteSourceProvider
+ );
+
+ if ($qliroOrderItem) {
+ $this->eventManager->dispatch(
+ 'qliroone_order_item_build_after',
+ [
+ 'quote' => $this->quote,
+ 'container' => $qliroOrderItem,
+ ]
+ );
+
+ if ($qliroOrderItem->getMerchantReference()) {
+ $result[] = $qliroOrderItem;
+ }
+ }
+ }
+
+ foreach ($this->handlers as $handler) {
+ if ($handler instanceof OrderItemHandlerInterface) {
+ $result = $handler->handle($result, $this->quote);
+ }
+ }
+
+ $this->quote = null;
+ $this->quoteSourceProvider->setQuote($this->quote);
+
+ return $result;
+ }
+}
diff --git a/Model/QliroOrder/Builder/ShippingConfigBuilder.php b/Model/QliroOrder/Builder/ShippingConfigBuilder.php
new file mode 100644
index 0000000..eb6f1b7
--- /dev/null
+++ b/Model/QliroOrder/Builder/ShippingConfigBuilder.php
@@ -0,0 +1,110 @@
+shippingConfigFactory = $shippingConfigFactory;
+ $this->shippingConfigUnifaunBuilder = $shippingConfigUnifaunBuilder;
+ $this->eventManager = $eventManager;
+ $this->qliroConfig = $qliroConfig;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return $this
+ */
+ public function setQuote(Quote $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Create a QliroOne order shipping Config container
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+ if (!$this->qliroConfig->isUnifaunEnabled($this->quote->getStoreId())) {
+ return null;
+ }
+ if ($this->quote->isVirtual()) {
+ return null;
+ }
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigInterface $container */
+ $container = $this->shippingConfigFactory->create();
+ $unifaunContainer = $this->shippingConfigUnifaunBuilder->setQuote($this->quote)->create();
+ $container->setUnifaun($unifaunContainer);
+
+ $this->eventManager->dispatch(
+ 'qliroone_shipping_config_build_after',
+ [
+ 'quote' => $this->quote,
+ 'container' => $container,
+ ]
+ );
+
+ $this->quote = null;
+
+ return $container;
+ }
+}
diff --git a/Model/QliroOrder/Builder/ShippingConfigUnifaunBuilder.php b/Model/QliroOrder/Builder/ShippingConfigUnifaunBuilder.php
new file mode 100644
index 0000000..ba4f017
--- /dev/null
+++ b/Model/QliroOrder/Builder/ShippingConfigUnifaunBuilder.php
@@ -0,0 +1,185 @@
+shippingConfigUnifaunFactory = $shippingConfigUnifaunFactory;
+ $this->eventManager = $eventManager;
+ $this->qliroConfig = $qliroConfig;
+ $this->qliroHelper = $qliroHelper;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return $this
+ */
+ public function setQuote(Quote $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Create a QliroOne order shipping Config container
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigUnifaunInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigUnifaunInterface $container */
+ $container = $this->shippingConfigUnifaunFactory->create();
+ $container->setCheckoutId($this->qliroConfig->getUnifaunCheckoutId());
+ $container->setTags($this->buildTags($this->qliroConfig->getUnifaunParameters()));
+
+ $this->eventManager->dispatch(
+ 'qliroone_shipping_config_unifaun_build_after',
+ [
+ 'quote' => $this->quote,
+ 'container' => $container,
+ ]
+ );
+
+ $this->quote = null;
+
+ return $container;
+ }
+
+ /** Should get rewritten for easier customizations
+ * @param array $params
+ */
+ private function buildTags($params)
+ {
+ $tags = null;
+ foreach ($params as $param) {
+ switch ($param[self::UNIFAUN_TAGS_SETTING_FUNC]) {
+ case self::UNIFAUN_TAGS_FUNC_BULKY:
+ $tags[$param[self::UNIFAUN_TAGS_SETTING_TAG]] =
+ $this->calculateQuoteBulky($param[self::UNIFAUN_TAGS_SETTING_VALUE]);
+ break;
+ case self::UNIFAUN_TAGS_FUNC_USERDEFINED:
+ $tags[$param[self::UNIFAUN_TAGS_SETTING_TAG]] = $param[self::UNIFAUN_TAGS_SETTING_VALUE];
+ break;
+ case self::UNIFAUN_TAGS_FUNC_WEIGHT:
+ $tags[$param[self::UNIFAUN_TAGS_SETTING_TAG]] =
+ $this->calculateQuoteWeight($param[self::UNIFAUN_TAGS_SETTING_VALUE]);
+ break;
+ case self::UNIFAUN_TAGS_FUNC_CARTPRICE:
+ $tags[$param[self::UNIFAUN_TAGS_SETTING_TAG]] =
+ $this->calculateQuoteCartPrice($param[self::UNIFAUN_TAGS_SETTING_VALUE]);
+ break;
+ }
+ }
+
+ return $tags;
+ }
+
+ /**
+ * @param $attributeCode
+ */
+ private function calculateQuoteBulky($attributeCode)
+ {
+ $isBulky = false;
+ /** @var \Magento\Quote\Model\Quote\Item $item */
+ foreach ($this->quote->getAllVisibleItems() as $item) {
+ $product = $item->getProduct();
+ $bulky = $product->getData($attributeCode);
+ if ($bulky) {
+ $isBulky = true;
+ break;
+ }
+ }
+
+ return $isBulky;
+ }
+
+ private function calculateQuoteWeight($attributeCode)
+ {
+ $totalWeight = 0;
+ /** @var \Magento\Quote\Model\Quote\Item $item */
+ foreach ($this->quote->getAllVisibleItems() as $item) {
+ $product = $item->getProduct();
+ $weight = $product->getData($attributeCode);
+ if ($weight > 0) {
+ $totalWeight += $weight;
+ }
+ }
+
+ return $totalWeight;
+ }
+
+ private function calculateQuoteCartPrice($attributeCode)
+ {
+ $totalAmount = $this->qliroHelper->formatPrice($this->quote->getData($attributeCode));
+
+ return $totalAmount;
+ }
+}
diff --git a/Model/QliroOrder/Builder/ShippingMethodBuilder.php b/Model/QliroOrder/Builder/ShippingMethodBuilder.php
new file mode 100644
index 0000000..9e282aa
--- /dev/null
+++ b/Model/QliroOrder/Builder/ShippingMethodBuilder.php
@@ -0,0 +1,175 @@
+taxHelper = $taxHelper;
+ $this->shippingMethodFactory = $shippingMethodFactory;
+ $this->shippingMethodBrandResolver = $shippingMethodBrandResolver;
+ $this->qliroHelper = $qliroHelper;
+ $this->eventManager = $eventManager;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return $this
+ */
+ public function setQuote(Quote $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Set shipping rate for data extraction
+ *
+ * @param \Magento\Quote\Model\Quote\Address\Rate $rate
+ * @return $this
+ */
+ public function setShippingRate(Rate $rate)
+ {
+ $this->rate = $rate;
+
+ return $this;
+ }
+
+ /**
+ * Create a QliroOne order shipping method container
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderShippingMethodInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ if (empty($this->rate)) {
+ throw new \LogicException('Shipping rate entity is not set.');
+ }
+
+ $shippingAddress = $this->quote->getShippingAddress();
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderShippingMethodInterface $container */
+ $container = $this->shippingMethodFactory->create();
+
+ $priceExVat = $this->taxHelper->getShippingPrice(
+ $this->rate->getPrice(),
+ false,
+ $shippingAddress,
+ $this->quote->getCustomerTaxClassId()
+ );
+
+ $priceIncVat = $this->taxHelper->getShippingPrice(
+ $this->rate->getPrice(),
+ true,
+ $shippingAddress,
+ $this->quote->getCustomerTaxClassId()
+ );
+
+ $container->setMerchantReference($this->rate->getCode());
+ $container->setDisplayName($this->rate->getMethodTitle());
+ $container->setBrand($this->shippingMethodBrandResolver->resolve($this->rate));
+
+ $descriptions = [];
+
+ if ($this->rate->getCarrierTitle() !== null) {
+ $descriptions[] = $this->rate->getCarrierTitle();
+ }
+
+ if ($this->rate->getMethodDescription() !== null) {
+ $descriptions[] = $this->rate->getMethodDescription();
+ }
+
+ if (!empty($descriptions)) {
+ $container->setDescriptions($descriptions);
+ }
+
+ $container->setPriceIncVat($this->qliroHelper->formatPrice($priceIncVat));
+ $container->setPriceExVat($this->qliroHelper->formatPrice($priceExVat));
+ $container->setSupportsDynamicSecondaryOptions(false);
+
+ $this->eventManager->dispatch(
+ 'qliroone_shipping_method_build_after',
+ [
+ 'quote' => $this->quote,
+ 'rate' => $this->rate,
+ 'container' => $container,
+ ]
+ );
+
+ $this->quote = null;
+ $this->rate = null;
+
+ return $container;
+ }
+}
diff --git a/Model/QliroOrder/Builder/ShippingMethodsBuilder.php b/Model/QliroOrder/Builder/ShippingMethodsBuilder.php
new file mode 100644
index 0000000..f6936da
--- /dev/null
+++ b/Model/QliroOrder/Builder/ShippingMethodsBuilder.php
@@ -0,0 +1,155 @@
+shippingMethodsResponseFactory = $shippingMethodsResponseFactory;
+ $this->shippingMethodBuilder = $shippingMethodBuilder;
+ $this->eventManager = $eventManager;
+ $this->storeManager = $storeManager;
+ $this->qliroConfig = $qliroConfig;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return $this
+ */
+ public function setQuote(Quote $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Data\UpdateShippingMethodsResponseInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ /** @var \Qliro\QliroOne\Api\Data\UpdateShippingMethodsResponseInterface $container */
+ $container = $this->shippingMethodsResponseFactory->create();
+
+ if ($this->qliroConfig->isUnifaunEnabled($this->quote->getStoreId())) {
+ return $container;
+ }
+
+ $shippingAddress = $this->quote->getShippingAddress();
+ $rateGroups = $shippingAddress->getGroupedAllShippingRates();
+
+ $collectedShippingMethods = [];
+
+ if ($this->quote->getIsVirtual()) {
+ $container->setAvailableShippingMethods($collectedShippingMethods);
+ } else {
+ foreach ($rateGroups as $group) {
+ /** @var \Magento\Quote\Model\Quote\Address\Rate $rate */
+ foreach ($group as $rate) {
+ if (substr($rate->getCode(), -6) === '_error') {
+ continue;
+ }
+
+ $this->shippingMethodBuilder->setQuote($this->quote);
+
+ /** @var \Magento\Store\Api\Data\StoreInterface */
+ $store = $this->storeManager->getStore();
+ $amountPrice = $store->getBaseCurrency()
+ ->convert($rate->getPrice(), $store->getCurrentCurrencyCode());
+ $rate->setPrice($amountPrice);
+
+ $this->shippingMethodBuilder->setShippingRate($rate);
+ $shippingMethodContainer = $this->shippingMethodBuilder->create();
+
+ if (!$shippingMethodContainer->getMerchantReference()) {
+ continue;
+ }
+
+ $collectedShippingMethods[] = $shippingMethodContainer;
+ }
+ }
+
+ if (empty($collectedShippingMethods)) {
+ $container->setDeclineReason(UpdateShippingMethodsResponseInterface::REASON_POSTAL_CODE);
+ } else {
+ $container->setAvailableShippingMethods($collectedShippingMethods);
+ }
+ }
+
+ $this->eventManager->dispatch(
+ 'qliroone_shipping_methods_response_build_after',
+ [
+ 'quote' => $this->quote,
+ 'container' => $container,
+ ]
+ );
+
+ $this->quote = null;
+
+ return $container;
+ }
+}
diff --git a/Model/QliroOrder/Builder/ShippingOrderItemBuilder.php b/Model/QliroOrder/Builder/ShippingOrderItemBuilder.php
new file mode 100644
index 0000000..373fa8d
--- /dev/null
+++ b/Model/QliroOrder/Builder/ShippingOrderItemBuilder.php
@@ -0,0 +1,118 @@
+orderItemFactory = $orderItemFactory;
+ $this->taxHelper = $taxHelper;
+ $this->eventManager = $eventManager;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return $this
+ */
+ public function setQuote(Quote $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Create a QliroOne order item container for a shipping method
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ $shippingAddress = $this->quote->getShippingAddress();
+ $code = $shippingAddress->getShippingMethod();
+ $rate = $shippingAddress->getShippingRateByCode($code);
+
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $container */
+ $container = $this->orderItemFactory->create();
+
+ $priceExVat = $this->taxHelper->getShippingPrice(
+ $rate->getPrice(),
+ false,
+ $shippingAddress,
+ $this->quote->getCustomerTaxClassId()
+ );
+
+ $priceIncVat = $this->taxHelper->getShippingPrice(
+ $rate->getPrice(),
+ true,
+ $shippingAddress,
+ $this->quote->getCustomerTaxClassId()
+ );
+
+ $container->setMerchantReference($code);
+
+ $container->setPricePerItemIncVat($priceIncVat);
+ $container->setPricePerItemExVat($priceExVat);
+
+ $this->eventManager->dispatch(
+ 'qliroone_order_item_build_after',
+ [
+ 'quote' => $this->quote,
+ 'container' => $container,
+ ]
+ );
+
+ $this->quote = null;
+
+ return $container;
+ }
+}
diff --git a/Model/QliroOrder/Builder/UpdateRequestBuilder.php b/Model/QliroOrder/Builder/UpdateRequestBuilder.php
new file mode 100644
index 0000000..fc7ad17
--- /dev/null
+++ b/Model/QliroOrder/Builder/UpdateRequestBuilder.php
@@ -0,0 +1,137 @@
+qliroConfig = $qliroConfig;
+ $this->scopeConfig = $scopeConfig;
+ $this->updateRequestFactory = $updateRequestFactory;
+ $this->orderItemsBuilder = $orderItemsBuilder;
+ $this->shippingMethodsBuilder = $shippingMethodsBuilder;
+ $this->shippingConfigBuilder = $shippingConfigBuilder;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Api\Data\CartInterface $quote
+ * @return $this
+ */
+ public function setQuote(CartInterface $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Generate a QliroOne order update request object
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderUpdateRequestInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ $updateRequest = $this->prepareUpdateRequest();
+
+ $orderItems = $this->orderItemsBuilder->setQuote($this->quote)->create();
+
+ $updateRequest->setOrderItems($orderItems);
+ $shippingMethods = $this->shippingMethodsBuilder->setQuote($this->quote)->create();
+ $updateRequest->setAvailableShippingMethods($shippingMethods->getAvailableShippingMethods());
+
+ $shippingConfig = $this->shippingConfigBuilder->setQuote($this->quote)->create();
+ if ($shippingConfig) {
+ $updateRequest->setShippingConfiguration($shippingConfig);
+ }
+
+ return $updateRequest;
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderUpdateRequestInterface
+ */
+ private function prepareUpdateRequest()
+ {
+ /** @var \Qliro\QliroOne\Api\Data\QliroOrderUpdateRequestInterface $qliroOrderUpdateRequest */
+ $qliroOrderUpdateRequest = $this->updateRequestFactory->create();
+ $qliroOrderUpdateRequest->setRequireIdentityVerification($this->qliroConfig->requireIdentityVerification());
+ foreach ($this->quote->getItems() as $item) {
+ if ($item->getProductType() == Type::TYPE_VIRTUAL && !$item->getParentItemId()) {
+ $qliroOrderUpdateRequest->setRequireIdentityVerification(1);
+ }
+ }
+
+ return $qliroOrderUpdateRequest;
+ }
+}
diff --git a/Model/QliroOrder/Builder/ValidateOrderBuilder.php b/Model/QliroOrder/Builder/ValidateOrderBuilder.php
new file mode 100644
index 0000000..b326bdd
--- /dev/null
+++ b/Model/QliroOrder/Builder/ValidateOrderBuilder.php
@@ -0,0 +1,317 @@
+validateOrderResponseFactory = $validateOrderResponseFactory;
+ $this->stockRegistry = $stockRegistry;
+ $this->orderItemsBuilder = $orderItemsBuilder;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Set quote for data extraction
+ *
+ * @param \Magento\Quote\Api\Data\CartInterface $quote
+ * @return $this
+ */
+ public function setQuote(CartInterface $quote)
+ {
+ $this->quote = $quote;
+
+ return $this;
+ }
+
+ /**
+ * Set validation request for data extraction
+ *
+ * @param \Qliro\QliroOne\Api\Data\ValidateOrderNotificationInterface $validationRequest
+ * @return $this
+ */
+ public function setValidationRequest($validationRequest)
+ {
+ $this->validationRequest = $validationRequest;
+
+ return $this;
+ }
+
+ /**
+ * @return \Qliro\QliroOne\Api\Data\ValidateOrderResponseInterface
+ */
+ public function create()
+ {
+ if (empty($this->quote)) {
+ throw new \LogicException('Quote entity is not set.');
+ }
+
+ if (empty($this->validationRequest)) {
+ throw new \LogicException('QliroOne validation request is not set.');
+ }
+
+ /** @var \Qliro\QliroOne\Api\Data\ValidateOrderResponseInterface $container */
+ $container = $this->validateOrderResponseFactory->create();
+
+ $allInStock = $this->checkItemsInStock();
+
+ if (!$allInStock) {
+ $this->quote = null;
+ $this->validationRequest = null;
+
+ return $container->setDeclineReason(ValidateOrderResponseInterface::REASON_OUT_OF_STOCK);
+ }
+
+ if (!$this->quote->isVirtual() && !$this->quote->getShippingAddress()->getShippingMethod()) {
+ $this->quote = null;
+ $this->validationRequest = null;
+ $this->logValidateError(
+ 'create',
+ 'not a virtual order, invalid shipping method selected',
+ ['method' => $this->quote->getShippingAddress()->getShippingMethod()]
+ );
+
+ return $container->setDeclineReason(ValidateOrderResponseInterface::REASON_SHIPPING);
+ }
+
+ $orderItemsFromQuote = $this->orderItemsBuilder->setQuote($this->quote)->create();
+
+ $allMatch = $this->compareQuoteAndQliroOrderItems(
+ $orderItemsFromQuote,
+ $this->validationRequest->getOrderItems()
+ );
+
+ if (!$allMatch) {
+ return $container->setDeclineReason(ValidateOrderResponseInterface::REASON_OTHER);
+ }
+
+ $container->setDeclineReason(null);
+
+ $this->quote = null;
+ $this->validationRequest = null;
+
+ return $container;
+ }
+
+ /**
+ * Check if any items are out of stock
+ *
+ * @return bool
+ */
+ private function checkItemsInStock()
+ {
+ /** @var \Magento\Quote\Model\Quote\Item $quoteItem */
+ foreach ($this->quote->getAllVisibleItems() as $quoteItem) {
+ $stockItem = $this->stockRegistry->getStockItem(
+ $quoteItem->getProduct()->getId(),
+ $quoteItem->getProduct()->getStore()->getWebsiteId()
+ );
+
+ if (!$stockItem->getIsInStock()) {
+ $this->logValidateError(
+ 'checkItemsInStock',
+ 'not enough stock',
+ ['sku' => $quoteItem->getSku()]
+ );
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Return true if the quote items and QliroOne order items match
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $quoteItems
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $qliroOrderItems
+ * @return bool
+ */
+ private function compareQuoteAndQliroOrderItems($quoteItems, $qliroOrderItems)
+ {
+ $hashedQuoteItems = [];
+ $hashedQliroItems = [];
+
+ $skipTypes = [QliroOrderItemInterface::TYPE_SHIPPING, QliroOrderItemInterface::TYPE_FEE];
+
+ if (!$quoteItems) {
+ $this->logValidateError('compareQuoteAndQliroOrderItems','no Cart Items');
+ return false;
+ }
+
+ // Gather order items converted from quote and hash them for faster search
+ foreach ($quoteItems as $quoteItem) {
+ if (!in_array($quoteItem->getType(), $skipTypes)) {
+ $hashedQuoteItems[$quoteItem->getMerchantReference()] = $quoteItem;
+ }
+ }
+
+ if (!$qliroOrderItems) {
+ $this->logValidateError('compareQuoteAndQliroOrderItems','no Qliro Items');
+ return false;
+ }
+
+ // Gather order items from QliroOne order and hash them for faster search, then try to see a diff
+ foreach ($qliroOrderItems as $qliroOrderItem) {
+ if (!in_array($qliroOrderItem->getType(), $skipTypes)) {
+ $hash = $qliroOrderItem->getMerchantReference();
+ if ($qliroOrderItem->getType() == QliroOrderItemInterface::TYPE_DISCOUNT) {
+ $qliroOrderItem->setPricePerItemExVat(\abs($qliroOrderItem->getPricePerItemExVat()));
+ $qliroOrderItem->setPricePerItemIncVat(\abs($qliroOrderItem->getPricePerItemIncVat()));
+ }
+ $hashedQliroItems[$hash] = $qliroOrderItem;
+
+ if (!isset($hashedQuoteItems[$hash])) {
+ $this->logValidateError('compareQuoteAndQliroOrderItems','hashedQuoteItems failed');
+ return false;
+ }
+
+ if (!$this->compareItems($hashedQuoteItems[$hash], $hashedQliroItems[$hash])) {
+ return false;
+ }
+ }
+ }
+
+ // Try to see a diff between order items converted from quote and from QliroOne order
+ foreach ($quoteItems as $quoteItem) {
+ if (!in_array($quoteItem->getType(), $skipTypes)) {
+ $hash = $quoteItem->getMerchantReference();
+
+ if (!isset($hashedQliroItems[$hash])) {
+ $this->logValidateError('compareQuoteAndQliroOrderItems','$hashedQliroItems failed');
+ return false;
+ }
+
+ if (!$this->compareItems($hashedQuoteItems[$hash], $hashedQliroItems[$hash])) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Compare two QliroOne order items
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $item1
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface $item2
+ * @return bool
+ */
+ private function compareItems(QliroOrderItemInterface $item1, QliroOrderItemInterface $item2)
+ {
+ if ($item1->getPricePerItemExVat() != $item2->getPricePerItemExVat()) {
+ $this->logValidateError(
+ 'compareItems',
+ 'pricePerItemExVat different',
+ ['item1' => $item1->getPricePerItemExVat(), 'item2 => $item2->getPricePerItemExVat()']
+ );
+ return false;
+ }
+
+ if ($item1->getPricePerItemIncVat() != $item2->getPricePerItemIncVat()) {
+ $this->logValidateError(
+ 'compareItems',
+ 'pricePerItemIncVat different',
+ ['item1' => $item1->getPricePerItemIncVat(), 'item2 => $item2->getPricePerItemIncVat()']
+ );
+ return false;
+ }
+
+ if ($item1->getQuantity() != $item2->getQuantity()) {
+ $this->logValidateError(
+ 'compareItems',
+ 'quantity different',
+ ['item1' => $item1->getQuantity(), 'item2 => $item2->getQuantity()']
+ );
+ return false;
+ }
+
+ if ($item1->getType() != $item2->getType()) {
+ $this->logValidateError(
+ 'compareItems',
+ 'type different',
+ ['item1' => $item1->getType(), 'item2 => $item2->getType()']
+ );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param string $function
+ * @param string $reason
+ * @param array $details
+ */
+ private function logValidateError($function, $reason, $details = [])
+ {
+ $this->logManager->debug(
+ 'ValidateOrder',
+ [
+ 'extra' => [
+ 'function' => $function,
+ 'reason' => $reason,
+ 'details' => $details,
+ ],
+ ]
+ );
+ }
+}
diff --git a/Model/QliroOrder/Converter/AddressConverter.php b/Model/QliroOrder/Converter/AddressConverter.php
new file mode 100644
index 0000000..ac646a3
--- /dev/null
+++ b/Model/QliroOrder/Converter/AddressConverter.php
@@ -0,0 +1,59 @@
+ $qliroAddress ? $qliroAddress->getFirstName() : null,
+ 'lastname' => $qliroAddress ? $qliroAddress->getLastName() : null,
+ 'email' => $qliroCustomer? $qliroCustomer->getEmail() : null,
+ 'care_of' => $qliroAddress ? $qliroAddress->getCareOf() : null, // Is ignored for now if no attribute
+ 'street' => $qliroAddress ? $qliroAddress->getStreet() : null,
+ 'telephone' => $qliroCustomer ? $qliroCustomer->getMobileNumber() : null,
+ 'city' => $qliroAddress ? $qliroAddress->getCity() : null,
+ 'postcode' => $qliroAddress ? $qliroAddress->getPostalCode() : null,
+ 'company' => $qliroAddress ? $qliroAddress->getCompanyName() : null,
+ ];
+
+ $changed = false;
+ foreach ($addressData as $key => $value) {
+ if ($value !== null && $address->getData($key) != $value) {
+ $address->setData($key, $value);
+ $changed = true;
+ }
+ }
+
+ if (!$address->getCountryId() && $countryCode !== null) {
+ $address->setCountryId($countryCode);
+ $changed = true;
+ }
+
+ if ($changed && $address->getCustomerAddressId()) {
+ $address->setCustomerAddressId(null);
+ }
+ }
+}
diff --git a/Model/QliroOrder/Converter/CustomerConverter.php b/Model/QliroOrder/Converter/CustomerConverter.php
new file mode 100644
index 0000000..5600b19
--- /dev/null
+++ b/Model/QliroOrder/Converter/CustomerConverter.php
@@ -0,0 +1,75 @@
+addressConverter = $addressConverter;
+ $this->helper = $helper;
+ }
+
+ /**
+ * Convert QliroOne order customer info into quote customer
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $qliroCustomer
+ * @param \Magento\Quote\Model\Quote $quote
+ */
+ public function convert($qliroCustomer, Quote $quote)
+ {
+ if ($qliroCustomer) {
+ $customer = $quote->getCustomer();
+ $qliroAddress = $qliroCustomer->getAddress();
+
+ $customerData = [
+ 'email' => $qliroCustomer->getEmail(),
+ ];
+
+ foreach ($customerData as $key => $value) {
+ if ($value !== null) {
+ $customer->setData($key, $value);
+ }
+ }
+
+ if ($qliroAddress) {
+ $billingAddress = $quote->getBillingAddress();
+ $this->addressConverter->convert($qliroAddress, $qliroCustomer, $billingAddress);
+
+ if (!$quote->isVirtual()) {
+ $shippingAddress = $quote->getShippingAddress();
+ $this->addressConverter->convert($qliroAddress, $qliroCustomer, $shippingAddress);
+ $shippingAddress->setSameAsBilling($this->helper->doAddressesMatch($shippingAddress, $billingAddress));
+ }
+ }
+ }
+ }
+}
diff --git a/Model/QliroOrder/Converter/OrderItemsConverter.php b/Model/QliroOrder/Converter/OrderItemsConverter.php
new file mode 100644
index 0000000..42476e5
--- /dev/null
+++ b/Model/QliroOrder/Converter/OrderItemsConverter.php
@@ -0,0 +1,141 @@
+typePoolHandler = $typePoolHandler;
+ $this->fee = $fee;
+ $this->quoteSourceProvider = $quoteSourceProvider;
+ }
+
+ /**
+ * Convert QliroOne order items into relevant quote items
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $qliroOrderItems
+ * @param \Magento\Quote\Model\Quote $quote
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function convert($qliroOrderItems, Quote $quote)
+ {
+ $feeAmount = 0;
+ $shippingCode = null;
+ $this->quoteSourceProvider->setQuote($quote);
+
+ if (!$quote->isVirtual()) {
+ $shippingCode = $quote->getShippingAddress()->getShippingMethod();
+ }
+
+ foreach ($qliroOrderItems as $orderItem) {
+ switch ($orderItem->getType()) {
+ case QliroOrderItemInterface::TYPE_PRODUCT:
+ $this->typePoolHandler->resolveQuoteItem($orderItem, $this->quoteSourceProvider);
+ break;
+
+ case QliroOrderItemInterface::TYPE_SHIPPING:
+ $shippingCode = $orderItem->getMerchantReference();
+ break;
+
+ case QliroOrderItemInterface::TYPE_DISCOUNT:
+ // Not doing it now
+ break;
+
+ case QliroOrderItemInterface::TYPE_FEE:
+ $feeAmount += $orderItem->getPricePerItemIncVat();
+ $quote->getPayment()->setAdditionalInformation(
+ InvoiceFeeHandler::MERCHANT_REFERENCE_CODE_FIELD,
+ $orderItem->getMerchantReference()
+ );
+ $quote->getPayment()->setAdditionalInformation(
+ InvoiceFeeHandler::MERCHANT_REFERENCE_DESCRIPTION_FIELD,
+ $orderItem->getDescription()
+ );
+ break;
+ }
+ }
+
+ if (!$quote->isVirtual() && $shippingCode) {
+ $this->applyShippingMethod($shippingCode, $quote);
+ }
+
+ $this->fee->setQlirooneFeeInclTax($quote, $feeAmount);
+ }
+
+ /**
+ * @param string $code
+ * @param \Magento\Quote\Model\Quote $quote
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ private function applyShippingMethod($code, Quote $quote)
+ {
+ if (empty($code)) {
+ throw new LocalizedException(__('Invalid shipping method, empty code.'));
+ }
+
+ $rate = $quote->getShippingAddress()->getShippingRateByCode($code);
+
+ if (!$rate) {
+ throw new LocalizedException(__('Invalid shipping method, blank rate.'));
+ }
+
+ if ($quote->isMultipleShippingAddresses()) {
+ throw new LocalizedException(
+ __('There are more than one shipping addresses.')
+ );
+ }
+
+ $extensionAttributes = $quote->getExtensionAttributes();
+
+ if ($extensionAttributes !== null) {
+ $shippingAssignments = $quote->getExtensionAttributes()->getShippingAssignments();
+
+ foreach ($shippingAssignments as $assignment) {
+ $assignment->getShipping()->setMethod($code);
+ }
+ }
+
+ $quote->getShippingAddress()->setShippingMethod($code);
+ }
+}
diff --git a/Model/QliroOrder/Converter/QuoteFromOrderConverter.php b/Model/QliroOrder/Converter/QuoteFromOrderConverter.php
new file mode 100644
index 0000000..1cf698c
--- /dev/null
+++ b/Model/QliroOrder/Converter/QuoteFromOrderConverter.php
@@ -0,0 +1,92 @@
+orderItemsConverter = $orderItemsConverter;
+ $this->customerConverter = $customerConverter;
+ $this->subscription = $subscription;
+ $this->addressConverter = $addressConverter;
+ }
+
+ /**
+ * Convert update shipping methods request into quote
+ *
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderInterface $container
+ * @param \Magento\Quote\Model\Quote $quote
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function convert($container, Quote $quote)
+ {
+ $this->customerConverter->convert($container->getCustomer(), $quote);
+
+ if ($qliroBillingAddress = $container->getBillingAddress()) {
+ $this->addressConverter->convert(
+ $qliroBillingAddress,
+ $container->getCustomer(),
+ $quote->getBillingAddress()
+ );
+ }
+
+ if (!$quote->isVirtual() && ($qliroShippingAddress = $container->getShippingAddress())) {
+ $this->addressConverter->convert(
+ $qliroShippingAddress,
+ $container->getCustomer(),
+ $quote->getShippingAddress()
+ );
+ }
+
+ $this->orderItemsConverter->convert($container->getOrderItems(), $quote);
+
+ $signupForNewsletter = $container->getSignupForNewsletter();
+ if ($signupForNewsletter) {
+ $email = $quote->getCustomer()->getEmail();
+ $this->subscription->addSubscription($email, $quote->getStoreId());
+ }
+ }
+}
diff --git a/Model/QliroOrder/Converter/QuoteFromShippingMethodsConverter.php b/Model/QliroOrder/Converter/QuoteFromShippingMethodsConverter.php
new file mode 100644
index 0000000..7aeac0e
--- /dev/null
+++ b/Model/QliroOrder/Converter/QuoteFromShippingMethodsConverter.php
@@ -0,0 +1,71 @@
+addressConverter = $addressConverter;
+ $this->helper = $helper;
+ }
+
+ /**
+ * Convert update shipping methods request into quote
+ *
+ * @param \Qliro\QliroOne\Api\Data\UpdateShippingMethodsNotificationInterface $container
+ * @param \Magento\Quote\Model\Quote $quote
+ */
+ public function convert(UpdateShippingMethodsNotificationInterface $container, Quote $quote)
+ {
+ $billingAddress = $quote->getBillingAddress();
+
+ $this->addressConverter->convert(
+ $container->getShippingAddress(),
+ $container->getCustomer(),
+ $billingAddress,
+ $container->getCountryCode()
+ );
+
+ if (!$quote->isVirtual()) {
+ $shippingAddress = $quote->getShippingAddress();
+
+ $this->addressConverter->convert(
+ $container->getShippingAddress(),
+ $container->getCustomer(),
+ $shippingAddress,
+ $container->getCountryCode()
+ );
+ $shippingAddress->setSameAsBilling($this->helper->doAddressesMatch($shippingAddress, $billingAddress));
+ }
+ }
+}
diff --git a/Model/QliroOrder/Converter/QuoteFromValidateConverter.php b/Model/QliroOrder/Converter/QuoteFromValidateConverter.php
new file mode 100644
index 0000000..ba84a61
--- /dev/null
+++ b/Model/QliroOrder/Converter/QuoteFromValidateConverter.php
@@ -0,0 +1,47 @@
+addressConverter = $addressConverter;
+ }
+
+ /**
+ * Convert validate order request into quote
+ *
+ * @param \Qliro\QliroOne\Api\Data\ValidateOrderNotificationInterface $container
+ * @param \Magento\Quote\Model\Quote $quote
+ */
+ public function convert(ValidateOrderNotificationInterface $container, Quote $quote)
+ {
+ if ($quote->isVirtual()) {
+ $shippingAddress = $quote->getShippingAddress();
+ $shippingAddress->setShippingMethod($container->getSelectedShippingMethod());
+ $this->addressConverter->convert($container->getShippingAddress(), $container->getCustomer(), $shippingAddress);
+ }
+ }
+}
diff --git a/Model/QliroOrder/CreateRequest.php b/Model/QliroOrder/CreateRequest.php
new file mode 100644
index 0000000..6a24f97
--- /dev/null
+++ b/Model/QliroOrder/CreateRequest.php
@@ -0,0 +1,683 @@
+merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return CreateRequest
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantApiKey()
+ {
+ return $this->merchantApiKey;
+ }
+
+ /**
+ * @param string $merchantApiKey
+ * @return CreateRequest
+ */
+ public function setMerchantApiKey($merchantApiKey)
+ {
+ $this->merchantApiKey = $merchantApiKey;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCountry()
+ {
+ return $this->country;
+ }
+
+ /**
+ * @param string $country
+ * @return CreateRequest
+ */
+ public function setCountry($country)
+ {
+ $this->country = $country;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCurrency()
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @param string $currency
+ * @return CreateRequest
+ */
+ public function setCurrency($currency)
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getLanguage()
+ {
+ return $this->language;
+ }
+
+ /**
+ * @param string $language
+ * @return CreateRequest
+ */
+ public function setLanguage($language)
+ {
+ $this->language = $language;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantConfirmationUrl()
+ {
+ return $this->merchantConfirmationUrl;
+ }
+
+ /**
+ * @param string $merchantConfirmationUrl
+ * @return CreateRequest
+ */
+ public function setMerchantConfirmationUrl($merchantConfirmationUrl)
+ {
+ $this->merchantConfirmationUrl = $merchantConfirmationUrl;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantTermsUrl()
+ {
+ return $this->merchantTermsUrl;
+ }
+
+ /**
+ * @param string $merchantTermsUrl
+ * @return CreateRequest
+ */
+ public function setMerchantTermsUrl($merchantTermsUrl)
+ {
+ $this->merchantTermsUrl = $merchantTermsUrl;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[]
+ */
+ public function getOrderItems()
+ {
+ return $this->orderItems;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $orderItems
+ * @return CreateRequest
+ */
+ public function setOrderItems($orderItems)
+ {
+ $this->orderItems = $orderItems;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantCheckoutStatusPushUrl()
+ {
+ return $this->merchantCheckoutStatusPushUrl;
+ }
+
+ /**
+ * @param string $merchantCheckoutStatusPushUrl
+ * @return CreateRequest
+ */
+ public function setMerchantCheckoutStatusPushUrl($merchantCheckoutStatusPushUrl)
+ {
+ if (self::TEST_LOCAL_URL && $merchantCheckoutStatusPushUrl) {
+ $merchantCheckoutStatusPushUrl = str_replace('http:', 'https:', $merchantCheckoutStatusPushUrl);
+ }
+ $this->merchantCheckoutStatusPushUrl = $merchantCheckoutStatusPushUrl;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantOrderManagementStatusPushUrl()
+ {
+ return $this->merchantOrderManagementStatusPushUrl;
+ }
+
+ /**
+ * @param string $merchantOrderManagementStatusPushUrl
+ * @return CreateRequest
+ */
+ public function setMerchantOrderManagementStatusPushUrl($merchantOrderManagementStatusPushUrl)
+ {
+ if (self::TEST_LOCAL_URL && $merchantOrderManagementStatusPushUrl) {
+ $merchantOrderManagementStatusPushUrl = str_replace('http:', 'https:', $merchantOrderManagementStatusPushUrl);
+ }
+ $this->merchantOrderManagementStatusPushUrl = $merchantOrderManagementStatusPushUrl;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantOrderValidationUrl()
+ {
+ return $this->merchantOrderValidationUrl;
+ }
+
+ /**
+ * @param string $merchantOrderValidationUrl
+ * @return CreateRequest
+ */
+ public function setMerchantOrderValidationUrl($merchantOrderValidationUrl)
+ {
+ if (self::TEST_LOCAL_URL && $merchantOrderValidationUrl) {
+ $merchantOrderValidationUrl = str_replace('http:', 'https:', $merchantOrderValidationUrl);
+ }
+ $this->merchantOrderValidationUrl = $merchantOrderValidationUrl;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantOrderAvailableShippingMethodsUrl()
+ {
+ return $this->merchantOrderAvailableShippingMethodsUrl;
+ }
+
+ /**
+ * @param string $merchantOrderAvailableShippingMethodsUrl
+ * @return CreateRequest
+ */
+ public function setMerchantOrderAvailableShippingMethodsUrl($merchantOrderAvailableShippingMethodsUrl)
+ {
+ if (self::TEST_LOCAL_URL && $merchantOrderAvailableShippingMethodsUrl) {
+ $merchantOrderAvailableShippingMethodsUrl = str_replace('http:', 'https:', $merchantOrderAvailableShippingMethodsUrl);
+ }
+ $this->merchantOrderAvailableShippingMethodsUrl = $merchantOrderAvailableShippingMethodsUrl;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMerchantIntegrityPolicyUrl()
+ {
+ return $this->merchantIntegrityPolicyUrl;
+ }
+
+ /**
+ * @param string $merchantIntegrityPolicyUrl
+ * @return CreateRequest
+ */
+ public function setMerchantIntegrityPolicyUrl($merchantIntegrityPolicyUrl)
+ {
+ if (self::TEST_LOCAL_URL && $merchantIntegrityPolicyUrl) {
+ $merchantIntegrityPolicyUrl = str_replace('http:', 'https:', $merchantIntegrityPolicyUrl);
+ }
+ $this->merchantIntegrityPolicyUrl = $merchantIntegrityPolicyUrl;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getBackgroundColor()
+ {
+ return $this->backgroundColor;
+ }
+
+ /**
+ * @param string $backgroundColor
+ * @return CreateRequest
+ */
+ public function setBackgroundColor($backgroundColor)
+ {
+ $this->backgroundColor = $backgroundColor;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getPrimaryColor()
+ {
+ return $this->primaryColor;
+ }
+
+ /**
+ * @param string $primaryColor
+ * @return CreateRequest
+ */
+ public function setPrimaryColor($primaryColor)
+ {
+ $this->primaryColor = $primaryColor;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCallToActionColor()
+ {
+ return $this->callToActionColor;
+ }
+
+ /**
+ * @param string $callToActionColor
+ * @return CreateRequest
+ */
+ public function setCallToActionColor($callToActionColor)
+ {
+ $this->callToActionColor = $callToActionColor;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getCallToActionHoverColor()
+ {
+ return $this->callToActionHoverColor;
+ }
+
+ /**
+ * @param string $callToActionHoverColor
+ * @return CreateRequest
+ */
+ public function setCallToActionHoverColor($callToActionHoverColor)
+ {
+ $this->callToActionHoverColor = $callToActionHoverColor;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getCornerRadius()
+ {
+ return $this->cornerRadius;
+ }
+
+ /**
+ * @param int $cornerRadius
+ * @return CreateRequest
+ */
+ public function setCornerRadius($cornerRadius)
+ {
+ $this->cornerRadius = $cornerRadius;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getButtonCornerRadius()
+ {
+ return $this->buttonCornerRadius;
+ }
+
+ /**
+ * @param int $buttonCornerRadius
+ * @return CreateRequest
+ */
+ public function setButtonCornerRadius($buttonCornerRadius)
+ {
+ $this->buttonCornerRadius = $buttonCornerRadius;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface
+ */
+ public function getCustomerInformation()
+ {
+ return $this->customerInformation;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerInterface $customerInformation
+ * @return CreateRequest
+ */
+ public function setCustomerInformation($customerInformation)
+ {
+ $this->customerInformation = $customerInformation;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getAvailableShippingMethods()
+ {
+ return $this->availableShippingMethods;
+ }
+
+ /**
+ * @param string $availableShippingMethods
+ * @return CreateRequest
+ */
+ public function setAvailableShippingMethods($availableShippingMethods)
+ {
+ $this->availableShippingMethods = $availableShippingMethods;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigInterface
+ */
+ public function getShippingConfiguration()
+ {
+ return $this->shippingConfiguration;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigInterface $shippingConfiguration
+ * @return CreateRequest
+ */
+ public function setShippingConfiguration($shippingConfiguration)
+ {
+ $this->shippingConfiguration = $shippingConfiguration;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getMinimumCustomerAge()
+ {
+ return $this->minimumCustomerAge;
+ }
+
+ /**
+ * @param int $minimumCustomerAge
+ * @return CreateRequest
+ */
+ public function setMinimumCustomerAge($minimumCustomerAge)
+ {
+ $this->minimumCustomerAge = $minimumCustomerAge;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getAskForNewsletterSignup()
+ {
+ return $this->askForNewsletterSignup;
+ }
+
+ /**
+ * @param bool $askForNewsletterSignup
+ * @return CreateRequest
+ */
+ public function setAskForNewsletterSignup($askForNewsletterSignup)
+ {
+ $this->askForNewsletterSignup = $askForNewsletterSignup;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getRequireIdentityVerification()
+ {
+ return $this->requireIdentityVerification;
+ }
+
+ /**
+ * @param bool $requireIdentityVerification
+ * @return CreateRequest
+ */
+ public function setRequireIdentityVerification($requireIdentityVerification)
+ {
+ $this->requireIdentityVerification = $requireIdentityVerification;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Customer.php b/Model/QliroOrder/Customer.php
new file mode 100644
index 0000000..9df9e22
--- /dev/null
+++ b/Model/QliroOrder/Customer.php
@@ -0,0 +1,197 @@
+email;
+ }
+
+ /**
+ * @param string $email
+ * @return Customer
+ */
+ public function setEmail($email)
+ {
+ $this->email = $email;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getMobileNumber()
+ {
+ return $this->mobileNumber;
+ }
+
+ /**
+ * @param string $mobileNumber
+ * @return Customer
+ */
+ public function setMobileNumber($mobileNumber)
+ {
+ $this->mobileNumber = $mobileNumber;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface
+ */
+ public function getAddress()
+ {
+ return $this->address;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderCustomerAddressInterface $address
+ * @return Customer
+ */
+ public function setAddress($address)
+ {
+ $this->address = $address;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getLockCustomerInformation()
+ {
+ return $this->lockCustomerInformation;
+ }
+
+ /**
+ * @param bool $lockCustomerInformation
+ * @return Customer
+ */
+ public function setLockCustomerInformation($lockCustomerInformation)
+ {
+ $this->lockCustomerInformation = $lockCustomerInformation;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getLockCustomerEmail()
+ {
+ return $this->lockCustomerEmail;
+ }
+
+ /**
+ * @param bool $lockCustomerEmail
+ * @return Customer
+ */
+ public function setLockCustomerEmail($lockCustomerEmail)
+ {
+ $this->lockCustomerEmail = $lockCustomerEmail;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getLockCustomerMobileNumber()
+ {
+ return $this->lockCustomerMobileNumber;
+ }
+
+ /**
+ * @param bool $lockCustomerMobileNumber
+ * @return Customer
+ */
+ public function setLockCustomerMobileNumber($lockCustomerMobileNumber)
+ {
+ $this->lockCustomerMobileNumber = $lockCustomerMobileNumber;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getLockCustomerAddress()
+ {
+ return $this->lockCustomerAddress;
+ }
+
+ /**
+ * @param bool $lockCustomerAddress
+ * @return Customer
+ */
+ public function setLockCustomerAddress($lockCustomerAddress)
+ {
+ $this->lockCustomerAddress = $lockCustomerAddress;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/IdentityVerification.php b/Model/QliroOrder/IdentityVerification.php
new file mode 100644
index 0000000..231990c
--- /dev/null
+++ b/Model/QliroOrder/IdentityVerification.php
@@ -0,0 +1,63 @@
+requireIdentityVerification;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getIdentityVerified()
+ {
+ return $this->identityVerified;
+ }
+
+ /**
+ * @param bool $value
+ * @return $this
+ */
+ public function setRequireIdentityVerification($value)
+ {
+ $this->requireIdentityVerification = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param bool $value
+ * @return $this
+ */
+ public function setIdentityVerified($value)
+ {
+ $this->identityVerified = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/Item.php b/Model/QliroOrder/Item.php
new file mode 100644
index 0000000..973b953
--- /dev/null
+++ b/Model/QliroOrder/Item.php
@@ -0,0 +1,200 @@
+merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return Item
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @param string $type
+ * @return Item
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return int
+ */
+ public function getQuantity()
+ {
+ return $this->quantity;
+ }
+
+ /**
+ * @param int $quantity
+ * @return Item
+ */
+ public function setQuantity($quantity)
+ {
+ $this->quantity = $quantity;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getPricePerItemIncVat()
+ {
+ return $this->pricePerItemIncVat;
+ }
+
+ /**
+ * @param float $pricePerItemIncVat
+ * @return Item
+ */
+ public function setPricePerItemIncVat($pricePerItemIncVat)
+ {
+ $this->pricePerItemIncVat = $pricePerItemIncVat;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getPricePerItemExVat()
+ {
+ return $this->pricePerItemExVat;
+ }
+
+ /**
+ * @param float $pricePerItemExVat
+ * @return Item
+ */
+ public function setPricePerItemExVat($pricePerItemExVat)
+ {
+ $this->pricePerItemExVat = $pricePerItemExVat;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @param string $description
+ * @return Item
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return array
+ */
+ public function getMetaData()
+ {
+ return $this->metaData;
+ }
+
+ /**
+ * @param array $metaData
+ * @return Item
+ */
+ public function setMetaData($metaData)
+ {
+ $this->metaData = $metaData;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/PaymentMethod.php b/Model/QliroOrder/PaymentMethod.php
new file mode 100644
index 0000000..2a34a9a
--- /dev/null
+++ b/Model/QliroOrder/PaymentMethod.php
@@ -0,0 +1,67 @@
+paymentMethodName;
+ }
+
+ /**
+ * @param string $paymentMethodName
+ * @return PaymentMethod
+ */
+ public function setPaymentMethodName($paymentMethodName)
+ {
+ $this->paymentMethodName = $paymentMethodName;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getPaymentTypeCode()
+ {
+ return $this->paymentTypeCode;
+ }
+
+ /**
+ * @param string $paymentTypeCode
+ * @return PaymentMethod
+ */
+ public function setPaymentTypeCode($paymentTypeCode)
+ {
+ $this->paymentTypeCode = $paymentTypeCode;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/ReferenceHashResolver.php b/Model/QliroOrder/ReferenceHashResolver.php
new file mode 100644
index 0000000..ee1f771
--- /dev/null
+++ b/Model/QliroOrder/ReferenceHashResolver.php
@@ -0,0 +1,39 @@
+unifaun;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setUnifaun($value)
+ {
+ $this->unifaun = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/ShippingConfig/Unifaun.php b/Model/QliroOrder/ShippingConfig/Unifaun.php
new file mode 100644
index 0000000..8fa0784
--- /dev/null
+++ b/Model/QliroOrder/ShippingConfig/Unifaun.php
@@ -0,0 +1,62 @@
+checkoutId;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getTags()
+ {
+ return $this->tags;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setCheckoutId($value)
+ {
+ $this->checkoutId = $value;
+
+ return $this;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setTags($value)
+ {
+ $this->tags = $value;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/ShippingMethod.php b/Model/QliroOrder/ShippingMethod.php
new file mode 100644
index 0000000..3630b8a
--- /dev/null
+++ b/Model/QliroOrder/ShippingMethod.php
@@ -0,0 +1,275 @@
+merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return $this
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getDisplayName()
+ {
+ return $this->displayName;
+ }
+
+ /**
+ * @param string $displayName
+ * @return $this
+ */
+ public function setDisplayName($displayName)
+ {
+ $this->displayName = $displayName;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getPriceIncVat()
+ {
+ return $this->priceIncVat;
+ }
+
+ /**
+ * @param float $priceIncVat
+ * @return $this
+ */
+ public function setPriceIncVat($priceIncVat)
+ {
+ $this->priceIncVat = $priceIncVat;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return float
+ */
+ public function getPriceExVat()
+ {
+ return $this->priceExVat;
+ }
+
+ /**
+ * @param float $priceExVat
+ * @return $this
+ */
+ public function setPriceExVat($priceExVat)
+ {
+ $this->priceExVat = $priceExVat;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return array
+ */
+ public function getDescriptions()
+ {
+ return $this->descriptions;
+ }
+
+ /**
+ * @param array $descriptions
+ * @return $this
+ */
+ public function setDescriptions($descriptions)
+ {
+ $this->descriptions = $descriptions;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getBrand()
+ {
+ return $this->brand;
+ }
+
+ /**
+ * @param string $brand
+ * @return $this
+ */
+ public function setBrand($brand)
+ {
+ $this->brand = $brand;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getSupportsAccessCode()
+ {
+ return $this->supportsAccessCode;
+ }
+
+ /**
+ * @param bool $supportsAccessCode
+ * @return $this
+ */
+ public function setSupportsAccessCode($supportsAccessCode)
+ {
+ $this->supportsAccessCode = $supportsAccessCode;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderShippingMethodOptionInterface[]
+ */
+ public function getSecondaryOptions()
+ {
+ return $this->secondaryOptions;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderShippingMethodOptionInterface[] $secondaryOptions
+ * @return $this
+ */
+ public function setSecondaryOptions($secondaryOptions)
+ {
+ $this->secondaryOptions = $secondaryOptions;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getShippingFeeMerchantReference()
+ {
+ return $this->shippingFeeMerchantReference;
+ }
+
+ /**
+ * @param string $shippingFeeMerchantReference
+ * @return $this
+ */
+ public function setShippingFeeMerchantReference($shippingFeeMerchantReference)
+ {
+ $this->shippingFeeMerchantReference = $shippingFeeMerchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return bool
+ */
+ public function getSupportsDynamicSecondaryOptions()
+ {
+ return $this->supportsDynamicSecondaryOptions;
+ }
+
+ /**
+ * @param bool $supportsDynamicSecondaryOptions
+ * @return $this
+ */
+ public function setSupportsDynamicSecondaryOptions($supportsDynamicSecondaryOptions)
+ {
+ $this->supportsDynamicSecondaryOptions = $supportsDynamicSecondaryOptions;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/ShippingMethod/Option.php b/Model/QliroOrder/ShippingMethod/Option.php
new file mode 100644
index 0000000..37ed76e
--- /dev/null
+++ b/Model/QliroOrder/ShippingMethod/Option.php
@@ -0,0 +1,93 @@
+merchantReference;
+ }
+
+ /**
+ * @param string $merchantReference
+ * @return $this
+ */
+ public function setMerchantReference($merchantReference)
+ {
+ $this->merchantReference = $merchantReference;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return string
+ */
+ public function getDisplayName()
+ {
+ return $this->displayName;
+ }
+
+ /**
+ * @param string $displayName
+ * @return $this
+ */
+ public function setDisplayName($displayName)
+ {
+ $this->displayName = $displayName;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return array
+ */
+ public function getDescriptions()
+ {
+ return $this->descriptions;
+ }
+
+ /**
+ * @param array $descriptions
+ * @return $this
+ */
+ public function setDescriptions($descriptions)
+ {
+ $this->descriptions = $descriptions;
+
+ return $this;
+ }
+}
diff --git a/Model/QliroOrder/UpdateRequest.php b/Model/QliroOrder/UpdateRequest.php
new file mode 100644
index 0000000..97f78af
--- /dev/null
+++ b/Model/QliroOrder/UpdateRequest.php
@@ -0,0 +1,113 @@
+orderItems;
+ }
+
+ /**
+ * @return string
+ */
+ public function getAvailableShippingMethods()
+ {
+ return $this->availableShippingMethods;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getRequireIdentityVerification()
+ {
+ return $this->requireIdentityVerification;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderItemInterface[] $value
+ * @return $this
+ */
+ public function setOrderItems($value)
+ {
+ $this->orderItems = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param array $value
+ * @return $this
+ */
+ public function setAvailableShippingMethods($value)
+ {
+ $this->availableShippingMethods = $value;
+
+ return $this;
+ }
+
+ /**
+ * @param bool $value
+ * @return $this
+ */
+ public function setRequireIdentityVerification($value)
+ {
+ $this->requireIdentityVerification = $value;
+
+ return $this;
+ }
+
+ /**
+ * Getter.
+ *
+ * @return \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigInterface
+ */
+ public function getShippingConfiguration()
+ {
+ return $this->shippingConfiguration;
+ }
+
+ /**
+ * @param \Qliro\QliroOne\Api\Data\QliroOrderShippingConfigInterface $shippingConfiguration
+ * @return CreateRequest
+ */
+ public function setShippingConfiguration($shippingConfiguration)
+ {
+ $this->shippingConfiguration = $shippingConfiguration;
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Model/Quote/Address/Total/Fee.php b/Model/Quote/Address/Total/Fee.php
new file mode 100644
index 0000000..5d01c4a
--- /dev/null
+++ b/Model/Quote/Address/Total/Fee.php
@@ -0,0 +1,131 @@
+quoteValidator = $quoteValidator;
+ $this->checkoutSession = $checkoutSession;
+ $this->fee = $fee;
+ }
+
+ /**
+ * Collect totals
+ *
+ * @param Quote $quote
+ * @param ShippingAssignmentInterface $shippingAssignment
+ * @param Total $total
+ * @return $this
+ */
+ public function collect(
+ Quote $quote,
+ ShippingAssignmentInterface $shippingAssignment,
+ Total $total
+ ) {
+ parent::collect($quote, $shippingAssignment, $total);
+
+ $total->setQlirooneFee(0);
+ $total->setBaseQlirooneFee(0);
+ $total->setQlirooneFeeExclTax(0);
+ $total->setBaseQlirooneFeeExclTax(0);
+
+ $total->setTotalAmount(Config::TOTALS_FEE_CODE, 0);
+ $total->setBaseTotalAmount(Config::TOTALS_BASE_FEE_CODE, 0);
+
+ if ($quote->isVirtual()) {
+ if ($shippingAssignment->getShipping()->getAddress()->getAddressType() != Address::TYPE_BILLING) {
+ return $this;
+ }
+ } else {
+ if (!count($shippingAssignment->getItems())) {
+ return $this;
+ }
+ }
+
+ $fee = $this->fee->getQlirooneFeeInclTax($quote);
+ $baseFee = $this->fee->getBaseQlirooneFeeInclTax($quote);
+
+ $feeExclTax = $this->fee->getQlirooneFeeExclTax($quote);
+ $baseFeeExclTax = $this->fee->getBaseQlirooneFeeExclTax($quote);
+
+ $total->setQlirooneFee($fee);
+ $total->setBaseQlirooneFee($baseFee);
+ $total->setQlirooneFeeExclTax($feeExclTax);
+ $total->setBaseQlirooneFeeExclTax($baseFeeExclTax);
+
+ $total->setTotalAmount(Config::TOTALS_FEE_CODE, $feeExclTax);
+ $total->setBaseTotalAmount(Config::TOTALS_BASE_FEE_CODE, $baseFeeExclTax);
+
+ return $this;
+ }
+
+ /**
+ * Assign subtotal amount and label to address object
+ *
+ * @param Quote $quote
+ * @param Total $total
+ * @return array
+ */
+ public function fetch(Quote $quote, Total $total)
+ {
+ return $this->fee->getFeeArray($quote, $total->getQlirooneFee());
+ }
+
+ /**
+ * Get Subtotal label. Doubt this is ever used...
+ *
+ * @return \Magento\Framework\Phrase
+ */
+ public function getLabel()
+ {
+ return __('Payment Fee');
+ }
+}
\ No newline at end of file
diff --git a/Model/Quote/Address/Total/Tax.php b/Model/Quote/Address/Total/Tax.php
new file mode 100644
index 0000000..9471902
--- /dev/null
+++ b/Model/Quote/Address/Total/Tax.php
@@ -0,0 +1,116 @@
+setCode('tax_qliroone_fee');
+ $this->config = $config;
+ $this->fee = $fee;
+ }
+
+ /**
+ * Collect Payment Fee and add it to tax calculation
+ *
+ * @param Quote $quote
+ * @param ShippingAssignmentInterface $shippingAssignment
+ * @param Total $total
+ * @return $this
+ */
+ public function collect(
+ Quote $quote,
+ ShippingAssignmentInterface $shippingAssignment,
+ Total $total
+ ) {
+ $total->setQlirooneFeeTax(0);
+ $total->setBaseQlirooneFeeTax(0);
+
+ if ($quote->isVirtual()) {
+ if ($shippingAssignment->getShipping()->getAddress()->getAddressType() != Address::TYPE_BILLING) {
+ return $this;
+ }
+ } else {
+ if ($shippingAssignment->getShipping()->getAddress()->getAddressType() != Address::TYPE_SHIPPING) {
+ return $this;
+ }
+ }
+
+ $productTaxClassId = $this->config->getFeeTaxClass($quote->getStore());
+
+ $address = $shippingAssignment->getShipping()->getAddress();
+
+ $fee = $total->getQlirooneFee();
+ $baseFee = $total->getBaseQlirooneFee();
+
+ $feeExclTax = $total->getQlirooneFeeExclTax();
+ $baseFeeExclTax = $total->getBaseQlirooneFeeExclTax();
+
+ $feeAmountTax = $fee - $feeExclTax;
+ $feeBaseAmountTax = $baseFee - $baseFeeExclTax;
+
+ $associatedTaxables = $address->getAssociatedTaxables();
+ if (!$associatedTaxables) {
+ $associatedTaxables = [];
+ }
+
+ $associatedTaxables[] = [
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_TYPE => Config::TOTALS_FEE_CODE,
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_CODE => Config::TOTALS_FEE_CODE,
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_UNIT_PRICE => $fee,
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_BASE_UNIT_PRICE => $baseFee,
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_QUANTITY => 1,
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_TAX_CLASS_ID => $productTaxClassId,
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_PRICE_INCLUDES_TAX => true,
+ CommonTaxCollector::KEY_ASSOCIATED_TAXABLE_ASSOCIATION_ITEM_CODE
+ => CommonTaxCollector::ASSOCIATION_ITEM_CODE_FOR_QUOTE,
+ ];
+
+ $address->setAssociatedTaxables($associatedTaxables);
+
+ $total->setQlirooneFeeTax($feeAmountTax);
+ $total->setBaseQlirooneFeeTax($feeBaseAmountTax);
+
+ return $this;
+ }
+
+ /**
+ * Assign Payment Fee tax totals and labels to address object
+ *
+ * @param Quote $quote
+ * @param Total $total
+ * @return null
+ */
+ public function fetch(Quote $quote, Total $total)
+ {
+ return null;
+ }
+}
diff --git a/Model/Quote/Agent.php b/Model/Quote/Agent.php
new file mode 100644
index 0000000..a8c3890
--- /dev/null
+++ b/Model/Quote/Agent.php
@@ -0,0 +1,215 @@
+checkoutSession = $checkoutSession;
+ $this->cookieManager = $cookieManager;
+ $this->cookieMetadataFactory = $cookieMetadataFactory;
+ $this->linkRepository = $linkRepository;
+ $this->quoteRepository = $quoteRepository;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Store specific quote in a Quote cookie agent
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ */
+ public function store(Quote $quote)
+ {
+ $this->logManager->addTag('cookies');
+
+ try {
+ $link = $this->linkRepository->getByQuoteId($quote->getId());
+ $merchantReference = $link->getReference();
+ $this->logManager->setMerchantReference($merchantReference);
+
+ $cookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata();
+ $cookieMetadata->setDuration(self::COOKIE_LIFETIME);
+ $cookieMetadata->setPath('/');
+
+ $this->cookieManager->setPublicCookie(self::COOKIE_NAME, $merchantReference, $cookieMetadata);
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_merchant_reference' => $merchantReference ?? null,
+ ]
+ ]
+ );
+ }
+
+ $this->logManager->removeTag('cookies');
+ }
+
+ /**
+ * Return the current quote if relevant, otherwise fetch a quote previously stored by the Quote cookie agent.
+ * Lazy-loadable for better performance.
+ *
+ * @return \Magento\Quote\Model\Quote|null
+ */
+ public function fetchRelevantQuote()
+ {
+ if (!$this->relevantQuote) {
+ $this->logManager->addTag('cookies');
+
+ $quote = $this->checkoutSession->getQuote();
+
+ if ($this->isQuoteRelevant($quote)) {
+ return $quote;
+ }
+
+ $quote = null;
+
+ try {
+ $merchantReference = $this->cookieManager->getCookie(self::COOKIE_NAME);
+ $link = $this->linkRepository->getByReference($merchantReference);
+ $this->logManager->setMerchantReference($merchantReference);
+
+ /** @var \Magento\Quote\Model\Quote $quote */
+ $quote = $this->quoteRepository->get($link->getQuoteId());
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_merchant_reference' => $merchantReference ?? null,
+ ]
+ ]
+ );
+ }
+
+ $this->logManager->removeTag('cookies');
+ $this->relevantQuote = $quote;
+ }
+
+ return $this->relevantQuote;
+ }
+
+ /**
+ * Clear the quote if it's stored
+ */
+ public function clear()
+ {
+ $this->logManager->addTag('cookies');
+ $merchantReference = $this->cookieManager->getCookie(self::COOKIE_NAME);
+
+ if ($merchantReference) {
+ $this->logManager->setMerchantReference($merchantReference);
+ }
+
+ try {
+ $cookieMetadata = $this->cookieMetadataFactory->createPublicCookieMetadata();
+ $cookieMetadata->setDuration(0);
+ $cookieMetadata->setPath('/');
+ $this->cookieManager->deleteCookie(self::COOKIE_NAME, $cookieMetadata);
+ } catch (\Exception $exception) {
+ $this->logManager->critical(
+ $exception,
+ [
+ 'extra' => [
+ 'qliro_merchant_reference' => $merchantReference ?? null,
+ ]
+ ]
+ );
+ }
+
+ $this->logManager->removeTag('cookies');
+ }
+
+ /**
+ * Check if the quote relevant for QliroOne Checkout
+ *
+ * @param \Magento\Quote\Model\Quote $quote
+ * @return bool
+ */
+ private function isQuoteRelevant(Quote $quote)
+ {
+ if (empty($quote->getAllVisibleItems())) {
+ return false;
+ }
+
+ try {
+ $this->linkRepository->getByQuoteId($quote->getId());
+ } catch (NoSuchEntityException $exception) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Model/ResourceModel/Link.php b/Model/ResourceModel/Link.php
new file mode 100644
index 0000000..f9759db
--- /dev/null
+++ b/Model/ResourceModel/Link.php
@@ -0,0 +1,43 @@
+_init(self::TABLE_LINK, LinkModel::FIELD_ID);
+ }
+
+ /**
+ * Update the timestamp on every save
+ *
+ * @param \Magento\Framework\DataObject $object
+ */
+ public function beforeSave(DataObject $object)
+ {
+ $object->setData('updated_at', new \Zend_Db_Expr('NOW()'));
+
+ parent::beforeSave($object);
+ }
+}
diff --git a/Model/ResourceModel/Link/Collection.php b/Model/ResourceModel/Link/Collection.php
new file mode 100644
index 0000000..72a3645
--- /dev/null
+++ b/Model/ResourceModel/Link/Collection.php
@@ -0,0 +1,28 @@
+_init(LinkModel::class, LinkResource::class);
+ }
+}
diff --git a/Model/ResourceModel/Lock.php b/Model/ResourceModel/Lock.php
new file mode 100644
index 0000000..78f8bd5
--- /dev/null
+++ b/Model/ResourceModel/Lock.php
@@ -0,0 +1,151 @@
+logManager = $logManager;
+ $this->helper = $helper;
+ }
+
+ /**
+ * @var \Qliro\QliroOne\Helper\Data
+ */
+ private $helper;
+
+ /**
+ * Dummy init method
+ */
+ protected function _construct()
+ {
+ $this->_init(self::TABLE_LOCK, self::FIELD_ID);
+ }
+
+ /**
+ * Perform a lock and check result.
+ * - true, the lock was successful
+ * - false, the lock has failed
+ *
+ * @param string $qliroOrderId
+ * @param bool $checkProcess
+ * @return bool
+ */
+ public function lock($qliroOrderId, $checkProcess = true)
+ {
+ /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */
+ $connection = $this->getConnection();
+
+ $retireAfter = self::LOCK_EXPIRATION * 60;
+
+ $where = [
+ sprintf('%s < NOW() - ?' , self::FIELD_CREATED_AT) => $retireAfter
+ ];
+ $rows = $connection->delete($this->getTable(self::TABLE_LOCK), $where);
+
+ if ($rows > 0) {
+ $this->logManager->notice('lock: retired {count} locks', ['count' => $rows]);
+ }
+
+ try {
+ $rows = $connection->insert($this->getTable(self::TABLE_LOCK), [
+ self::FIELD_ID => $qliroOrderId,
+ self::FIELD_PROCESS_ID => $this->helper->getPid(),
+ self::FIELD_CREATED_AT => new \Zend_Db_Expr('NOW()')
+ ]);
+ } catch (\Exception $e) {
+ if ($checkProcess) {
+ $select = $connection->select()
+ ->from($this->getTable(self::TABLE_LOCK), self::FIELD_PROCESS_ID)
+ ->where(sprintf('%s = :id', self::FIELD_ID ));
+ $row = $connection->fetchRow($select, [':id' => $qliroOrderId]);
+ if (!empty($row[self::FIELD_PROCESS_ID])) {
+ $pid = $row[self::FIELD_PROCESS_ID];
+ if (!$this->helper->isProcessAlive($pid)) {
+ $rows = $this->unlock($qliroOrderId, true);
+ if ($rows > 0) {
+ $this->logManager->notice('lock: retired lock for dead process {pid}', ['pid' => $pid]);
+ }
+
+ return $this->lock($qliroOrderId, false);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ return $rows > 0;
+ }
+
+ /**
+ * Perform an unlock and check result.
+ * - true, the unlock was successful
+ * - false, the unlock has failed
+ *
+ * @param string $qliroOrderId
+ * @param bool $forced Attempt to remove lock even if this is a different process.
+ * @return bool
+ */
+ public function unlock($qliroOrderId, $forced = false)
+ {
+ /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */
+ $connection = $this->getConnection();
+
+ $where = [sprintf('%s = ?', self::FIELD_ID) => $qliroOrderId];
+ if (!$forced) {
+ $where[sprintf('%s = ?', self::FIELD_PROCESS_ID)] = $this->helper->getPid();
+ }
+ try {
+ $rows = $connection->delete($this->getTable(self::TABLE_LOCK), $where);
+ } catch (\Exception $e) {
+ return false;
+ }
+ if ($rows == 0) {
+ $this->logManager->notice('unlock: no lock found for {id}', ['id' => $qliroOrderId]);
+
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Model/ResourceModel/LogRecord.php b/Model/ResourceModel/LogRecord.php
new file mode 100644
index 0000000..b22b174
--- /dev/null
+++ b/Model/ResourceModel/LogRecord.php
@@ -0,0 +1,68 @@
+connectionProvider = $connectionProvider;
+ }
+
+ protected function _construct()
+ {
+ $this->_init(self::TABLE_LOG, LogRecordModel::FIELD_ID);
+ }
+
+ /**
+ * When we have a merchantReference, we should patch any recent logging to ensure that the reference is present
+ * on all log lines.
+ *
+ * @param string $merchantReference
+ */
+ public function patchMerchantReference($merchantReference)
+ {
+ /** @var \Magento\Framework\DB\Adapter\AdapterInterface $connection */
+ $connection = $this->connectionProvider->getConnection();
+
+ $where = [
+ sprintf("%s = ?", LogRecordInterface::FIELD_PROCESS_ID) => \getmypid(),
+ sprintf("%s = ?", LogRecordInterface::FIELD_REFERENCE) => '',
+ sprintf("%s >= NOW() - ?", LogRecordInterface::FIELD_DATE) => self::RECENT_EVENT
+ ];
+ try {
+ $rows = $connection->update($this->getTable(
+ self::TABLE_LOG),
+ [LogRecordInterface::FIELD_REFERENCE => $merchantReference],
+ $where);
+ } catch (\Exception $e) {
+ }
+ }
+}
diff --git a/Model/ResourceModel/OrderManagementStatus.php b/Model/ResourceModel/OrderManagementStatus.php
new file mode 100644
index 0000000..8e7b42d
--- /dev/null
+++ b/Model/ResourceModel/OrderManagementStatus.php
@@ -0,0 +1,43 @@
+_init(self::TABLE_OM_STATUS, OrderManagementStatusModel::FIELD_ID);
+ }
+
+ /**
+ * Update the timestamp on every save
+ *
+ * @param \Magento\Framework\DataObject $object
+ */
+ public function beforeSave(DataObject $object)
+ {
+ $object->setData('updated_at', new \Zend_Db_Expr('NOW()'));
+
+ parent::beforeSave($object);
+ }
+}
diff --git a/Model/ResourceModel/OrderManagementStatus/Collection.php b/Model/ResourceModel/OrderManagementStatus/Collection.php
new file mode 100644
index 0000000..d489bf4
--- /dev/null
+++ b/Model/ResourceModel/OrderManagementStatus/Collection.php
@@ -0,0 +1,28 @@
+_init(OrderManagementStatusModel::class, OrderManagementStatusResource::class);
+ }
+}
diff --git a/Model/Security/AjaxToken.php b/Model/Security/AjaxToken.php
new file mode 100644
index 0000000..caada1f
--- /dev/null
+++ b/Model/Security/AjaxToken.php
@@ -0,0 +1,52 @@
+quote = $quote;
+ return $this;
+ }
+
+ /**
+ * Get an expiration timestamp for the token
+ *
+ * @return int
+ */
+ public function getExpirationTimestamp()
+ {
+ return strtotime('+1 hour');
+ }
+
+ /**
+ * Add quote ID to the token
+ *
+ * @return mixed
+ */
+ public function getAdditionalData()
+ {
+ return $this->quote instanceof Quote ? (int)$this->quote->getId() : null;
+ }
+}
diff --git a/Model/Security/CallbackToken.php b/Model/Security/CallbackToken.php
new file mode 100644
index 0000000..509a4ab
--- /dev/null
+++ b/Model/Security/CallbackToken.php
@@ -0,0 +1,163 @@
+jwt = $jwt;
+ $this->qliroConfig = $qliroConfig;
+ $this->logManager = $logManager;
+ }
+
+ /**
+ * Get a new token that will expire in 2 hours
+ *
+ * @return string
+ */
+ public function getToken()
+ {
+ $payload = [
+ 'merchant' => $this->qliroConfig->getMerchantApiKey(),
+ 'expires' => date('Y-m-d H:i:s', $this->getExpirationTimestamp()),
+ 'additional_data' => $this->getAdditionalData(),
+ ];
+
+ return $this->jwt->encode($payload, $this->qliroConfig->getMerchantApiSecret());
+ }
+
+ /**
+ * Verify if the token is valid
+ *
+ * @param string $token
+ * @return bool
+ */
+ public function verifyToken($token)
+ {
+ try {
+ $payload = $this->jwt->decode($token, $this->qliroConfig->getMerchantApiSecret(), true);
+ } catch (\Exception $exception) {
+ return false;
+ }
+
+ $merchant = $payload['merchant'] ?? null;
+ $expiresAt = isset($payload['expires']) ? strtotime($payload['expires']) : 0;
+ $additionalData = $payload['additional_data'] ?? null;
+
+ $this->logManager->setMark('SECURITY TOKEN');
+ $this->logManager->addTag('security');
+
+ if ($merchant !== $this->qliroConfig->getMerchantApiKey()) {
+ $this->logManager->debug(
+ 'merchant ID mismatch',
+ [
+ 'extra' => [
+ 'request' => $merchant,
+ 'configured' => $this->qliroConfig->getMerchantApiKey()
+ ]
+ ]
+ );
+
+ $this->logManager->setMark(null);
+ $this->logManager->removeTag('security');
+
+ return false;
+ }
+
+ if ($additionalData != $this->getAdditionalData()) {
+ $this->logManager->debug(
+ 'additional data mismatch',
+ [
+ 'extra' => [
+ 'additional_data' => $additionalData,
+ ]
+ ]
+ );
+
+ $this->logManager->setMark(null);
+ $this->logManager->removeTag('security');
+
+ return false;
+ }
+
+ if ($expiresAt - time() < 0) {
+ $this->logManager->debug(
+ 'expired {expired} seconds ago',
+ [
+ 'expired' => time() - $expiresAt
+ ]
+ );
+
+ $this->logManager->setMark(null);
+ $this->logManager->removeTag('security');
+
+ return false;
+ }
+
+ $this->logManager->setMark(null);
+ $this->logManager->removeTag('security');
+
+ return true;
+ }
+
+ /**
+ * Get an expiration timestamp for the token
+ *
+ * @return int
+ */
+ public function getExpirationTimestamp()
+ {
+ /* Reason from QliroOne API documentation:
+ * If the callback is not received by Qliro One, seven retry attempts are scheduled
+ * at 30 seconds, 60 seconds, 2 minutes, 30 minutes, 1 hour, 24 hours and 3 days.
+ * We also need an additional day for the shopper taking their time before placing the order.
+ */
+ return strtotime('+4 day');
+ }
+
+ /**
+ * Get additional data used for modifying security token
+ *
+ * @return mixed
+ */
+ public function getAdditionalData()
+ {
+ return null;
+ }
+}
diff --git a/Model/Security/Jwt.php b/Model/Security/Jwt.php
new file mode 100644
index 0000000..4e75f14
--- /dev/null
+++ b/Model/Security/Jwt.php
@@ -0,0 +1,194 @@
+ 'sha256',
+ 'HS384' => 'sha384',
+ 'HS512' => 'sha512',
+ ];
+
+ /**
+ * @var \Magento\Framework\Serialize\Serializer\Json
+ */
+ private $json;
+
+ /**
+ * Inject dependencies
+ *
+ * @param \Magento\Framework\Serialize\Serializer\Json $json
+ */
+ public function __construct(
+ Json $json
+ ) {
+ $this->json = $json;
+ }
+
+ /**
+ * Decode a JWT string into an array
+ *
+ * @param string $jwt
+ * @param string|null $secretKey
+ * @param bool $verify Don't skip verification process
+ * @return array
+ * @throws \UnexpectedValueException
+ * @throws \DomainException
+ */
+ public function decode($jwt, $secretKey = null, $verify = true)
+ {
+ $tokenSegments = explode('.', $jwt);
+
+ if (count($tokenSegments) != 3) {
+ throw new \UnexpectedValueException('Wrong number of segments.');
+ }
+
+ list($encodedHead, $encodedBody, $encryption) = $tokenSegments;
+
+ if (null === ($header = $this->jsonDecode($this->safeDecode($encodedHead)))) {
+ throw new \UnexpectedValueException('Invalid segment encoding.');
+ }
+
+ if (null === $payload = $this->jsonDecode($this->safeDecode($encodedBody))) {
+ throw new \UnexpectedValueException('Invalid segment encoding.');
+ }
+
+ $sig = $this->safeDecode($encryption);
+
+ if ($verify) {
+ if (empty($header['alg'])) {
+ throw new \DomainException('Empty algorithm');
+ }
+
+ if ($sig != $this->sign("$encodedHead.$encodedBody", $secretKey, $header['alg'])) {
+ throw new \UnexpectedValueException('Signature verification failed.');
+ }
+ }
+
+ return $payload;
+ }
+
+ /**
+ * Convert and sign a payload into a JWT string
+ *
+ * @param array $payload
+ * @param string $secretKey
+ * @param string $algorithm
+ * @return string
+ */
+ public function encode($payload, $secretKey, $algorithm = 'HS256')
+ {
+ $header = ['typ' => 'JWT', 'alg' => $algorithm];
+ $segments = [];
+ $segments[] = $this->safeEncode($this->jsonEncode($header));
+ $segments[] = $this->safeEncode($this->jsonEncode($payload));
+ $signingInput = implode('.', $segments);
+ $signature = $this->sign($signingInput, $secretKey, $algorithm);
+ $segments[] = $this->safeEncode($signature);
+
+ return implode('.', $segments);
+ }
+
+ /**
+ * Sign a string with a given key and algorithm
+ *
+ * @param string $message
+ * @param string $secretKey
+ * @param string $algorithm
+ * @return string
+ * @throws \DomainException
+ */
+ private function sign($message, $secretKey, $algorithm = 'HS256')
+ {
+ if (!isset($this->supportedMethods[$algorithm])) {
+ throw new \DomainException('Algorithm is not supported.');
+ }
+
+ return \hash_hmac($this->supportedMethods[$algorithm], $message, $secretKey, true);
+ }
+
+ /**
+ * Decode a JSON string into a PHP object.
+ *
+ * @param string $input JSON string
+ * @return array
+ * @throws \DomainException Provided string was invalid JSON
+ */
+ private function jsonDecode($input)
+ {
+ try {
+ $data = $this->json->unserialize($input);
+ } catch (\InvalidArgumentException $exception) {
+ throw new \DomainException('Unknown JSON decode error.');
+ }
+
+ if ($data === null && $input !== 'null') {
+ throw new \DomainException('Null result with non-null input.');
+ }
+
+ return $data;
+ }
+
+ /**
+ * Encode an array into a JSON string
+ *
+ * @param array $input
+ * @return string
+ * @throws \DomainException
+ */
+ private function jsonEncode($input)
+ {
+ try {
+ $json = $this->json->serialize($input);
+ } catch (\InvalidArgumentException $exception) {
+ throw new \DomainException('Unknown JSON encode error.');
+ }
+
+ if ($json === 'null' && $input !== null) {
+ throw new \DomainException('Null result with non-null input');
+ }
+
+ return $json;
+ }
+
+ /**
+ * Decode a string with URL-safe Base64
+ *
+ * @param string $input
+ * @return string
+ */
+ private function safeDecode($input)
+ {
+ $remainder = strlen($input) % 4;
+ if ($remainder) {
+ $padlen = 4 - $remainder;
+ $input .= str_repeat('=', $padlen);
+ }
+
+ return \base64_decode(strtr($input, '-_', '+/'));
+ }
+
+ /**
+ * Encode a string with URL-safe Base64
+ *
+ * @param string $input
+ * @return string
+ */
+ private function safeEncode($input)
+ {
+ return str_replace('=', '', strtr(\base64_encode($input), '+/', '-_'));
+ }
+}
diff --git a/Model/ShippingMethod/BrandResolver.php b/Model/ShippingMethod/BrandResolver.php
new file mode 100644
index 0000000..5465c58
--- /dev/null
+++ b/Model/ShippingMethod/BrandResolver.php
@@ -0,0 +1,29 @@
+getCarrierTitle();
+ }
+}
diff --git a/Model/Success/Session.php b/Model/Success/Session.php
new file mode 100644
index 0000000..beca712
--- /dev/null
+++ b/Model/Success/Session.php
@@ -0,0 +1,89 @@
+checkoutSession = $checkoutSession;
+ }
+
+ /**
+ * @param string $snippet
+ * @param \Magento\Sales\Model\Order $order
+ */
+ public function save($snippet, $order)
+ {
+ $this->checkoutSession->setSuccessHtmlSnippet($snippet);
+ $this->checkoutSession->setSuccessIncrementId($order->getIncrementId());
+ $this->checkoutSession->setSuccessOrderId($order->getId());
+ $this->checkoutSession->setSuccessHasDisplayed(false);
+ }
+
+ /**
+ * Clears saves success
+ */
+ public function clear()
+ {
+ $this->checkoutSession->unsSuccessHtmlSnippet();
+ $this->checkoutSession->unsSuccessIncrementId();
+ $this->checkoutSession->unsSuccessOrderId();
+ $this->checkoutSession->unsSuccessHasDisplayed();
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSuccessHtmlSnippet()
+ {
+ return $this->checkoutSession->getSuccessHtmlSnippet();
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSuccessIncrementId()
+ {
+ return $this->checkoutSession->getSuccessIncrementId();
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getSuccessOrderId()
+ {
+ return $this->checkoutSession->getSuccessOrderId();
+ }
+
+ /**
+ * @return bool
+ */
+ public function hasSuccessDisplayed()
+ {
+ return (bool)$this->checkoutSession->getSuccessHasDisplayed();
+ }
+
+ /**
+ * Mark success as being displayed, thus not triggering GTM etc if success page is reloaded
+ */
+ public function setSuccessDisplayed()
+ {
+ $this->checkoutSession->setSuccessHasDisplayed(true);
+ }
+}
diff --git a/Observer/AddFeeToOrder.php b/Observer/AddFeeToOrder.php
new file mode 100644
index 0000000..61273b3
--- /dev/null
+++ b/Observer/AddFeeToOrder.php
@@ -0,0 +1,50 @@
+getQuote();
+
+ /** @var \Magento\Quote\Model\Quote\Address $address */
+ if ($quote->isVirtual()) {
+ $address = $quote->getBillingAddress();
+ } else {
+ $address = $quote->getShippingAddress();
+ }
+
+ $feeAmount = $address->getQlirooneFee();
+ $baseQlirooneFee = $address->getBaseQlirooneFee();
+ if (!$feeAmount || !$baseQlirooneFee) {
+ return $this;
+ }
+ $feeAmountTax = $address->getQlirooneFeeTax();
+ $baseQlirooneFeeTax = $address->getBaseQlirooneFeeTax();
+
+ //Set fee data to order
+ $order = $observer->getOrder();
+ $order->setQlirooneFee($feeAmount);
+ $order->setQlirooneFeeTax($feeAmountTax);
+ $order->setBaseQlirooneFee($baseQlirooneFee);
+ $order->setBaseQlirooneFeeTax($baseQlirooneFeeTax);
+
+ return $this;
+ }
+}
\ No newline at end of file
diff --git a/Observer/AddInvoiceToCapture.php b/Observer/AddInvoiceToCapture.php
new file mode 100644
index 0000000..8eff386
--- /dev/null
+++ b/Observer/AddInvoiceToCapture.php
@@ -0,0 +1,30 @@
+getPayment();
+
+ if ($payment->getMethod() == QliroOne::PAYMENT_METHOD_CHECKOUT_CODE) {
+ $payment->setInvoice($observer->getInvoice());
+ }
+ }
+}
diff --git a/Observer/CaptureOnShipment.php b/Observer/CaptureOnShipment.php
new file mode 100644
index 0000000..62dd2c5
--- /dev/null
+++ b/Observer/CaptureOnShipment.php
@@ -0,0 +1,50 @@
+qliroManagement = $qliroManagement;
+ }
+
+ /**
+ * @param Observer $observer
+ */
+ public function execute(\Magento\Framework\Event\Observer $observer)
+ {
+ /** @var \Magento\Sales\Model\Order\Shipment $shipment */
+ $shipment = $observer->getEvent()->getShipment();
+
+ $order = $shipment->getOrder();
+ $payment = $order->getPayment();
+
+ if ($payment->getMethod() == QliroOne::PAYMENT_METHOD_CHECKOUT_CODE) {
+ $this->qliroManagement->captureByShipment($shipment);
+ }
+ }
+}
diff --git a/Observer/CustomerLogin.php b/Observer/CustomerLogin.php
new file mode 100644
index 0000000..62b1b5d
--- /dev/null
+++ b/Observer/CustomerLogin.php
@@ -0,0 +1,64 @@
+linkRepository = $linkRepository;
+ $this->checkoutSession = $checkoutSession;
+ }
+
+ /**
+ * @param Observer $observer
+ */
+ public function execute(\Magento\Framework\Event\Observer $observer)
+ {
+ try {
+ $link = $this->linkRepository->getByQuoteId($this->getQuote()->getId());
+ $link->setIsActive(false);
+ $link->setMessage('Unlinking quote due to customer login');
+ $this->linkRepository->save($link);
+ } catch (\Exception $exception) {
+ }
+ }
+
+ /**
+ * Get current quote from checkout session
+ *
+ * @return \Magento\Quote\Model\Quote
+ */
+ private function getQuote()
+ {
+ return $this->checkoutSession->getQuote();
+ }
+}
diff --git a/Observer/QliroCheckoutRedirect.php b/Observer/QliroCheckoutRedirect.php
new file mode 100644
index 0000000..799757c
--- /dev/null
+++ b/Observer/QliroCheckoutRedirect.php
@@ -0,0 +1,103 @@
+url = $urlModel;
+ $this->manager = $manager;
+ $this->session = $session;
+ $this->qliroConfig = $qliroConfig;
+ $this->successSession = $successSession;
+ }
+
+ /**
+ * Override the redirect to checkout but make it possible to control this override in custom extensions
+ *
+ * @param \Magento\Framework\Event\Observer $observer
+ * @return void
+ */
+ public function execute(Observer $observer)
+ {
+ $this->successSession->clear();
+
+ $state = new DataObject();
+
+ $state->setData([
+ 'redirect_url' => $this->url->getRouteUrl('checkout/qliro'),
+ ]);
+
+ $this->manager->dispatch(
+ 'qliroone_override_load_checkout',
+ [
+ 'state' => $state,
+ 'checkout_observer' => $observer,
+ ]
+ );
+
+ $mustEnable = $state->getMustEnable();
+ $mustDisable = $state->getMustDisable();
+ $qliroOverride = $this->session->getQliroOverride();
+
+
+ if ($mustEnable || (!$mustDisable&& !$qliroOverride && $this->qliroConfig->isActive())) {
+ $observer->getControllerAction()
+ ->getResponse()
+ ->setRedirect($state->getRedirectUrl())
+ ->sendResponse();
+ }
+ }
+}
diff --git a/Plugin/Block/Checkout/LayoutProcessorPlugin.php b/Plugin/Block/Checkout/LayoutProcessorPlugin.php
new file mode 100644
index 0000000..8e59169
--- /dev/null
+++ b/Plugin/Block/Checkout/LayoutProcessorPlugin.php
@@ -0,0 +1,69 @@
+quote = $session->getQuote();
+ $this->qliroManagement = $qliroManagement;
+ }
+
+ /**
+ * Alter the checkout configuration array to add binds for QliroOne OnePage checkout
+ *
+ * @param \Magento\Checkout\Block\Checkout\LayoutProcessor $subject
+ * @param array $result
+ * @return array
+ */
+ public function afterProcess(LayoutProcessor $subject, $result)
+ {
+ if (isset($result['components']['checkout']['children']['steps']['children']['qliroone-step'])) {
+ $htmlSnippet = $this->generateHtmlSnippet();
+ $result['components']['checkout']['children']['steps']['children']['qliroone-step']['html_snippet'] =
+ $htmlSnippet;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns iframe snippet of checkout form
+ *
+ * @return string
+ */
+ private function generateHtmlSnippet()
+ {
+ return (string)$this->qliroManagement->setQuote($this->quote)->getHtmlSnippet();
+ }
+}
diff --git a/Plugin/Callbacks/CsrfValidatorSkip.php b/Plugin/Callbacks/CsrfValidatorSkip.php
new file mode 100644
index 0000000..ce3a957
--- /dev/null
+++ b/Plugin/Callbacks/CsrfValidatorSkip.php
@@ -0,0 +1,30 @@
+getModuleName() == 'checkout' &&
+ $request->getControllerModule() == 'Qliro_QliroOne' &&
+ $request->getControllerName() == 'qliro_callback') {
+ return; // Callbacks must ignore formKey
+ }
+ $proceed($request, $action);
+ }
+}
diff --git a/Plugin/ZeroTotalPlugin.php b/Plugin/ZeroTotalPlugin.php
new file mode 100644
index 0000000..4b5102e
--- /dev/null
+++ b/Plugin/ZeroTotalPlugin.php
@@ -0,0 +1,30 @@
+getCode() == $quote->getPayment()->getMethod() &&
+ $paymentMethod->getCode() == \Qliro\QliroOne\Model\Method\QliroOne::PAYMENT_METHOD_CHECKOUT_CODE) {
+ $result = true;
+ }
+
+ return $result;
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..433669f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,195 @@
+# Qliro One Checkout for Magento 2
+
+(verified in Magento 2.2.3 and 2.3.4)
+
+The module is a fully functional implementation of a custom checkout that uses Qliro One functionality through its API.
+
+Out of the box, only purchase of virtual, simple and configurable Magento products are supported, but there is a
+built-in mechanism for extending support for custom product types, custom shipping methods, custom discounts, provided
+within the code of the module.
+
+## System configuration options
+
+As Qliro One Checkout is designed as a custom payment method, all configuration options are located in admin panel,
+under **STORES > Settings > Configuration > SALES > Payment Methods > Qliro One Checkout**.
+
+- **Enabled** — must be set to "Yes" for specific website, to turn on Qliro One Checkout functionality.
+ Note that Qliro One checkout replaces the standard Magento One Page Checkout for that website, they cannot be
+ used together at the same time.
+- **Title** — a store-specific name of Qliro One checkout, visible as a title on the checkout page, as well as used
+ in order management to represent Qliro One payment method.
+- **Debug Mode** — a website-specific flag that indicates debugging mode. In debugging mode, the logging is very
+ detailed, but debug mode must only be used for troubleshooting or during development, otherwise website will be
+ easily overflown with extensive logged information.
+- **Eager Checkout Refreshes** - If enabled, the lock and unlock of the Qliro One checkout is skipped. It also skips
+ the refresh of checkout when an update is made in the Iframe. Enabling this is not encouraged!
+
+### Merchant Configuration Settings
+
+All settings here are website-specific.
+
+- **API type** — specifies a type of API. Two are supported, Sandbox and Production.
+- **Merchant API Key** — specifies merchant'specific API Key.
+- **Merchant API Secret** — specifies merchant'specific API secret. this option is stored encrypted.
+- **Preset Shipping Address** — if set to "Yes", Qliro One checkout will use a fictional shipping address based on
+ the current store postal code to create Qliro One order. Otherwise, the order will be created without any
+ shipping address.
+
+### General Module Settings
+
+All settings here are website-specific.
+
+- **Geo IP** — a flag that enables country detection using current GeoIP functionality on the server.
+ By default only built-in PHP GeoIP extension is supported, but there is a way to extend support of custom GeoIP system.
+- **Logging Level** — a level of logging records that are being stored in the log, may be used to reduce versatility
+ of the logging as required.
+- **New Order Status** — a status that is used for marking newly placed Magento orders.
+- **Payment from Applicable Countries** — Magento standard countries setting, can limit which countries that can pay
+- **Trigger capture when shipment is created** — makes Magento trigger capturing money on shipment creation.
+- **Triger capture when invoice is created** — makes Magento trigger capturing money on invoice creation.
+- **Allow Newsletter Signup** — if set to "Yes", makes Qliro One checkout display a corresponding checkbox in the
+ checkout IFRAME.
+- **Require Identity Verification** — if set to "Yes", makes Qliro One checkout perform identity verification. Even if
+ this flag is set to No, it will set this flag if a Virtual product is in the cart. Virtual products can be anything
+ from gift cards to vouchers and various downloadable content.
+- **Minimum Customer Age** - Write the minimum age a person shopping on the site must be. If you enter anything in here
+ it becomes mandatory for the customer to identify his age.
+
+### CSS Styling Input Fields
+
+All settings here are store-specific.
+
+- **Background Color** — specifies a CSS HEX value for Qliro One checkout IFRAME background.
+- **Primary Color** — specifies a CSS HEX value for Qliro One checkout IFRAME primary text color.
+- **Call To Action Color** — specifies a CSS HEX value for Qliro One checkout IFRAME button color.
+- **Call To Action Hover Color** — specifies a CSS HEX value for Qliro One checkout IFRAME button hover color.
+- **Corner Radius** — specifies a corner radius for all frames inside the IFRAME.
+- **Button Corner Radius** — specifies a button corner radius inside the IFRAME.
+
+### Merchant Specific Information
+
+All settings here are website-specific.
+
+- **Fee Merchant Reference** — a merchant reference assigned to the Qliro One fee, if the fee is applicable.
+- **Terms URL** — should be a valid URL to the website page containing Terms and Conditions. If not specified, will be
+ defaulted to the website's home page.
+- **Integrity Policy URL** — if specified, must be a valid URL to Integrity Policy URL.
+
+### Notification Callbacks
+
+- **XDebug Session Flag Name for callback URLs** — store-specific value for the XDEBUG session flag used in callbacks
+ for debugging purposes only.
+- **Redirect Callbacks** — if set to "Yes", the next option appears, allowing substitute the callback base URL in order
+ to debug Qliro One remote callback requests locally, through tunneling.
+- **URI Prefix for Callbacks** — a base URI for callbacks that allows debugging Qliro One remote callback requests
+ locally, through tunneling.
+
+### Nshift Integration
+
+- **Enabled** — enables the integration to Nshift (formerly known as unifaun). Before you can use this, you will need
+ to have it enabled in your Qliro One account
+- **Nshift Checkout ID** — the checkout id provided to you
+- **Parameters** — a way to configure additional parameters. Pushing add adds a new line with three columns. Tag is the
+ tag name added, the function is one of the three user defined values or a plain user defined one. Last column is the
+ value sent for that tag. For more information, see the comment in the setting.
+- If Nshift is enabled and only virtual products are added to cart, the Nshift integration is not sent to Qliro, which
+ means it will not requrire the customer to choose a freight option.
+
+## Events
+
+Module-specific event dispatch points are provided in the places which cannot be customized using Magento plugins.
+
+### `qliroone_shipping_method_build_after`
+
+**Arguments:**
+
+- `quote` — an instance of quote (`\Magento\Quote\Model\Quote`)
+- `rate` — an instance of a shipping rate (`\Magento\Quote\Model\Quote\Address\Rate`)
+- `container` — shipping method container (`\Qliro\QliroOne\Api\Data\QliroOrderShippingMethodInterface`)
+
+**Notes:** Happens after the shipping method container is already built, can be updated before sent to the Qliro order
+create or update requests. If you want to have this particular shipping method skipped and not sent to the request,
+set the merchant reference of the container to `null`.
+
+### `qliroone_shipping_methods_response_build_after`
+
+**Arguments:**
+
+- `quote` — an instance of quote (`\Magento\Quote\Model\Quote`)
+- `container` — shipping methods response container (`\Qliro\QliroOne\Api\Data\UpdateShippingMethodsResponseInterface`)
+
+**Notes:** Happens after all shipping methods are built and packed into a shipping methods response container.
+
+
+### `qliroone_order_item_build_after`
+
+**Arguments:**
+
+- `quote` — an instance of quote (`\Magento\Quote\Model\Quote`)
+- `container` — QliroOne order item container (`\Qliro\QliroOne\Api\Data\QliroOrderItemInterface`)
+
+**Notes:** Happens after a QliroOne order item container is formed, can be updated before it is sent to the create or
+update request. If you want to have this particular order item skipped and not sent to the request, set the merchant
+reference of the container to `null`.
+
+
+### `qliroone_order_create_request_build_after`
+
+**Arguments:**
+
+- `quote` — an instance of quote (`\Magento\Quote\Model\Quote`)
+- `container` — QliroOne order item container (`\Qliro\QliroOne\Api\Data\QliroOrderCreateRequestInterface`)
+
+**Notes:** Happens after a QliroOne order create request container is formed, can be updated before it is sent to Qliro.
+
+
+### `qliroone_shipping_method_update_before`
+
+**Arguments:**
+
+- `quote` — an instance of quote (`\Magento\Quote\Model\Quote`)
+- `container` — simple `\Magento\Framework\DataObject` container that contains three fields,
+ - `shipping_method` — a code of the shipping method, as it is coming from QliroOne update
+ - `secondary_option` — a secondary option, as it is coming from QliroOne update
+ - `shipping_price` — an updated shipping price, as it is coming from QliroOne update
+ - `can_save_quote` — a flag that is initially set to `true` if the shipping method is different from one in quote.
+ If only secondary option has to be applied, or something else requires quote recalculation and saving, it should be
+ set to `true` by one of the event listeners.
+
+**Notes:** Happens before QliroOne checkout module is ready to set the updated shipping method to Magento quote.
+
+
+### `qliroone_shipping_price_update_before`
+
+**Arguments:**
+
+- `quote` — an instance of quote (`\Magento\Quote\Model\Quote`)
+- `container` — simple `\Magento\Framework\DataObject` container that contains three fields,
+ - `shipping_price` — an updated shipping price, as it is coming from QliroOne update
+ - `can_save_quote` — a flag that is initially set to `false`. It should be set to `true` by one of the event
+ listeners if the quote requires recalculation and saving.
+
+**Notes:** Happens before QliroOne checkout module is ready to get the shipping price updated in Magento quote.
+
+
+
+### Plugins
+
+Obviously, no special plugin entry points are provided for the original module, but developers can use standard Magento 2
+plugin mechanism to plug into any public method in any class instantiated using DI, as specified in
+https://devdocs.magento.com/guides/v2.2/extension-dev-guide/plugins.html.
+
+
+### Database tables and Logs
+
+**qliroone_link** - The table that stores the Qliro One reference with Magento Quote. It contains more details as well,
+ such as status and last known comment regarding that order.
+
+**qliroone_log** - a detailed log of everything that takes place in the code. All logs are linked with the Qliro One
+ reference id.
+
+**qliroone_om_status** - contains the history of all notifications received from Qliro One, plus some additional status
+ updates
+
+**qliroone_order_lock** - during certain events and functions, we use this table as a semaphore lock to allow or
+ disallow certain concurrent functions.
diff --git a/Setup/InstallSchema.php b/Setup/InstallSchema.php
new file mode 100644
index 0000000..4312bcb
--- /dev/null
+++ b/Setup/InstallSchema.php
@@ -0,0 +1,353 @@
+startSetup();
+
+ if (!$installer->tableExists(LogRecord::TABLE_LOG)) {
+ $table = $installer->getConnection()
+ ->newTable($installer->getTable(LogRecord::TABLE_LOG))
+ ->addColumn(
+ LogRecordModel::FIELD_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
+ 'Log line id'
+ )
+ ->addColumn(
+ LogRecordModel::FIELD_DATE,
+ Table::TYPE_TIMESTAMP,
+ null,
+ ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
+ 'Date'
+ )
+ ->addColumn(LogRecordModel::FIELD_LEVEL, Table::TYPE_TEXT, 32, ['nullable' => false], 'Log level')
+ ->addColumn(
+ LogRecordModel::FIELD_PROCESS_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['unsigned' => true, 'nullable' => false],
+ 'Process ID'
+ )
+ ->addColumn(
+ LogRecordModel::FIELD_REFERENCE,
+ Table::TYPE_TEXT,
+ 25,
+ [],
+ 'Merchant ID')
+ ->addColumn(
+ LogRecordModel::FIELD_TAGS,
+ Table::TYPE_TEXT,
+ 256,
+ ['unsigned' => true],
+ 'Comma separated list of tags')
+ ->addColumn(LogRecordModel::FIELD_MESSAGE, Table::TYPE_TEXT, null, [], 'Message')
+ ->addColumn(LogRecordModel::FIELD_EXTRA, Table::TYPE_TEXT, null, [], 'Extra data')
+ ->addIndex(
+ $installer->getIdxName(LogRecord::TABLE_LOG, [LogRecordModel::FIELD_DATE]),
+ [LogRecordModel::FIELD_DATE]
+ )
+ ->addIndex(
+ $installer->getIdxName(LogRecord::TABLE_LOG, [LogRecordModel::FIELD_LEVEL]),
+ [LogRecordModel::FIELD_LEVEL]
+ )
+ ->addIndex(
+ $installer->getIdxName(LogRecord::TABLE_LOG, [LogRecordModel::FIELD_REFERENCE]),
+ [LogRecordModel::FIELD_REFERENCE]
+ )
+ ->addIndex(
+ $installer->getIdxName(LogRecord::TABLE_LOG, [LogRecordModel::FIELD_PROCESS_ID]),
+ [LogRecordModel::FIELD_PROCESS_ID]
+ )
+ ->setComment('QliroOne log');
+ $installer->getConnection()->createTable($table);
+ }
+
+ if (!$installer->tableExists(Link::TABLE_LINK)) {
+ $table = $installer->getConnection()
+ ->newTable($installer->getTable(Link::TABLE_LINK))
+ ->addColumn(
+ LinkModel::FIELD_ID,
+ Table::TYPE_INTEGER,
+ 10,
+ ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
+ 'Link ID'
+ )->addColumn(
+ LinkModel::FIELD_IS_ACTIVE,
+ Table::TYPE_SMALLINT,
+ 1,
+ ['unsigned' => true, 'default' => 1, 'nullable' => false],
+ 'Flag indicating if link is still in used'
+ )
+ ->addColumn(
+ LinkModel::FIELD_REFERENCE,
+ Table::TYPE_TEXT,
+ 25,
+ ['nullable' => false],
+ 'Unique QliroOne order merchant reference'
+ )
+ ->addColumn(
+ LinkModel::FIELD_QUOTE_ID,
+ Table::TYPE_INTEGER,
+ 10,
+ ['unsigned' => true],
+ 'Quote ID, null when order has been created'
+ )
+ ->addColumn(
+ LinkModel::FIELD_QLIRO_ORDER_ID,
+ Table::TYPE_INTEGER,
+ 12,
+ ['unsigned' => true],
+ 'QliroOne Order ID'
+ )
+ ->addColumn(
+ LinkModel::FIELD_QLIRO_ORDER_STATUS,
+ Table::TYPE_TEXT,
+ 32,
+ ['default' => CheckoutStatusInterface::STATUS_IN_PROCESS],
+ 'QliroOne Order Status'
+ )
+ ->addColumn(
+ LinkModel::FIELD_ORDER_ID,
+ Table::TYPE_INTEGER,
+ 10,
+ ['unsigned' => true],
+ 'Order ID, null before order has been created'
+ )
+ ->addColumn(
+ LinkModel::FIELD_QUOTE_SNAPSHOT,
+ Table::TYPE_TEXT,
+ null,
+ ['nullable' => false],
+ 'Quote snapshot signature'
+ )
+ ->addColumn(
+ LinkModel::FIELD_REMOTE_IP,
+ Table::TYPE_TEXT,
+ null,
+ [],
+ 'Client IP when link was created'
+ )
+ ->addColumn(
+ LinkModel::FIELD_CREATED_AT,
+ Table::TYPE_TIMESTAMP,
+ null,
+ ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
+ 'Link creation timestamp'
+ )
+ ->addColumn(
+ LinkModel::FIELD_UPDATED_AT,
+ Table::TYPE_TIMESTAMP,
+ null,
+ ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
+ 'Link last update timestamp'
+ )
+ ->addColumn(
+ LinkModel::FIELD_MESSAGE,
+ Table::TYPE_TEXT,
+ null,
+ ['nullable' => true],
+ 'Latest message or error message'
+ )
+ ->addIndex(
+ $installer->getIdxName(Link::TABLE_LINK, [LinkModel::FIELD_IS_ACTIVE]),
+ [LinkModel::FIELD_IS_ACTIVE]
+ )
+ ->addIndex(
+ $installer->getIdxName(
+ Link::TABLE_LINK,
+ [LinkModel::FIELD_REFERENCE],
+ AdapterInterface::INDEX_TYPE_UNIQUE
+ ),
+ [LinkModel::FIELD_REFERENCE],
+ ['type' => AdapterInterface::INDEX_TYPE_UNIQUE]
+ )
+ ->addIndex(
+ $installer->getIdxName(Link::TABLE_LINK, [LinkModel::FIELD_QUOTE_ID]),
+ [LinkModel::FIELD_QUOTE_ID]
+ )
+ ->addIndex(
+ $installer->getIdxName(Link::TABLE_LINK, [LinkModel::FIELD_QLIRO_ORDER_ID]),
+ [LinkModel::FIELD_QLIRO_ORDER_ID]
+ )
+ ->addIndex(
+ $installer->getIdxName(Link::TABLE_LINK, [LinkModel::FIELD_ORDER_ID]),
+ [LinkModel::FIELD_ORDER_ID]
+ )
+ ->addIndex(
+ $installer->getIdxName(Link::TABLE_LINK, [LinkModel::FIELD_CREATED_AT]),
+ [LinkModel::FIELD_CREATED_AT]
+ )
+ ->addIndex(
+ $installer->getIdxName(Link::TABLE_LINK, [LinkModel::FIELD_UPDATED_AT]),
+ [LinkModel::FIELD_UPDATED_AT]
+ )
+ ->setComment('Link QliroOne orders with Magento');
+ $installer->getConnection()->createTable($table);
+ }
+
+ if (!$installer->tableExists(Lock::TABLE_LOCK)) {
+ $table = $installer->getConnection()
+ ->newTable($installer->getTable(Lock::TABLE_LOCK))
+ ->addColumn(
+ Lock::FIELD_ID,
+ Table::TYPE_INTEGER,
+ 12,
+ ['unsigned' => true, 'nullable' => false],
+ 'QliroOne Order ID'
+ )
+ ->addColumn(
+ Lock::FIELD_CREATED_AT,
+ Table::TYPE_TIMESTAMP,
+ null,
+ ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
+ 'Timestamp when lock was created'
+ )
+ ->addColumn(
+ Lock::FIELD_PROCESS_ID,
+ Table::TYPE_INTEGER,
+ 8,
+ ['unsigned' => true, 'nullable' => false],
+ 'PID'
+ )
+ ->addIndex(
+ $installer->getIdxName(Lock::TABLE_LOCK, [Lock::FIELD_ID], AdapterInterface::INDEX_TYPE_UNIQUE),
+ [Lock::FIELD_ID],
+ ['type' => AdapterInterface::INDEX_TYPE_UNIQUE]
+ )
+ ->addIndex(
+ $installer->getIdxName(Lock::TABLE_LOCK, [Lock::FIELD_CREATED_AT]),
+ [Lock::FIELD_CREATED_AT]
+ )
+ ->setComment('Lock for creating Magento order');
+ $installer->getConnection()->createTable($table);
+ }
+
+ if (!$installer->tableExists(OrderManagementStatus::TABLE_OM_STATUS)) {
+ $table = $installer->getConnection()
+ ->newTable($installer->getTable(OrderManagementStatus::TABLE_OM_STATUS))
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
+ 'Id'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_DATE,
+ Table::TYPE_TIMESTAMP,
+ null,
+ ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
+ 'Date'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_TRANSACTION_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['unsigned' => true, 'nullable' => false],
+ 'Payment Transaction ID'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_RECORD_TYPE,
+ Table::TYPE_TEXT,
+ 25,
+ [],
+ 'Record Type'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_RECORD_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['unsigned' => true, 'nullable' => false],
+ 'Record ID'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_TRANSACTION_STATUS,
+ Table::TYPE_TEXT,
+ 255,
+ [],
+ 'Transaction Status'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_NOTIFICATION_STATUS,
+ Table::TYPE_TEXT,
+ 10,
+ [],
+ 'Notification Status'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_MESSAGE,
+ Table::TYPE_TEXT,
+ null,
+ [],
+ 'Possible Message'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_QLIRO_ORDER_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['unsigned' => true, 'nullable' => false],
+ 'Qliro Order Id'
+ )->addIndex(
+ $installer->getIdxName(
+ OrderManagementStatus::TABLE_OM_STATUS,
+ [OrderManagementStatusModel::FIELD_DATE]
+ ),
+ [OrderManagementStatusModel::FIELD_DATE]
+ )->addIndex(
+ $installer->getIdxName(
+ OrderManagementStatus::TABLE_OM_STATUS,
+ [OrderManagementStatusModel::FIELD_TRANSACTION_ID]
+ ),
+ [OrderManagementStatusModel::FIELD_TRANSACTION_ID]
+ )->addIndex(
+ $installer->getIdxName(
+ OrderManagementStatus::TABLE_OM_STATUS,
+ [OrderManagementStatusModel::FIELD_TRANSACTION_STATUS]
+ ),
+ [OrderManagementStatusModel::FIELD_TRANSACTION_STATUS]
+ )->addIndex(
+ $installer->getIdxName(
+ OrderManagementStatus::TABLE_OM_STATUS,
+ [OrderManagementStatusModel::FIELD_RECORD_ID]
+ ),
+ [OrderManagementStatusModel::FIELD_RECORD_ID]
+ )
+ ->setComment('QliroOne OM Notification Statuses');
+ $installer->getConnection()->createTable($table);
+ }
+
+ $installer->endSetup();
+ }
+}
diff --git a/Setup/UpgradeSchema.php b/Setup/UpgradeSchema.php
new file mode 100644
index 0000000..6412b0e
--- /dev/null
+++ b/Setup/UpgradeSchema.php
@@ -0,0 +1,346 @@
+startSetup();
+
+ $connection = $setup->getConnection();
+ if (version_compare($context->getVersion(), '0.1.2') < 0) {
+ $connection->addColumn($setup->getTable(LogRecord::TABLE_LOG), LogRecordModel::FIELD_REFERENCE, [
+ 'type' => Table::TYPE_TEXT,
+ 25,
+ 'comment' => 'Merchant ID',
+ ]);
+ $connection->addColumn($setup->getTable(LogRecord::TABLE_LOG), LogRecordModel::FIELD_TAGS, [
+ 'type' => Table::TYPE_TEXT,
+ 256,
+ 'comment' => 'Comma separated list of tags',
+ ]);
+ $connection->dropColumn($setup->getTable(LogRecord::TABLE_LOG), 'tag');
+ }
+
+ if (version_compare($context->getVersion(), '0.1.3') < 0) {
+ //Quote address tables
+ $connection->addColumn($setup->getTable(self::TABLE_QUOTE_ADDRESS), 'qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_QUOTE_ADDRESS), 'base_qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Amount',
+ ]);
+
+ //Order tables
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'base_qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'qliroone_fee_refunded', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Amount Refunded',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'base_qliroone_fee_refunded', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Amount Refunded',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'qliroone_fee_invoiced', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Amount Invoiced',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'base_qliroone_fee_invoiced', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Amount Invoiced',
+ ]);
+
+ //Invoice tables
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_INVOICE), 'qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_INVOICE), 'base_qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Amount',
+ ]);
+
+ //Credit memo tables
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_CREDITMEMO), 'qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_CREDITMEMO), 'base_qliroone_fee', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Amount',
+ ]);
+ //Quote address tables
+ $connection->addColumn($setup->getTable(self::TABLE_QUOTE_ADDRESS), 'qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Tax Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_QUOTE_ADDRESS), 'base_qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Tax Amount',
+ ]);
+
+ //Order tables
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Tax Amount',
+
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_ORDER), 'base_qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Tax Amount',
+ ]);
+
+ //Invoice tables
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_INVOICE), 'qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Tax Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_INVOICE), 'base_qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Tax Amount',
+ ]);
+
+ //Credit memo tables
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_CREDITMEMO), 'qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Fee Tax Amount',
+ ]);
+
+ $connection->addColumn($setup->getTable(self::TABLE_SALES_CREDITMEMO), 'base_qliroone_fee_tax', [
+ 'type' => Table::TYPE_DECIMAL,
+ '12,4',
+ 'default' => 0.00,
+ 'nullable' => true,
+ 'comment' => 'Base Fee Tax Amount',
+ ]);
+ }
+
+ if (version_compare($context->getVersion(), '0.1.4') < 0) {
+ $connection->addColumn($setup->getTable(Link::TABLE_LINK), LinkModel::FIELD_QLIRO_ORDER_STATUS, [
+ 'type' => Table::TYPE_TEXT,
+ 32,
+ 'comment' => 'Qliro Order Status',
+ ]);
+ }
+
+ if (version_compare($context->getVersion(), '0.1.5') < 0) {
+ $connection->addColumn($setup->getTable(Link::TABLE_LINK), LinkModel::FIELD_REMOTE_IP, [
+ 'type' => Table::TYPE_TEXT,
+ 32,
+ 'comment' => 'Client IP when link was created',
+ ]);
+ }
+
+ if (version_compare($context->getVersion(), '0.1.6') < 0) {
+ if (!$setup->tableExists(OrderManagementStatus::TABLE_OM_STATUS)) {
+ $table = $setup->getConnection()
+ ->newTable($setup->getTable(OrderManagementStatus::TABLE_OM_STATUS))
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['identity' => true, 'unsigned' => true, 'nullable' => false, 'primary' => true],
+ 'Id'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_DATE,
+ Table::TYPE_TIMESTAMP,
+ null,
+ ['nullable' => false, 'default' => Table::TIMESTAMP_INIT],
+ 'Date'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_TRANSACTION_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['unsigned' => true, 'nullable' => false],
+ 'Payment Transaction ID'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_RECORD_TYPE,
+ Table::TYPE_TEXT,
+ 25,
+ [],
+ 'Record Type'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_RECORD_ID,
+ Table::TYPE_INTEGER,
+ null,
+ ['unsigned' => true, 'nullable' => false],
+ 'Record ID'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_TRANSACTION_STATUS,
+ Table::TYPE_TEXT,
+ 255,
+ [],
+ 'Transaction Status'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_NOTIFICATION_STATUS,
+ Table::TYPE_TEXT,
+ 10,
+ [],
+ 'Notification Status'
+ )
+ ->addColumn(
+ OrderManagementStatusModel::FIELD_MESSAGE,
+ Table::TYPE_TEXT,
+ null,
+ [],
+ 'Possible Message'
+ )
+ ->addIndex(
+ $setup->getIdxName(OrderManagementStatus::TABLE_OM_STATUS, [OrderManagementStatusModel::FIELD_DATE]),
+ [OrderManagementStatusModel::FIELD_DATE]
+ )
+ ->addIndex(
+ $setup->getIdxName(OrderManagementStatus::TABLE_OM_STATUS, [OrderManagementStatusModel::FIELD_TRANSACTION_ID]),
+ [OrderManagementStatusModel::FIELD_TRANSACTION_ID]
+ )
+ ->addIndex(
+ $setup->getIdxName(OrderManagementStatus::TABLE_OM_STATUS, [OrderManagementStatusModel::FIELD_TRANSACTION_STATUS]),
+ [OrderManagementStatusModel::FIELD_TRANSACTION_STATUS]
+ )
+ ->addIndex(
+ $setup->getIdxName(OrderManagementStatus::TABLE_OM_STATUS, [OrderManagementStatusModel::FIELD_RECORD_ID]),
+ [OrderManagementStatusModel::FIELD_RECORD_ID]
+ )
+ ->setComment('QliroOne OM Notification Statuses');
+ $setup->getConnection()->createTable($table);
+ }
+ }
+
+ if (version_compare($context->getVersion(), '0.1.7') < 0) {
+ $connection->addColumn($setup->getTable(OrderManagementStatus::TABLE_OM_STATUS), OrderManagementStatusModel::FIELD_QLIRO_ORDER_ID, [
+ 'type' => Table::TYPE_INTEGER,
+ 'length' => 10,
+ 'unsigned' => true,
+ 'nullable' => false,
+ 'comment' => 'Qliro Order Id',
+ ]);
+ }
+
+ if (version_compare($context->getVersion(), '0.1.8') < 0) {
+ $connection->addColumn($setup->getTable(Link::TABLE_LINK), LinkModel::FIELD_PLACED_AT, [
+ 'type' => Table::TYPE_TIMESTAMP,
+ 'nullable' => true,
+ 'default' => null,
+ 'comment' => 'When pending is opened',
+ ]);
+ }
+
+ if (version_compare($context->getVersion(), '1.2.3') < 0) {
+ $connection->addColumn($setup->getTable(Link::TABLE_LINK), LinkModel::FIELD_UNIFAUN_SHIPPING_AMOUNT, [
+ 'type' => Table::TYPE_FLOAT,
+ 'nullable' => true,
+ 'default' => null,
+ 'comment' => 'If unifaun is used it stores the freight amount here for the shipping method to read',
+ ]);
+ }
+
+ $setup->endSetup();
+ }
+}
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..cafdd30
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "qliro/module-qliroone",
+ "type": "magento2-module",
+ "description": "QliroOne checkout",
+ "license": "proprietary",
+ "authors": [
+ {
+ "name": "Andreas Wickberg",
+ "email": "andreas.wickberg@vaimo.com"
+ },
+ {
+ "name": "Kjell Holmqvist",
+ "email": "kjell.holmqvist@vaimo.com"
+ }
+ ],
+ "require": {
+ "ext-json": "*",
+ "magento/framework": ">=100.0",
+ "guzzlehttp/guzzle": ">=6.0"
+ },
+ "autoload": {
+ "files": [
+ "registration.php"
+ ],
+ "psr-4": {
+ "Qliro\\QliroOne\\": ""
+ }
+ },
+ "extra": {
+ "vaimo-module-category": "Payment"
+ },
+ "repositories": [
+ {
+ "url": "https://repo.magento.com/",
+ "type": "composer"
+ }
+ ]
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..c20bf40
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,3033 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "5752663b8c9ee8eeaabc5c4493461035",
+ "packages": [
+ {
+ "name": "colinmollenhour/credis",
+ "version": "1.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/colinmollenhour/credis.git",
+ "reference": "9c14b4bb0779127638a17dd8aab8f05f28c6df43"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://packages.vaimo.com/dist/colinmollenhour-credis-9c14b4bb0779127638a17dd8aab8f05f28c6df43-zip-f08d0a.zip",
+ "reference": "9c14b4bb0779127638a17dd8aab8f05f28c6df43",
+ "shasum": "67d590e465a434a991d07b5b920738d29d86e22e"
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "Client.php",
+ "Cluster.php",
+ "Sentinel.php",
+ "Module.php"
+ ]
+ },
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Colin Mollenhour",
+ "email": "colin@mollenhour.com"
+ }
+ ],
+ "description": "Credis is a lightweight interface to the Redis key-value store which wraps the phpredis library when available for better performance.",
+ "homepage": "https://github.com/colinmollenhour/credis",
+ "support": {
+ "source": "https://github.com/colinmollenhour/credis/tree/1.8.2",
+ "issues": "https://github.com/colinmollenhour/credis/issues"
+ },
+ "time": "2017-07-05T15:32:38+00:00"
+ },
+ {
+ "name": "colinmollenhour/php-redis-session-abstract",
+ "version": "v1.3.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/colinmollenhour/php-redis-session-abstract.git",
+ "reference": "9f69f5c1be512d5ff168e731e7fa29ab2905d847"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/colinmollenhour/php-redis-session-abstract/zipball/9f69f5c1be512d5ff168e731e7fa29ab2905d847",
+ "reference": "9f69f5c1be512d5ff168e731e7fa29ab2905d847",
+ "shasum": ""
+ },
+ "require": {
+ "colinmollenhour/credis": "~1.6",
+ "php": "~5.5.0|~5.6.0|~7.0.0|~7.1.0|~7.2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Cm\\RedisSession\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Colin Mollenhour"
+ }
+ ],
+ "description": "A Redis-based session handler with optimistic locking",
+ "homepage": "https://github.com/colinmollenhour/php-redis-session-abstract",
+ "time": "2018-01-08T14:53:13+00:00"
+ },
+ {
+ "name": "composer/ca-bundle",
+ "version": "1.2.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/ca-bundle.git",
+ "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd",
+ "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd",
+ "shasum": ""
+ },
+ "require": {
+ "ext-openssl": "*",
+ "ext-pcre": "*",
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8",
+ "psr/log": "^1.0",
+ "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\CaBundle\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
+ "keywords": [
+ "cabundle",
+ "cacert",
+ "certificate",
+ "ssl",
+ "tls"
+ ],
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-04-08T08:27:21+00:00"
+ },
+ {
+ "name": "composer/composer",
+ "version": "1.10.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/composer.git",
+ "reference": "56e0e094478f30935e9128552188355fa9712291"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/composer/zipball/56e0e094478f30935e9128552188355fa9712291",
+ "reference": "56e0e094478f30935e9128552188355fa9712291",
+ "shasum": ""
+ },
+ "require": {
+ "composer/ca-bundle": "^1.0",
+ "composer/semver": "^1.0",
+ "composer/spdx-licenses": "^1.2",
+ "composer/xdebug-handler": "^1.1",
+ "justinrainbow/json-schema": "^5.2.10",
+ "php": "^5.3.2 || ^7.0",
+ "psr/log": "^1.0",
+ "seld/jsonlint": "^1.4",
+ "seld/phar-utils": "^1.0",
+ "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0",
+ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0",
+ "symfony/finder": "^2.7 || ^3.0 || ^4.0 || ^5.0",
+ "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0"
+ },
+ "conflict": {
+ "symfony/console": "2.8.38"
+ },
+ "require-dev": {
+ "phpspec/prophecy": "^1.10",
+ "symfony/phpunit-bridge": "^4.2"
+ },
+ "suggest": {
+ "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages",
+ "ext-zip": "Enabling the zip extension allows you to unzip archives",
+ "ext-zlib": "Allow gzip compression of HTTP requests"
+ },
+ "bin": [
+ "bin/composer"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.10-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\": "src/Composer"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Composer helps you declare, manage and install dependencies of PHP projects. It ensures you have the right stack everywhere.",
+ "homepage": "https://getcomposer.org/",
+ "keywords": [
+ "autoload",
+ "dependency",
+ "package"
+ ],
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-06-24T19:23:30+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "1.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
+ "reference": "c6bea70230ef4dd483e6bbcab6005f682ed3a8de",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.5 || ^5.0.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "time": "2020-01-13T12:06:48+00:00"
+ },
+ {
+ "name": "composer/spdx-licenses",
+ "version": "1.5.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/spdx-licenses.git",
+ "reference": "0c3e51e1880ca149682332770e25977c70cf9dae"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/0c3e51e1880ca149682332770e25977c70cf9dae",
+ "reference": "0c3e51e1880ca149682332770e25977c70cf9dae",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Spdx\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "SPDX licenses list and validation library.",
+ "keywords": [
+ "license",
+ "spdx",
+ "validator"
+ ],
+ "time": "2020-02-14T07:44:31+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "1.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51",
+ "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0",
+ "psr/log": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-06-04T11:16:35+00:00"
+ },
+ {
+ "name": "container-interop/container-interop",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/container-interop/container-interop.git",
+ "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8",
+ "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8",
+ "shasum": ""
+ },
+ "require": {
+ "psr/container": "^1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Interop\\Container\\": "src/Interop/Container/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
+ "homepage": "https://github.com/container-interop/container-interop",
+ "abandoned": "psr/container",
+ "time": "2017-02-14T19:40:03+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "6.5.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+ "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.0",
+ "guzzlehttp/psr7": "^1.6.1",
+ "php": ">=5.5",
+ "symfony/polyfill-intl-idn": "^1.17.0"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+ "psr/log": "^1.1"
+ },
+ "suggest": {
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.5-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "homepage": "http://guzzlephp.org/",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "rest",
+ "web service"
+ ],
+ "time": "2020-06-16T21:01:06+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "v1.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "time": "2016-12-20T10:07:11+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "1.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a",
+ "reference": "239400de7a173fe9901b9ac7c06497751f00727a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0",
+ "psr/http-message": "~1.0",
+ "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+ },
+ "provide": {
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "ext-zlib": "*",
+ "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8"
+ },
+ "suggest": {
+ "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ },
+ "files": [
+ "src/functions_include.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Schultze",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "time": "2019-07-01T23:21:34+00:00"
+ },
+ {
+ "name": "justinrainbow/json-schema",
+ "version": "5.2.10",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/justinrainbow/json-schema.git",
+ "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
+ "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1",
+ "json-schema/json-schema-test-suite": "1.2.0",
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "bin": [
+ "bin/validate-json"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "JsonSchema\\": "src/JsonSchema/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bruno Prieto Reis",
+ "email": "bruno.p.reis@gmail.com"
+ },
+ {
+ "name": "Justin Rainbow",
+ "email": "justin.rainbow@gmail.com"
+ },
+ {
+ "name": "Igor Wiedler",
+ "email": "igor@wiedler.ch"
+ },
+ {
+ "name": "Robert Schönthal",
+ "email": "seroscho@googlemail.com"
+ }
+ ],
+ "description": "A library to validate a json schema.",
+ "homepage": "https://github.com/justinrainbow/json-schema",
+ "keywords": [
+ "json",
+ "schema"
+ ],
+ "time": "2020-05-27T16:41:55+00:00"
+ },
+ {
+ "name": "magento/framework",
+ "version": "101.0.11",
+ "dist": {
+ "type": "zip",
+ "url": "https://repo.magento.com/archives/magento/framework/magento-framework-101.0.11.0.zip",
+ "shasum": "133ff83acd65ca76dff831dad8967ac661f410a4"
+ },
+ "require": {
+ "colinmollenhour/php-redis-session-abstract": "~1.3.8",
+ "composer/composer": "^1.4",
+ "ext-bcmath": "*",
+ "ext-curl": "*",
+ "ext-dom": "*",
+ "ext-gd": "*",
+ "ext-hash": "*",
+ "ext-iconv": "*",
+ "ext-openssl": "*",
+ "ext-simplexml": "*",
+ "ext-spl": "*",
+ "ext-xsl": "*",
+ "lib-libxml": "*",
+ "magento/zendframework1": "~1.14.0",
+ "monolog/monolog": "^1.17",
+ "oyejorge/less.php": "~1.7.0",
+ "php": "~7.0.13||~7.1.0||~7.2.0",
+ "symfony/console": "~2.3, !=2.7.0",
+ "symfony/process": "~2.1",
+ "tedivm/jshrink": "~1.3.0",
+ "zendframework/zend-code": "~3.1.0",
+ "zendframework/zend-crypt": "^2.6.0",
+ "zendframework/zend-http": "^2.6.0",
+ "zendframework/zend-mvc": "~2.7.12",
+ "zendframework/zend-stdlib": "^2.7.7",
+ "zendframework/zend-uri": "^2.5.1",
+ "zendframework/zend-validator": "^2.6.0"
+ },
+ "suggest": {
+ "ext-imagick": "Use Image Magick >=3.0.0 as an optional alternative image processing library"
+ },
+ "type": "magento2-library",
+ "autoload": {
+ "psr-4": {
+ "Magento\\Framework\\": ""
+ },
+ "files": [
+ "registration.php"
+ ]
+ },
+ "license": [
+ "OSL-3.0",
+ "AFL-3.0"
+ ],
+ "description": "N/A"
+ },
+ {
+ "name": "magento/zendframework1",
+ "version": "1.14.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/magento/zf1.git",
+ "reference": "250f35c0e80b5e6fa1a1598c144cba2fff36b565"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/magento/zf1/zipball/250f35c0e80b5e6fa1a1598c144cba2fff36b565",
+ "reference": "250f35c0e80b5e6fa1a1598c144cba2fff36b565",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.2.11"
+ },
+ "require-dev": {
+ "phpunit/dbunit": "1.3.*",
+ "phpunit/phpunit": "3.7.*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Zend_": "library/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "include-path": [
+ "library/"
+ ],
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Magento Zend Framework 1",
+ "homepage": "http://framework.zend.com/",
+ "keywords": [
+ "ZF1",
+ "framework"
+ ],
+ "time": "2020-05-19T23:25:07+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "1.25.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "3022efff205e2448b560c833c6fbbf91c3139168"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/3022efff205e2448b560c833c6fbbf91c3139168",
+ "reference": "3022efff205e2448b560c833c6fbbf91c3139168",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "psr/log": "~1.0"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "graylog2/gelf-php": "~1.0",
+ "php-amqplib/php-amqplib": "~2.4",
+ "php-console/php-console": "^3.1.3",
+ "php-parallel-lint/php-parallel-lint": "^1.0",
+ "phpunit/phpunit": "~4.5",
+ "ruflin/elastica": ">=0.90 <3.0",
+ "sentry/sentry": "^0.13",
+ "swiftmailer/swiftmailer": "^5.3|^6.0"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-mongo": "Allow sending log messages to a MongoDB server",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "php-console/php-console": "Allow sending log messages to Google Chrome",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+ "sentry/sentry": "Allow sending log messages to a Sentry server"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "http://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-05-22T07:31:27+00:00"
+ },
+ {
+ "name": "oyejorge/less.php",
+ "version": "v1.7.0.14",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/oyejorge/less.php.git",
+ "reference": "42925c5a01a07d67ca7e82dfc8fb31814d557bc9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/oyejorge/less.php/zipball/42925c5a01a07d67ca7e82dfc8fb31814d557bc9",
+ "reference": "42925c5a01a07d67ca7e82dfc8fb31814d557bc9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.8.24"
+ },
+ "bin": [
+ "bin/lessc"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Less": "lib/"
+ },
+ "classmap": [
+ "lessc.inc.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Matt Agar",
+ "homepage": "https://github.com/agar"
+ },
+ {
+ "name": "Martin Jantošovič",
+ "homepage": "https://github.com/Mordred"
+ },
+ {
+ "name": "Josh Schmidt",
+ "homepage": "https://github.com/oyejorge"
+ }
+ ],
+ "description": "PHP port of the Javascript version of LESS http://lesscss.org (Originally maintained by Josh Schmidt)",
+ "homepage": "http://lessphp.gpeasy.com",
+ "keywords": [
+ "css",
+ "less",
+ "less.js",
+ "lesscss",
+ "php",
+ "stylesheet"
+ ],
+ "abandoned": true,
+ "time": "2017-03-28T22:19:25+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+ "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "time": "2017-02-14T16:28:37+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://packages.vaimo.com/dist/psr-http-message-f6561bf28d520154e4b0ec72be95418abe6d9363-zip-7ab320.zip",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": "fbd8cb79dc03965cd1ce5f49514ca9aa27750654"
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "homepage": "http://www.php-fig.org/",
+ "name": "PHP-FIG"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/1.0.1"
+ },
+ "time": "2016-08-06T14:39:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
+ "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "time": "2020-03-23T09:12:05+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "seld/jsonlint",
+ "version": "1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/jsonlint.git",
+ "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1",
+ "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ },
+ "bin": [
+ "bin/jsonlint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Seld\\JsonLint\\": "src/Seld/JsonLint/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "JSON Linter",
+ "keywords": [
+ "json",
+ "linter",
+ "parser",
+ "validator"
+ ],
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-04-30T19:05:18+00:00"
+ },
+ {
+ "name": "seld/phar-utils",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/phar-utils.git",
+ "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8800503d56b9867d43d9c303b9cbcc26016e82f0",
+ "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Seld\\PharUtils\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be"
+ }
+ ],
+ "description": "PHAR file format utilities, for when PHP phars you up",
+ "keywords": [
+ "phar"
+ ],
+ "time": "2020-02-14T15:25:33+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12",
+ "reference": "cbcf4b5e233af15cd2bbd50dee1ccc9b7927dc12",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9",
+ "symfony/debug": "^2.7.2|~3.0.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "require-dev": {
+ "psr/log": "~1.0",
+ "symfony/event-dispatcher": "~2.1|~3.0.0",
+ "symfony/process": "~2.1|~3.0.0"
+ },
+ "suggest": {
+ "psr/log-implementation": "For using the console logger",
+ "symfony/event-dispatcher": "",
+ "symfony/process": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Console Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-11-20T15:55:20+00:00"
+ },
+ {
+ "name": "symfony/debug",
+ "version": "v3.0.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/debug.git",
+ "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a",
+ "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5.9",
+ "psr/log": "~1.0"
+ },
+ "conflict": {
+ "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
+ },
+ "require-dev": {
+ "symfony/class-loader": "~2.8|~3.0",
+ "symfony/http-kernel": "~2.8|~3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Debug\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Debug Component",
+ "homepage": "https://symfony.com",
+ "time": "2016-07-30T07:22:48+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v3.4.42",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "0f625d0cb1e59c8c4ba61abb170125175218ff10"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/0f625d0cb1e59c8c4ba61abb170125175218ff10",
+ "reference": "0f625d0cb1e59c8c4ba61abb170125175218ff10",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Filesystem Component",
+ "homepage": "https://symfony.com",
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-05-30T17:48:24+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v3.4.42",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "5ec813ccafa8164ef21757e8c725d3a57da59200"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/5ec813ccafa8164ef21757e8c725d3a57da59200",
+ "reference": "5ec813ccafa8164ef21757e8c725d3a57da59200",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9|>=7.0.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Finder Component",
+ "homepage": "https://symfony.com",
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-02-14T07:34:21+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.17.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
+ "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.17-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-06-06T08:46:27+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-idn",
+ "version": "v1.17.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-idn.git",
+ "reference": "a57f8161502549a742a63c09f0a604997bf47027"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a57f8161502549a742a63c09f0a604997bf47027",
+ "reference": "a57f8161502549a742a63c09f0a604997bf47027",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/polyfill-mbstring": "^1.3",
+ "symfony/polyfill-php72": "^1.10"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.17-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Idn\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Laurent Bassin",
+ "email": "laurent@bassin.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "idn",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-06-06T08:46:27+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.17.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "7110338d81ce1cbc3e273136e4574663627037a7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7110338d81ce1cbc3e273136e4574663627037a7",
+ "reference": "7110338d81ce1cbc3e273136e4574663627037a7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.17-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-06-06T08:46:27+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php72",
+ "version": "v1.17.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php72.git",
+ "reference": "f048e612a3905f34931127360bdd2def19a5e582"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582",
+ "reference": "f048e612a3905f34931127360bdd2def19a5e582",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.17-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Php72\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2020-05-12T16:47:27+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v2.8.52",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8",
+ "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Process Component",
+ "homepage": "https://symfony.com",
+ "time": "2018-11-11T11:18:13+00:00"
+ },
+ {
+ "name": "tedivm/jshrink",
+ "version": "v1.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tedious/JShrink.git",
+ "reference": "566e0c731ba4e372be2de429ef7d54f4faf4477a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tedious/JShrink/zipball/566e0c731ba4e372be2de429ef7d54f4faf4477a",
+ "reference": "566e0c731ba4e372be2de429ef7d54f4faf4477a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6|^7.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.8",
+ "php-coveralls/php-coveralls": "^1.1.0",
+ "phpunit/phpunit": "^6"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "JShrink": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Robert Hafner",
+ "email": "tedivm@tedivm.com"
+ }
+ ],
+ "description": "Javascript Minifier built in PHP",
+ "homepage": "http://github.com/tedious/JShrink",
+ "keywords": [
+ "javascript",
+ "minifier"
+ ],
+ "time": "2019-06-28T18:11:46+00:00"
+ },
+ {
+ "name": "zendframework/zend-code",
+ "version": "3.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-code.git",
+ "reference": "2899c17f83a7207f2d7f53ec2f421204d3beea27"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-code/zipball/2899c17f83a7207f2d7f53ec2f421204d3beea27",
+ "reference": "2899c17f83a7207f2d7f53ec2f421204d3beea27",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || 7.0.0 - 7.0.4 || ^7.0.6",
+ "zendframework/zend-eventmanager": "^2.6 || ^3.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "~1.0",
+ "ext-phar": "*",
+ "phpunit/phpunit": "^4.8.21",
+ "squizlabs/php_codesniffer": "^2.5",
+ "zendframework/zend-stdlib": "^2.7 || ^3.0"
+ },
+ "suggest": {
+ "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features",
+ "zendframework/zend-stdlib": "Zend\\Stdlib component"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev",
+ "dev-develop": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Code\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "provides facilities to generate arbitrary code using an object oriented interface",
+ "homepage": "https://github.com/zendframework/zend-code",
+ "keywords": [
+ "code",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-code",
+ "time": "2016-10-24T13:23:32+00:00"
+ },
+ {
+ "name": "zendframework/zend-console",
+ "version": "2.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-console.git",
+ "reference": "e8aa08da83de3d265256c40ba45cd649115f0e18"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-console/zipball/e8aa08da83de3d265256c40ba45cd649115f0e18",
+ "reference": "e8aa08da83de3d265256c40ba45cd649115f0e18",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-stdlib": "^2.7.7 || ^3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7.23 || ^6.4.3",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-filter": "^2.7.2",
+ "zendframework/zend-json": "^2.6 || ^3.0",
+ "zendframework/zend-validator": "^2.10.1"
+ },
+ "suggest": {
+ "zendframework/zend-filter": "To support DefaultRouteMatcher usage",
+ "zendframework/zend-validator": "To support DefaultRouteMatcher usage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7.x-dev",
+ "dev-develop": "2.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Console\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Build console applications using getopt syntax or routing, complete with prompts",
+ "keywords": [
+ "ZendFramework",
+ "console",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-console",
+ "time": "2018-01-25T19:08:04+00:00"
+ },
+ {
+ "name": "zendframework/zend-crypt",
+ "version": "2.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-crypt.git",
+ "reference": "1b2f5600bf6262904167116fa67b58ab1457036d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-crypt/zipball/1b2f5600bf6262904167116fa67b58ab1457036d",
+ "reference": "1b2f5600bf6262904167116fa67b58ab1457036d",
+ "shasum": ""
+ },
+ "require": {
+ "container-interop/container-interop": "~1.0",
+ "php": "^5.5 || ^7.0",
+ "zendframework/zend-math": "^2.6",
+ "zendframework/zend-stdlib": "^2.7 || ^3.0"
+ },
+ "require-dev": {
+ "fabpot/php-cs-fixer": "1.7.*",
+ "phpunit/phpunit": "~4.0"
+ },
+ "suggest": {
+ "ext-mcrypt": "Required for most features of Zend\\Crypt"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6-dev",
+ "dev-develop": "2.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Crypt\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "homepage": "https://github.com/zendframework/zend-crypt",
+ "keywords": [
+ "crypt",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-crypt",
+ "time": "2016-02-03T23:46:30+00:00"
+ },
+ {
+ "name": "zendframework/zend-diactoros",
+ "version": "1.8.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-diactoros.git",
+ "reference": "a85e67b86e9b8520d07e6415fcbcb8391b44a75b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/a85e67b86e9b8520d07e6415fcbcb8391b44a75b",
+ "reference": "a85e67b86e9b8520d07e6415fcbcb8391b44a75b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "psr/http-message": "^1.0"
+ },
+ "provide": {
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "php-http/psr7-integration-tests": "dev-master",
+ "phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7",
+ "zendframework/zend-coding-standard": "~1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-release-1.8": "1.8.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions/create_uploaded_file.php",
+ "src/functions/marshal_headers_from_sapi.php",
+ "src/functions/marshal_method_from_sapi.php",
+ "src/functions/marshal_protocol_version_from_sapi.php",
+ "src/functions/marshal_uri_from_sapi.php",
+ "src/functions/normalize_server.php",
+ "src/functions/normalize_uploaded_files.php",
+ "src/functions/parse_cookie_header.php"
+ ],
+ "psr-4": {
+ "Zend\\Diactoros\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "description": "PSR HTTP Message implementations",
+ "homepage": "https://github.com/zendframework/zend-diactoros",
+ "keywords": [
+ "http",
+ "psr",
+ "psr-7"
+ ],
+ "abandoned": "laminas/laminas-diactoros",
+ "time": "2019-08-06T17:53:53+00:00"
+ },
+ {
+ "name": "zendframework/zend-escaper",
+ "version": "2.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-escaper.git",
+ "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-escaper/zipball/3801caa21b0ca6aca57fa1c42b08d35c395ebd5f",
+ "reference": "3801caa21b0ca6aca57fa1c42b08d35c395ebd5f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2",
+ "zendframework/zend-coding-standard": "~1.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6.x-dev",
+ "dev-develop": "2.7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Escaper\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs",
+ "keywords": [
+ "ZendFramework",
+ "escaper",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-escaper",
+ "time": "2019-09-05T20:03:20+00:00"
+ },
+ {
+ "name": "zendframework/zend-eventmanager",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-eventmanager.git",
+ "reference": "a5e2583a211f73604691586b8406ff7296a946dd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-eventmanager/zipball/a5e2583a211f73604691586b8406ff7296a946dd",
+ "reference": "a5e2583a211f73604691586b8406ff7296a946dd",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "athletic/athletic": "^0.1",
+ "container-interop/container-interop": "^1.1.0",
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-stdlib": "^2.7.3 || ^3.0"
+ },
+ "suggest": {
+ "container-interop/container-interop": "^1.1.0, to use the lazy listeners feature",
+ "zendframework/zend-stdlib": "^2.7.3 || ^3.0, to use the FilterChain feature"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.2-dev",
+ "dev-develop": "3.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\EventManager\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Trigger and listen to events within a PHP application",
+ "homepage": "https://github.com/zendframework/zend-eventmanager",
+ "keywords": [
+ "event",
+ "eventmanager",
+ "events",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-eventmanager",
+ "time": "2018-04-25T15:33:34+00:00"
+ },
+ {
+ "name": "zendframework/zend-filter",
+ "version": "2.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-filter.git",
+ "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-filter/zipball/d78f2cdde1c31975e18b2a0753381ed7b61118ef",
+ "reference": "d78f2cdde1c31975e18b2a0753381ed7b61118ef",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-stdlib": "^2.7.7 || ^3.1"
+ },
+ "conflict": {
+ "zendframework/zend-validator": "<2.10.1"
+ },
+ "require-dev": {
+ "pear/archive_tar": "^1.4.3",
+ "phpunit/phpunit": "^5.7.23 || ^6.4.3",
+ "psr/http-factory": "^1.0",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-crypt": "^3.2.1",
+ "zendframework/zend-servicemanager": "^2.7.8 || ^3.3",
+ "zendframework/zend-uri": "^2.6"
+ },
+ "suggest": {
+ "psr/http-factory-implementation": "psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters",
+ "zendframework/zend-crypt": "Zend\\Crypt component, for encryption filters",
+ "zendframework/zend-i18n": "Zend\\I18n component for filters depending on i18n functionality",
+ "zendframework/zend-servicemanager": "Zend\\ServiceManager component, for using the filter chain functionality",
+ "zendframework/zend-uri": "Zend\\Uri component, for the UriNormalize filter"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.9.x-dev",
+ "dev-develop": "2.10.x-dev"
+ },
+ "zf": {
+ "component": "Zend\\Filter",
+ "config-provider": "Zend\\Filter\\ConfigProvider"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Filter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Programmatically filter and normalize data and files",
+ "keywords": [
+ "ZendFramework",
+ "filter",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-filter",
+ "time": "2019-08-19T07:08:04+00:00"
+ },
+ {
+ "name": "zendframework/zend-form",
+ "version": "2.13.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-form.git",
+ "reference": "c713a12ccbd43148b71c9339e171ca11e3f8a1da"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-form/zipball/c713a12ccbd43148b71c9339e171ca11e3f8a1da",
+ "reference": "c713a12ccbd43148b71c9339e171ca11e3f8a1da",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-hydrator": "^1.1 || ^2.1 || ^3.0",
+ "zendframework/zend-inputfilter": "^2.8",
+ "zendframework/zend-stdlib": "^2.7 || ^3.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "~1.0",
+ "phpunit/phpunit": "^5.7.23 || ^6.5.3",
+ "zendframework/zend-cache": "^2.6.1",
+ "zendframework/zend-captcha": "^2.7.1",
+ "zendframework/zend-code": "^2.6 || ^3.0",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-escaper": "^2.5",
+ "zendframework/zend-eventmanager": "^2.6.2 || ^3.0",
+ "zendframework/zend-filter": "^2.6",
+ "zendframework/zend-i18n": "^2.6",
+ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3",
+ "zendframework/zend-session": "^2.8.1",
+ "zendframework/zend-text": "^2.6",
+ "zendframework/zend-validator": "^2.6",
+ "zendframework/zend-view": "^2.6.2",
+ "zendframework/zendservice-recaptcha": "^3.0.0"
+ },
+ "suggest": {
+ "zendframework/zend-captcha": "^2.7.1, required for using CAPTCHA form elements",
+ "zendframework/zend-code": "^2.6 || ^3.0, required to use zend-form annotations support",
+ "zendframework/zend-eventmanager": "^2.6.2 || ^3.0, reuired for zend-form annotations support",
+ "zendframework/zend-i18n": "^2.6, required when using zend-form view helpers",
+ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3, required to use the form factories or provide services",
+ "zendframework/zend-view": "^2.6.2, required for using the zend-form view helpers",
+ "zendframework/zendservice-recaptcha": "in order to use the ReCaptcha form element"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.13.x-dev",
+ "dev-develop": "2.14.x-dev"
+ },
+ "zf": {
+ "component": "Zend\\Form",
+ "config-provider": "Zend\\Form\\ConfigProvider"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Form\\": "src/"
+ },
+ "files": [
+ "autoload/formElementManagerPolyfill.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Validate and display simple and complex forms, casting forms to business objects and vice versa",
+ "keywords": [
+ "ZendFramework",
+ "form",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-form",
+ "time": "2018-12-11T22:51:29+00:00"
+ },
+ {
+ "name": "zendframework/zend-http",
+ "version": "2.8.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-http.git",
+ "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-http/zipball/d160aedc096be230af0fe9c31151b2b33ad4e807",
+ "reference": "d160aedc096be230af0fe9c31151b2b33ad4e807",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-loader": "^2.5.1",
+ "zendframework/zend-stdlib": "^3.1 || ^2.7.7",
+ "zendframework/zend-uri": "^2.5.2",
+ "zendframework/zend-validator": "^2.10.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.3",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-config": "^3.1 || ^2.6"
+ },
+ "suggest": {
+ "paragonie/certainty": "For automated management of cacert.pem"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8.x-dev",
+ "dev-develop": "2.9.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Http\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Provides an easy interface for performing Hyper-Text Transfer Protocol (HTTP) requests",
+ "keywords": [
+ "ZendFramework",
+ "http",
+ "http client",
+ "zend",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-http",
+ "time": "2019-02-07T17:47:08+00:00"
+ },
+ {
+ "name": "zendframework/zend-hydrator",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-hydrator.git",
+ "reference": "22652e1661a5a10b3f564cf7824a2206cf5a4a65"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/22652e1661a5a10b3f564cf7824a2206cf5a4a65",
+ "reference": "22652e1661a5a10b3f564cf7824a2206cf5a4a65",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5 || ^7.0",
+ "zendframework/zend-stdlib": "^2.7 || ^3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0",
+ "squizlabs/php_codesniffer": "^2.0@dev",
+ "zendframework/zend-eventmanager": "^2.6.2 || ^3.0",
+ "zendframework/zend-filter": "^2.6",
+ "zendframework/zend-inputfilter": "^2.6",
+ "zendframework/zend-serializer": "^2.6.1",
+ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3"
+ },
+ "suggest": {
+ "zendframework/zend-eventmanager": "^2.6.2 || ^3.0, to support aggregate hydrator usage",
+ "zendframework/zend-filter": "^2.6, to support naming strategy hydrator usage",
+ "zendframework/zend-serializer": "^2.6.1, to use the SerializableStrategy",
+ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3, to support hydrator plugin manager usage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-release-1.0": "1.0-dev",
+ "dev-release-1.1": "1.1-dev",
+ "dev-master": "2.0-dev",
+ "dev-develop": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Hydrator\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "homepage": "https://github.com/zendframework/zend-hydrator",
+ "keywords": [
+ "hydrator",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-hydrator",
+ "time": "2016-02-18T22:38:26+00:00"
+ },
+ {
+ "name": "zendframework/zend-inputfilter",
+ "version": "2.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-inputfilter.git",
+ "reference": "e7edd625f2fcdd72a719a7023114c5f4b4f38488"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-inputfilter/zipball/e7edd625f2fcdd72a719a7023114c5f4b4f38488",
+ "reference": "e7edd625f2fcdd72a719a7023114c5f4b4f38488",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-filter": "^2.6",
+ "zendframework/zend-stdlib": "^2.7 || ^3.0",
+ "zendframework/zend-validator": "^2.10.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7.23 || ^6.4.3",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3"
+ },
+ "suggest": {
+ "zendframework/zend-servicemanager": "To support plugin manager support"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev",
+ "dev-develop": "2.9-dev"
+ },
+ "zf": {
+ "component": "Zend\\InputFilter",
+ "config-provider": "Zend\\InputFilter\\ConfigProvider"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\InputFilter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Normalize and validate input sets from the web, APIs, the CLI, and more, including files",
+ "keywords": [
+ "ZendFramework",
+ "inputfilter",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-inputfilter",
+ "time": "2017-12-04T21:24:25+00:00"
+ },
+ {
+ "name": "zendframework/zend-loader",
+ "version": "2.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-loader.git",
+ "reference": "91da574d29b58547385b2298c020b257310898c6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-loader/zipball/91da574d29b58547385b2298c020b257310898c6",
+ "reference": "91da574d29b58547385b2298c020b257310898c6",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.4",
+ "zendframework/zend-coding-standard": "~1.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.6.x-dev",
+ "dev-develop": "2.7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Loader\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "Autoloading and plugin loading strategies",
+ "keywords": [
+ "ZendFramework",
+ "loader",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-loader",
+ "time": "2019-09-04T19:38:14+00:00"
+ },
+ {
+ "name": "zendframework/zend-math",
+ "version": "2.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-math.git",
+ "reference": "1abce074004dacac1a32cd54de94ad47ef960d38"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-math/zipball/1abce074004dacac1a32cd54de94ad47ef960d38",
+ "reference": "1abce074004dacac1a32cd54de94ad47ef960d38",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5 || ^7.0"
+ },
+ "require-dev": {
+ "fabpot/php-cs-fixer": "1.7.*",
+ "ircmaxell/random-lib": "~1.1",
+ "phpunit/phpunit": "~4.0"
+ },
+ "suggest": {
+ "ext-bcmath": "If using the bcmath functionality",
+ "ext-gmp": "If using the gmp functionality",
+ "ircmaxell/random-lib": "Fallback random byte generator for Zend\\Math\\Rand if Mcrypt extensions is unavailable"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev",
+ "dev-develop": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Math\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "homepage": "https://github.com/zendframework/zend-math",
+ "keywords": [
+ "math",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-math",
+ "time": "2018-12-04T15:34:17+00:00"
+ },
+ {
+ "name": "zendframework/zend-mvc",
+ "version": "2.7.15",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-mvc.git",
+ "reference": "a8d45689d37a9e4ff4b75ea0b7478fa3d4f9c089"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-mvc/zipball/a8d45689d37a9e4ff4b75ea0b7478fa3d4f9c089",
+ "reference": "a8d45689d37a9e4ff4b75ea0b7478fa3d4f9c089",
+ "shasum": ""
+ },
+ "require": {
+ "container-interop/container-interop": "^1.1",
+ "php": "^5.5 || ^7.0",
+ "zendframework/zend-console": "^2.7",
+ "zendframework/zend-eventmanager": "^2.6.4 || ^3.0",
+ "zendframework/zend-form": "^2.11",
+ "zendframework/zend-hydrator": "^1.1 || ^2.4",
+ "zendframework/zend-psr7bridge": "^0.2",
+ "zendframework/zend-servicemanager": "^2.7.10 || ^3.0.3",
+ "zendframework/zend-stdlib": "^2.7.5 || ^3.0"
+ },
+ "replace": {
+ "zendframework/zend-router": "^2.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "1.7.*",
+ "phpunit/phpunit": "^4.8.36",
+ "sebastian/comparator": "^1.2.4",
+ "sebastian/version": "^1.0.4",
+ "zendframework/zend-authentication": "^2.6",
+ "zendframework/zend-cache": "^2.8",
+ "zendframework/zend-di": "^2.6",
+ "zendframework/zend-filter": "^2.8",
+ "zendframework/zend-http": "^2.8",
+ "zendframework/zend-i18n": "^2.8",
+ "zendframework/zend-inputfilter": "^2.8",
+ "zendframework/zend-json": "^2.6.1",
+ "zendframework/zend-log": "^2.9.3",
+ "zendframework/zend-modulemanager": "^2.8",
+ "zendframework/zend-serializer": "^2.8",
+ "zendframework/zend-session": "^2.8.1",
+ "zendframework/zend-text": "^2.7",
+ "zendframework/zend-uri": "^2.6",
+ "zendframework/zend-validator": "^2.10",
+ "zendframework/zend-view": "^2.9"
+ },
+ "suggest": {
+ "zendframework/zend-authentication": "Zend\\Authentication component for Identity plugin",
+ "zendframework/zend-config": "Zend\\Config component",
+ "zendframework/zend-di": "Zend\\Di component",
+ "zendframework/zend-filter": "Zend\\Filter component",
+ "zendframework/zend-http": "Zend\\Http component",
+ "zendframework/zend-i18n": "Zend\\I18n component for translatable segments",
+ "zendframework/zend-inputfilter": "Zend\\Inputfilter component",
+ "zendframework/zend-json": "Zend\\Json component",
+ "zendframework/zend-log": "Zend\\Log component",
+ "zendframework/zend-modulemanager": "Zend\\ModuleManager component",
+ "zendframework/zend-serializer": "Zend\\Serializer component",
+ "zendframework/zend-servicemanager-di": "^1.0.1, if using zend-servicemanager v3 and requiring the zend-di integration",
+ "zendframework/zend-session": "Zend\\Session component for FlashMessenger, PRG, and FPRG plugins",
+ "zendframework/zend-text": "Zend\\Text component",
+ "zendframework/zend-uri": "Zend\\Uri component",
+ "zendframework/zend-validator": "Zend\\Validator component",
+ "zendframework/zend-view": "Zend\\View component"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev",
+ "dev-develop": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/autoload.php"
+ ],
+ "psr-4": {
+ "Zend\\Mvc\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "homepage": "https://github.com/zendframework/zend-mvc",
+ "keywords": [
+ "mvc",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-mvc",
+ "time": "2018-05-03T13:13:41+00:00"
+ },
+ {
+ "name": "zendframework/zend-psr7bridge",
+ "version": "0.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-psr7bridge.git",
+ "reference": "86c0b53b0c6381391c4add4a93a56e51d5c74605"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-psr7bridge/zipball/86c0b53b0c6381391c4add4a93a56e51d5c74605",
+ "reference": "86c0b53b0c6381391c4add4a93a56e51d5c74605",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.5",
+ "psr/http-message": "^1.0",
+ "zendframework/zend-diactoros": "^1.1",
+ "zendframework/zend-http": "^2.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.7",
+ "squizlabs/php_codesniffer": "^2.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev",
+ "dev-develop": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Psr7Bridge\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "PSR-7 <-> Zend\\Http bridge",
+ "homepage": "https://github.com/zendframework/zend-psr7bridge",
+ "keywords": [
+ "http",
+ "psr",
+ "psr-7"
+ ],
+ "abandoned": "laminas/laminas-psr7bridge",
+ "time": "2016-05-10T21:44:39+00:00"
+ },
+ {
+ "name": "zendframework/zend-servicemanager",
+ "version": "3.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-servicemanager.git",
+ "reference": "8a6078959a2e8c3717ee4945d4a2d9f3ddf81d38"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/8a6078959a2e8c3717ee4945d4a2d9f3ddf81d38",
+ "reference": "8a6078959a2e8c3717ee4945d4a2d9f3ddf81d38",
+ "shasum": ""
+ },
+ "require": {
+ "container-interop/container-interop": "~1.0",
+ "php": "^5.5 || ^7.0"
+ },
+ "provide": {
+ "container-interop/container-interop-implementation": "^1.1"
+ },
+ "require-dev": {
+ "ocramius/proxy-manager": "^1.0 || ^2.0",
+ "phpbench/phpbench": "^0.10.0",
+ "phpunit/phpunit": "^4.6 || ^5.2.10",
+ "zendframework/zend-coding-standard": "~1.0.0"
+ },
+ "suggest": {
+ "ocramius/proxy-manager": "ProxyManager 1.* to handle lazy initialization of services",
+ "zendframework/zend-stdlib": "zend-stdlib ^2.5 if you wish to use the MergeReplaceKey or MergeRemoveKey features in Config instances"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev",
+ "dev-develop": "3.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\ServiceManager\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "homepage": "https://github.com/zendframework/zend-servicemanager",
+ "keywords": [
+ "service-manager",
+ "servicemanager",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-servicemanager",
+ "time": "2016-12-19T19:51:37+00:00"
+ },
+ {
+ "name": "zendframework/zend-stdlib",
+ "version": "2.7.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-stdlib.git",
+ "reference": "0e44eb46788f65e09e077eb7f44d2659143bcc1f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/0e44eb46788f65e09e077eb7f44d2659143bcc1f",
+ "reference": "0e44eb46788f65e09e077eb7f44d2659143bcc1f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5 || ^7.0",
+ "zendframework/zend-hydrator": "~1.1"
+ },
+ "require-dev": {
+ "athletic/athletic": "~0.1",
+ "fabpot/php-cs-fixer": "1.7.*",
+ "phpunit/phpunit": "~4.0",
+ "zendframework/zend-config": "~2.5",
+ "zendframework/zend-eventmanager": "~2.5",
+ "zendframework/zend-filter": "~2.5",
+ "zendframework/zend-inputfilter": "~2.5",
+ "zendframework/zend-serializer": "~2.5",
+ "zendframework/zend-servicemanager": "~2.5"
+ },
+ "suggest": {
+ "zendframework/zend-eventmanager": "To support aggregate hydrator usage",
+ "zendframework/zend-filter": "To support naming strategy hydrator usage",
+ "zendframework/zend-serializer": "Zend\\Serializer component",
+ "zendframework/zend-servicemanager": "To support hydrator plugin manager usage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-release-2.7": "2.7-dev",
+ "dev-master": "3.0-dev",
+ "dev-develop": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Stdlib\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "homepage": "https://github.com/zendframework/zend-stdlib",
+ "keywords": [
+ "stdlib",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-stdlib",
+ "time": "2016-04-12T21:17:31+00:00"
+ },
+ {
+ "name": "zendframework/zend-uri",
+ "version": "2.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-uri.git",
+ "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-uri/zipball/bfc4a5b9a309711e968d7c72afae4ac50c650083",
+ "reference": "bfc4a5b9a309711e968d7c72afae4ac50c650083",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-escaper": "^2.5",
+ "zendframework/zend-validator": "^2.10"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.4",
+ "zendframework/zend-coding-standard": "~1.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7.x-dev",
+ "dev-develop": "2.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Uri\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "A component that aids in manipulating and validating » Uniform Resource Identifiers (URIs)",
+ "keywords": [
+ "ZendFramework",
+ "uri",
+ "zf"
+ ],
+ "abandoned": "laminas/laminas-uri",
+ "time": "2019-10-07T13:35:33+00:00"
+ },
+ {
+ "name": "zendframework/zend-validator",
+ "version": "2.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/zendframework/zend-validator.git",
+ "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/zendframework/zend-validator/zipball/3c28dfe4e5951ba38059cea895244d9d206190b3",
+ "reference": "3c28dfe4e5951ba38059cea895244d9d206190b3",
+ "shasum": ""
+ },
+ "require": {
+ "container-interop/container-interop": "^1.1",
+ "php": "^5.6 || ^7.0",
+ "zendframework/zend-stdlib": "^2.7.6 || ^3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0.8 || ^5.7.15",
+ "psr/http-message": "^1.0",
+ "zendframework/zend-cache": "^2.6.1",
+ "zendframework/zend-coding-standard": "~1.0.0",
+ "zendframework/zend-config": "^2.6",
+ "zendframework/zend-db": "^2.7",
+ "zendframework/zend-filter": "^2.6",
+ "zendframework/zend-http": "^2.5.4",
+ "zendframework/zend-i18n": "^2.6",
+ "zendframework/zend-math": "^2.6",
+ "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3",
+ "zendframework/zend-session": "^2.8",
+ "zendframework/zend-uri": "^2.5"
+ },
+ "suggest": {
+ "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators",
+ "zendframework/zend-db": "Zend\\Db component, required by the (No)RecordExists validator",
+ "zendframework/zend-filter": "Zend\\Filter component, required by the Digits validator",
+ "zendframework/zend-i18n": "Zend\\I18n component to allow translation of validation error messages",
+ "zendframework/zend-i18n-resources": "Translations of validator messages",
+ "zendframework/zend-math": "Zend\\Math component, required by the Csrf validator",
+ "zendframework/zend-servicemanager": "Zend\\ServiceManager component to allow using the ValidatorPluginManager and validator chains",
+ "zendframework/zend-session": "Zend\\Session component, ^2.8; required by the Csrf validator",
+ "zendframework/zend-uri": "Zend\\Uri component, required by the Uri and Sitemap\\Loc validators"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.11.x-dev",
+ "dev-develop": "2.12.x-dev"
+ },
+ "zf": {
+ "component": "Zend\\Validator",
+ "config-provider": "Zend\\Validator\\ConfigProvider"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Zend\\Validator\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "provides a set of commonly needed validators",
+ "homepage": "https://github.com/zendframework/zend-validator",
+ "keywords": [
+ "validator",
+ "zf2"
+ ],
+ "abandoned": "laminas/laminas-validator",
+ "time": "2019-01-29T22:26:39+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "ext-json": "*"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "1.1.0"
+}
diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml
new file mode 100644
index 0000000..5047aa0
--- /dev/null
+++ b/etc/adminhtml/system.xml
@@ -0,0 +1,334 @@
+
+
+
+
+
+
+ QliroOne Checkout
+
+
+ Enabled
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Title
+
+
+
+ Debug Mode
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Eager Checkout Refresh
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+
+ 1
+
+
+ General Module Settings
+
+
+ Geo IP
+ GeoIP mode will only detect the country if corresponding extension is installed on the server. Otherwise the country selected as website default country in System Configuration will always be used.
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Logging Level
+ Qliro\QliroOne\Model\Config\Source\LogLevels
+
+
+
+ New Order Status
+ Magento\Sales\Model\Config\Source\Order\Status\NewStatus
+
+
+
+ Payment from Applicable Countries
+ Magento\Shipping\Model\Config\Source\Allspecificcountries
+
+
+
+ Payment from Specific Countries
+
+ Magento\Directory\Model\Config\Source\Country
+
+
+ 1
+
+
+
+
+ Trigger capture when shipment is created
+ Magento\Config\Model\Config\Source\Yesno
+ This will also auto create invoice if capture was successful
+
+
+
+ Trigger capture when invoice is created
+ Magento\Config\Model\Config\Source\Yesno
+ This will only occur if invoice is created before shipment
+
+
+
+ Allow Newsletter Signup
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Require Identity Verification
+ Magento\Config\Model\Config\Source\Yesno
+
+
+
+ Minimum Customer Age
+ validate-number validate-zero-or-greater
+
+
+
+
+
+ 1
+
+
+ Merchant Configuration Settings
+
+
+ API type
+ Qliro\QliroOne\Model\Config\Source\ApiType
+
+
+
+ Merchant API Key
+
+
+
+ Merchant API Secret
+ Magento\Config\Model\Config\Backend\Encrypted
+
+
+
+ Preset Shipping Address
+ Magento\Config\Model\Config\Source\Yesno
+ Use store address for shipping methods until the real shipping address is available.
+
+
+
+
+
+ 1
+
+
+ CSS Styling Input Fields
+
+ Note that all colors must be HEX values with a leading # character, and all radii should be specified in pixels without "px" afterwards.
Only colors with saturation <= 10% are supported. If a color with saturation > 10% is provided, the saturation will be lowered to 10%.
If any of these values are specified incorrectly, Qliro One won't be able to create order.
]]>
+
+
+ Background Color
+ HEX color code to use as background color in Qliro One.
+
+
+
+ Primary Color
+ HEX color code to use as primary color in Qliro One.
+
+
+
+ Call To Action Color
+ HEX color code to use as call to action color in Qliro One.
+
+
+
+ Call To Action Hover Color
+ HEX color code to use as call to action hover color in Qliro One. If not provided, the hover color will be a blend between the call to action color and the background color.
+
+
+
+ Corner Radius
+ A pixel value to be used on corners throughout Qliro One.
+
+
+
+ Button Corner Radius
+ A pixel value to be used on corners of buttons throughout Qliro One.
+
+
+
+
+
+ 1
+
+
+ Merchant Specific Information
+
+
+ Fee Merchant Reference
+
+
+
+ Terms URL
+ If it is not a proper URL, Qliro One won't be able to create orders.]]>
+ validate-url
+
+
+
+ Integrity Policy URL
+ If it is not omitted or a proper URL, Qliro One won't be able to create orders.]]>
+ validate-url
+
+
+
+
+
+ 1
+
+
+ Notification Callbacks
+
+
+ XDebug Session Flag Name for callback URLs
+
+ 1
+
+
+
+
+ Redirect Callbacks
+ Magento\Config\Model\Config\Source\Yesno
+ To enable debugging of code that require callbacks it's possible to direct the callbacks to a real, public server. These callbacks should be redirected by running the script `ssh -vN -R11222:localhost:80 $server`. $server is the name of the server that receive the callbacks. This process is terminated with ^C.
+
+
+
+ URI Prefix for Callbacks
+ validate-url
+
+ 1
+
+
+
+
+
+ 1
+
+
+ Nshift Integration
+
+
+ Enabled
+ Magento\Config\Model\Config\Source\Yesno
+ If you active this, your Qliro account must also have it activated
+
+
+
+ Nshift Checkout ID
+
+ 1
+
+
+
+
+ Parameters
+
+ 1
+
+ Qliro\QliroOne\Block\Adminhtml\Form\Field\Parameters
+ Magento\Config\Model\Config\Backend\Serialized\ArraySerialized
+
+ Example tags are
+ "skrymmande, Bulk, bulk" ,
+ "weight, Weight, weight" ,
+ "cartprice, Cart Price, grand_total" .
+ The last example means the tag to Nshift is called cartprice, the function is Cart Price and we use the grand_total attribute of the cart.]]>
+
+
+
+
+
+
+
+
+ Tax Class for Payment Fee
+ Magento\Tax\Model\TaxClass\Source\Product
+
+
+
+
+ Payment Fee
+ This sets whether payment fees entered in Magento Admin include tax.
+ Magento\Tax\Model\Config\Price\IncludePrice
+ Magento\Tax\Model\System\Config\Source\PriceType
+
+
+
+
+
+ Display Payment Fee Prices
+ Magento\Tax\Model\System\Config\Source\Tax\Display\Type
+
+
+
+
+ Display Payment Fee Prices
+ Magento\Tax\Model\System\Config\Source\Tax\Display\Type
+
+
+
+
+
+ Qliro One Nshift Shipping
+
+ Enabled
+ Magento\Config\Model\Config\Source\Yesno
+
+
+ Method Name
+
+
+ Price
+ validate-number validate-zero-or-greater
+
+
+ Calculate Handling Fee
+ Magento\Shipping\Model\Source\HandlingType
+
+
+ Handling Fee
+ validate-number validate-zero-or-greater
+
+
+ Title
+
+
+ Ship to Applicable Countries
+ shipping-applicable-country
+ Magento\Shipping\Model\Config\Source\Allspecificcountries
+
+
+ Ship to Specific Countries
+ Magento\Directory\Model\Config\Source\Country
+ 1
+
+
+ Show Method in Frontend
+ Magento\Config\Model\Config\Source\Yesno
+ shipping-skip-hide
+
+
+ Displayed Error Message
+
+
+
+
+
diff --git a/etc/config.xml b/etc/config.xml
new file mode 100644
index 0000000..81378b6
--- /dev/null
+++ b/etc/config.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+ 0
+ Qliro\QliroOne\Model\Method\QliroOne
+ QliroOne Checkout Payment
+ 0
+ 0
+
+ 0
+ 100
+ pending
+ 0
+ 1
+ 1
+ 0
+ 0
+
+
+ sandbox
+
+
+ 0
+
+
+ #FFFFFF
+ #A1A1A1
+ #A5E0AD
+ #89E0A6
+ 3
+ 8
+
+
+ InvoiceFee
+
+
+ 0
+
+ 0
+ PHPSTORM
+
+
+
+ {"_1639062625474_474":{"tag":"skrymmande","func":"bulky","value":"bulky"},"_1639063062214_214":{"tag":"weight","func":"weight","value":"weight"},"_1639063072512_512":{"tag":"cartprice","func":"cartprice","value":"grand_total"}}
+
+
+
+ 0
+ 0
+ 1
+ 1
+ 0
+ 1
+ 1
+ 1
+ 1
+ 0
+ 1
+ 0
+ 1
+ 0
+ 0
+ 0
+
+
+
+
+ 2
+
+
+ 2
+
+
+ 1
+
+
+
+
+ 1
+ 0
+ Qliro\QliroOne\Model\Carrier\Unifaun
+ Qlrio One Unifaun Shipping
+ 999999999
+ Qlrio One Unifaun Shipping
+ This shipping method is only available if Nshift is active
+ F
+ 0
+
+
+
+
diff --git a/etc/di.xml b/etc/di.xml
new file mode 100644
index 0000000..bd09aa4
--- /dev/null
+++ b/etc/di.xml
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+ - Qliro\QliroOne\Console\PingCommand
+ - Qliro\QliroOne\Console\TestCommand
+ - Qliro\QliroOne\Console\GetOrderCommand
+ - Qliro\QliroOne\Console\UpdateOrderCommand
+ - Qliro\QliroOne\Console\LockCommand
+ - Qliro\QliroOne\Console\UnlockCommand
+
+
+
+
+
+
+ QliroOneAdapter
+ Qliro\QliroOne\Model\Logger\Formatter
+
+
+
+
+
+ QliroLogger
+
+
+
+
+
+ QliroOneAdapter
+
+
+
+
+
+ QliroOneAdapter
+
+
+
+
+
+
+ - Qliro\QliroOne\Model\Product\Type\Handler\DefaultHandler
+ - Qliro\QliroOne\Model\Product\Type\Handler\DefaultHandler
+ - Qliro\QliroOne\Model\Product\Type\Handler\ConfigurableHandler
+ - Qliro\QliroOne\Model\Product\Type\Handler\ConfigurableHandler
+
+
+
+
+
+
+
+
+ - Qliro\QliroOne\Model\QliroOrder\Builder\Handler\AppliedRulesHandler
+
+
+
+
+
+
+
+ - Qliro\QliroOne\Model\QliroOrder\Admin\Builder\Handler\AppliedRulesHandler
+ - Qliro\QliroOne\Model\QliroOrder\Admin\Builder\Handler\ShippingFeeHandler
+ - Qliro\QliroOne\Model\QliroOrder\Admin\Builder\Handler\InvoiceFeeHandler
+
+
+
+
+
+
+
+ - Qliro\QliroOne\Model\QliroOrder\Admin\Builder\Handler\AppliedRulesHandler
+ - Qliro\QliroOne\Model\QliroOrder\Admin\Builder\Handler\ShippingFeeHandler
+ - Qliro\QliroOne\Model\QliroOrder\Admin\Builder\Handler\InvoiceFeeHandler
+
+
+
+
+
+
+ QliroLogger
+
+
+
+
+
+ database_logger
+
+
+ - Qliro\QliroOne\Model\Logger\Handler
+
+
+
+ - Monolog\Processor\PsrLogMessageProcessor
+
+
+
+
+
+
+ Qliro\QliroOne\Model\Method\QliroOne::PAYMENT_METHOD_CHECKOUT_CODE
+ QliroOneValueHandlerPool
+ QliroOneValidatorPool
+ QliroOneCommandPool
+ Qliro\QliroOne\Model\Method\QliroOne::PAYMENT_METHOD_FORM_BLOCK_TYPE
+ Qliro\QliroOne\Model\Method\QliroOne::PAYMENT_METHOD_INFO_BLOCK_TYPE
+
+
+
+
+
+
+ - QliroOneConfigValueHandler
+
+
+
+
+
+
+
+ - Qliro\QliroOne\Model\Method\QliroOne\Capture
+ - Qliro\QliroOne\Model\Method\QliroOne\Cancel
+ - Qliro\QliroOne\Model\Method\QliroOne\Cancel
+
+
+
+
+
+
+
+ - Qliro\QliroOne\Model\OrderManagementStatus\Update\Handler\Shipment
+ - Qliro\QliroOne\Model\OrderManagementStatus\Update\Handler\Payment
+ - Qliro\QliroOne\Model\OrderManagementStatus\Update\Handler\Cancel
+
+
+
+
+
+
+
+ QliroOneConfig
+
+
+
+
+
+ Qliro\QliroOne\Model\Method\QliroOne::PAYMENT_METHOD_CHECKOUT_CODE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etc/events.xml b/etc/events.xml
new file mode 100644
index 0000000..c1acd84
--- /dev/null
+++ b/etc/events.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etc/frontend/di.xml b/etc/frontend/di.xml
new file mode 100644
index 0000000..53645a6
--- /dev/null
+++ b/etc/frontend/di.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ - Qliro\QliroOne\Model\CheckoutConfigProvider
+
+
+
+
+
+
+
+
diff --git a/etc/frontend/events.xml b/etc/frontend/events.xml
new file mode 100644
index 0000000..4679d47
--- /dev/null
+++ b/etc/frontend/events.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etc/frontend/routes.xml b/etc/frontend/routes.xml
new file mode 100644
index 0000000..ccea1dd
--- /dev/null
+++ b/etc/frontend/routes.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
diff --git a/etc/module.xml b/etc/module.xml
new file mode 100644
index 0000000..04fb56d
--- /dev/null
+++ b/etc/module.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/etc/payment.xml b/etc/payment.xml
new file mode 100644
index 0000000..a78e67f
--- /dev/null
+++ b/etc/payment.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ 1
+
+
+
diff --git a/etc/pdf.xml b/etc/pdf.xml
new file mode 100644
index 0000000..d67b803
--- /dev/null
+++ b/etc/pdf.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Payment Fee
+ fee
+ 7
+ false
+ 500
+
+
+
\ No newline at end of file
diff --git a/etc/sales.xml b/etc/sales.xml
new file mode 100644
index 0000000..13e08ec
--- /dev/null
+++ b/etc/sales.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/i18n/en_US.csv b/i18n/en_US.csv
new file mode 100644
index 0000000..55036c5
--- /dev/null
+++ b/i18n/en_US.csv
@@ -0,0 +1,11 @@
+"Add Fee","Add Fee"
+"Alt. Title","Alt. Title"
+"Calculate Handling Fee","Calculate Handling Fee"
+"Enable Payment Method Fee Functionality","Enable Payment Method Fee Functionality"
+"Fee","Fee"
+"Payment Fee","Payment Fee"
+"Payment Method","Payment Method"
+"Payment Method Fee","Payment Method Fee"
+"Title","Title"
+"Your order is being processed","Your order is being processed"
+"Order has successfully been placed. Order ID: #%1", "Order has successfully been placed. Order ID: #%1"
\ No newline at end of file
diff --git a/registration.php b/registration.php
new file mode 100644
index 0000000..c265c20
--- /dev/null
+++ b/registration.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml b/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml
new file mode 100644
index 0000000..4e0cd42
--- /dev/null
+++ b/view/adminhtml/layout/sales_order_creditmemo_updateqty.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/layout/sales_order_creditmemo_view.xml b/view/adminhtml/layout/sales_order_creditmemo_view.xml
new file mode 100644
index 0000000..12ba3c6
--- /dev/null
+++ b/view/adminhtml/layout/sales_order_creditmemo_view.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/layout/sales_order_invoice_new.xml b/view/adminhtml/layout/sales_order_invoice_new.xml
new file mode 100644
index 0000000..56c502b
--- /dev/null
+++ b/view/adminhtml/layout/sales_order_invoice_new.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/layout/sales_order_invoice_updateqty.xml b/view/adminhtml/layout/sales_order_invoice_updateqty.xml
new file mode 100644
index 0000000..56c502b
--- /dev/null
+++ b/view/adminhtml/layout/sales_order_invoice_updateqty.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/layout/sales_order_invoice_view.xml b/view/adminhtml/layout/sales_order_invoice_view.xml
new file mode 100644
index 0000000..56c502b
--- /dev/null
+++ b/view/adminhtml/layout/sales_order_invoice_view.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/layout/sales_order_view.xml b/view/adminhtml/layout/sales_order_view.xml
new file mode 100644
index 0000000..adc7db5
--- /dev/null
+++ b/view/adminhtml/layout/sales_order_view.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/adminhtml/templates/form/qliroone.phtml b/view/adminhtml/templates/form/qliroone.phtml
new file mode 100644
index 0000000..060d989
--- /dev/null
+++ b/view/adminhtml/templates/form/qliroone.phtml
@@ -0,0 +1,21 @@
+
+
+
+
escapeHtml(__('Identification Number')) ?>
+
+ " class="required-entry admin__control-text"
+ value="getInfoData('id_number') ?>"/>
+
+
+
diff --git a/view/adminhtml/templates/info/pdf/qliroone.phtml b/view/adminhtml/templates/info/pdf/qliroone.phtml
new file mode 100644
index 0000000..7051e8d
--- /dev/null
+++ b/view/adminhtml/templates/info/pdf/qliroone.phtml
@@ -0,0 +1,11 @@
+
+escapeHtml(__('Identification Number: %1', 123)) ?>
+ {{pdf_row_separator}}
diff --git a/view/adminhtml/templates/info/qliroone.phtml b/view/adminhtml/templates/info/qliroone.phtml
new file mode 100644
index 0000000..aaf2dd6
--- /dev/null
+++ b/view/adminhtml/templates/info/qliroone.phtml
@@ -0,0 +1,28 @@
+
+escapeHtml($block->getMethod()->getTitle()); ?>
+ showWarning()): ?>
+ getWarningText(); ?>
+
+
+
+
+ escapeHtml(__('Qliro Order Id')); ?>:
+ getQliroOrderId(); ?>
+
+
+ escapeHtml(__('Qliro Reference')); ?>:
+ getQliroReference(); ?>
+
+
+ escapeHtml(__('Payment Method')); ?>:
+ getQliroMethod(); ?>
+
+
diff --git a/view/frontend/layout/checkout_cart_index.xml b/view/frontend/layout/checkout_cart_index.xml
new file mode 100644
index 0000000..93c4aad
--- /dev/null
+++ b/view/frontend/layout/checkout_cart_index.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+ -
+
-
+
-
+
-
+
-
+
-
+
- Qliro_QliroOne/js/view/cart/summary/fee
+ -
+
- Payment Fee
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/frontend/layout/checkout_index_index.xml b/view/frontend/layout/checkout_index_index.xml
new file mode 100644
index 0000000..c36b264
--- /dev/null
+++ b/view/frontend/layout/checkout_index_index.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+ -
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- Qliro_QliroOne/js/view/payment/qliroone-payments
+ -
+
-
+
- true
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
-
+
-
+
-
+
-
+
-
+
-
+
- Qliro_QliroOne/js/view/checkout/cart/totals/fee
+ - 20
+ -
+
- Qliro_QliroOne/checkout/cart/totals/fee
+ - Payment Fee
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/checkout_qliro_index.xml b/view/frontend/layout/checkout_qliro_index.xml
new file mode 100644
index 0000000..7fbde53
--- /dev/null
+++ b/view/frontend/layout/checkout_qliro_index.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- true
+
+
+ -
+
-
+
- true
+
+
+ -
+
-
+
- true
+
+ -
+
-
+
- true
+
+
+ -
+
-
+
- true
+
+
+ -
+
-
+
- true
+
+
+ -
+
-
+
- true
+
+
+
+ -
+
-
+
- true
+
+
+
+
+
+
+
+ -
+
- uiComponent
+ -
+
-
+
-
+
- true
+
+
+ -
+
- Magento_SalesRule/js/view/payment/discount
+ - 0
+ -
+
-
+
- 0
+ - Magento_SalesRule/js/view/payment/discount-messages
+ - messages
+
+
+
+
+
+ -
+
- Qliro_QliroOne/js/view/qliroone-view
+ - 10
+ - qlirooneCustomCheckout
+
+
+
+
+
+ -
+
-
+
-
+
-
+
- true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/checkout_qliro_pending.xml b/view/frontend/layout/checkout_qliro_pending.xml
new file mode 100644
index 0000000..02a8a6f
--- /dev/null
+++ b/view/frontend/layout/checkout_qliro_pending.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/checkout_qliro_success.xml b/view/frontend/layout/checkout_qliro_success.xml
new file mode 100644
index 0000000..c5b4f75
--- /dev/null
+++ b/view/frontend/layout/checkout_qliro_success.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_email_order_creditmemo_items.xml b/view/frontend/layout/sales_email_order_creditmemo_items.xml
new file mode 100644
index 0000000..ab0b86d
--- /dev/null
+++ b/view/frontend/layout/sales_email_order_creditmemo_items.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_email_order_invoice_items.xml b/view/frontend/layout/sales_email_order_invoice_items.xml
new file mode 100644
index 0000000..2dad8e0
--- /dev/null
+++ b/view/frontend/layout/sales_email_order_invoice_items.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_email_order_items.xml b/view/frontend/layout/sales_email_order_items.xml
new file mode 100644
index 0000000..bd79d9c
--- /dev/null
+++ b/view/frontend/layout/sales_email_order_items.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_guest_invoice.xml b/view/frontend/layout/sales_guest_invoice.xml
new file mode 100644
index 0000000..dc2c910
--- /dev/null
+++ b/view/frontend/layout/sales_guest_invoice.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_guest_print.xml b/view/frontend/layout/sales_guest_print.xml
new file mode 100644
index 0000000..8d89bb9
--- /dev/null
+++ b/view/frontend/layout/sales_guest_print.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_guest_printinvoice.xml b/view/frontend/layout/sales_guest_printinvoice.xml
new file mode 100644
index 0000000..23ff298
--- /dev/null
+++ b/view/frontend/layout/sales_guest_printinvoice.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_guest_view.xml b/view/frontend/layout/sales_guest_view.xml
new file mode 100644
index 0000000..c317779
--- /dev/null
+++ b/view/frontend/layout/sales_guest_view.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/frontend/layout/sales_order_creditmemo.xml b/view/frontend/layout/sales_order_creditmemo.xml
new file mode 100644
index 0000000..3e0a371
--- /dev/null
+++ b/view/frontend/layout/sales_order_creditmemo.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_order_invoice.xml b/view/frontend/layout/sales_order_invoice.xml
new file mode 100644
index 0000000..23ff298
--- /dev/null
+++ b/view/frontend/layout/sales_order_invoice.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/layout/sales_order_print.xml b/view/frontend/layout/sales_order_print.xml
new file mode 100644
index 0000000..71ed030
--- /dev/null
+++ b/view/frontend/layout/sales_order_print.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/frontend/layout/sales_order_view.xml b/view/frontend/layout/sales_order_view.xml
new file mode 100644
index 0000000..71ed030
--- /dev/null
+++ b/view/frontend/layout/sales_order_view.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/frontend/page_layout/qliroone-checkout.xml b/view/frontend/page_layout/qliroone-checkout.xml
new file mode 100644
index 0000000..a89bd00
--- /dev/null
+++ b/view/frontend/page_layout/qliroone-checkout.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/requirejs-config.js b/view/frontend/requirejs-config.js
new file mode 100644
index 0000000..d4b8c60
--- /dev/null
+++ b/view/frontend/requirejs-config.js
@@ -0,0 +1,14 @@
+var config = {
+ map: {
+ '*': {
+ pollPending: 'Qliro_QliroOne/js/poll-pending'
+ }
+ },
+ config: {
+ mixins: {
+ "Magento_Checkout/js/view/shipping": {
+ "Qliro_QliroOne/js/mixins/shipping": true
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/view/frontend/templates/checkout/pending.phtml b/view/frontend/templates/checkout/pending.phtml
new file mode 100644
index 0000000..85b75e2
--- /dev/null
+++ b/view/frontend/templates/checkout/pending.phtml
@@ -0,0 +1,32 @@
+
+
+
+
",
+ "texts": {
+ "loaderText": "= __('Your order is being processed') ?>",
+ "imgAlt": ""
+ }
+ }
+ }'>
+
+
+
+
diff --git a/view/frontend/templates/checkout/success.phtml b/view/frontend/templates/checkout/success.phtml
new file mode 100644
index 0000000..f133080
--- /dev/null
+++ b/view/frontend/templates/checkout/success.phtml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+ = $block->getHtmlSnippet() ?>
+
+
+
+
diff --git a/view/frontend/templates/info/qliroone.phtml b/view/frontend/templates/info/qliroone.phtml
new file mode 100644
index 0000000..9b2bbba
--- /dev/null
+++ b/view/frontend/templates/info/qliroone.phtml
@@ -0,0 +1,29 @@
+
+
+escapeHtml($block->getMethod()->getTitle()); ?>
+ showWarning()): ?>
+ getWarningText(); ?>
+
+
+
+
+ escapeHtml(__('Qliro Order Id')); ?>:
+ getQliroOrderId(); ?>
+
+
+ escapeHtml(__('Qliro Reference')); ?>:
+ getQliroReference(); ?>
+
+
+ escapeHtml(__('Payment Method')); ?>:
+ getQliroMethod(); ?>
+
+
diff --git a/view/frontend/web/css/qliro.css b/view/frontend/web/css/qliro.css
new file mode 100644
index 0000000..c394484
--- /dev/null
+++ b/view/frontend/web/css/qliro.css
@@ -0,0 +1,95 @@
+.page-layout-qliroone-checkout .opc-payment-additional {
+ width: calc(100% - 48px);
+ max-width: 452px;
+ margin: 0 auto 32px;
+ background: #f8f8f8;
+ padding: 24px 24px 12px;
+}
+
+.page-layout-qliroone-checkout .actions-toolbar {
+ margin: 12px 0;
+}
+
+.page-layout-qliroone-checkout .payment-option-title {
+ margin-bottom: 12px;
+ cursor: pointer;
+}
+
+.page-layout-qliroone-checkout .action-toggle:after {
+ position: relative;
+ display: inline-block;
+ content: '>';
+ font-size: 16px;
+ line-height: 23px;
+ vertical-align: middle;
+ margin-left: 4px;
+}
+
+.page-layout-qliroone-checkout ._active .action-toggle:after {
+ transform: rotate(90deg);
+}
+
+.checkout-qliro-success .minicart-wrapper {
+ display: none; /* Hide minicart since quote might note be cleared on success */
+}
+
+.qliroone-checkout-processed {
+ text-align: center;
+}
+
+.qliroone-checkout-processed .loading-mask {
+ position: relative;
+}
+
+.qliroone-checkout-processed .loading-mask .loader > img {
+ position: relative;
+ margin-bottom: 16px;
+}
+
+.qliroone-checkout-processed .loading-mask .loader > p {
+ font-size: 2.2rem;
+ display: block;
+}
+
+.qliroone-checkout-success {
+ text-align: center;
+ font-size: 1.6rem;
+}
+
+@media only screen and (min-width: 780px) {
+ .page-layout-qliroone-checkout .checkout-container {
+ display: -webkit-box;
+ display: -ms-flexbox;
+ display: flex;
+
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ }
+
+ .page-layout-qliroone-checkout .checkout-container .authentication-wrapper {
+ width: 100%;
+ max-width: 100%;
+ }
+
+ .page-layout-qliroone-checkout .checkout-container .opc-sidebar {
+ width: 100%;
+ float: none;
+ -ms-flex-order: 1;
+ -webkit-order: 1;
+ order: 1;
+ }
+
+ .page-layout-qliroone-checkout .checkout-container .opc-wrapper {
+ width: 100%;
+ float: none;
+ -ms-flex-order: 2;
+ -webkit-order: 2;
+ order: 2;
+ }
+
+ .page-layout-qliroone-checkout .checkout-container .opc-payment-additional .actions-toolbar {
+ margin-left: 0;
+ }
+}
diff --git a/view/frontend/web/js/mixins/shipping.js b/view/frontend/web/js/mixins/shipping.js
new file mode 100644
index 0000000..320bbff
--- /dev/null
+++ b/view/frontend/web/js/mixins/shipping.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ *
+ * Remove template rendering for this component
+ */
+
+define([], function () {
+ 'use strict';
+
+ return function (shippingFunction) {
+ var result = {};
+ if (window.checkoutConfig.qliro.enabled) {
+ result = {defaults: {template: ''}};
+ }
+ return shippingFunction.extend(result);
+ }
+});
+
diff --git a/view/frontend/web/js/model/config.js b/view/frontend/web/js/model/config.js
new file mode 100644
index 0000000..e11df7f
--- /dev/null
+++ b/view/frontend/web/js/model/config.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+define([], function () {
+ 'use strict';
+
+ return window.checkoutConfig && window.checkoutConfig.qliro || {};
+});
diff --git a/view/frontend/web/js/model/qliro.js b/view/frontend/web/js/model/qliro.js
new file mode 100644
index 0000000..2ce2db8
--- /dev/null
+++ b/view/frontend/web/js/model/qliro.js
@@ -0,0 +1,194 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+// @codingStandardsIgnoreFile
+// phpcs:ignoreFile
+
+define([
+ 'jquery',
+ 'Qliro_QliroOne/js/model/config',
+ 'Magento_Checkout/js/model/quote',
+ 'Magento_Checkout/js/model/cart/totals-processor/default',
+ 'Magento_Checkout/js/model/cart/cache',
+ 'Magento_Customer/js/customer-data',
+ 'mage/translate'
+], function(
+ $,
+ config,
+ quote,
+ totalsProcessor,
+ cartCache,
+ customerData,
+ __
+) {
+ function sendUpdateQuote() {
+ return (
+ $.ajax({
+ url: config.updateQuoteUrl + '?quote_id=' + quote.getQuoteId() + '&token=' + config.securityToken,
+ method: 'POST'
+ })
+ )
+ }
+
+ function sendAjaxAsJson(url, data) {
+ qliroDebug('Calling sendAjaxAsJson', data);
+ return $.ajax({
+ url: url + '?token=' + config.securityToken,
+ method: 'POST',
+ data: JSON.stringify(data),
+ processData: false,
+ contentType: 'application/json'
+ });
+ }
+
+ function showErrorMessage(message) {
+ qliroDebug('Calling showErrorMessage', message);
+ customerData.set('messages', {
+ messages: [{
+ type: 'error',
+ text: message
+ }]
+ });
+ }
+
+ function qliroDebug(caption, data) {
+ if (config.isDebug) {
+ console.log(caption, data);
+ }
+ }
+
+ function qliroSuccessDebug(caption, data) {
+ qliroDebug('Success: ' + caption, data);
+ }
+
+ function updateTotals() {
+ cartCache.set('totals', null);
+ totalsProcessor.estimateTotals(quote.shippingAddress());
+ }
+
+ return {
+ updateCart: function() {
+ if (!config.isEagerCheckoutRefresh) {
+ window.q1.lock();
+ } else {
+ qliroDebug('Skipping checkout lock.');
+ }
+
+ sendUpdateQuote()
+ .then(
+ function(data) {
+ var unmatchCount = 0;
+
+ window.q1.onOrderUpdated(function(order) {
+ if (config.isEagerCheckoutRefresh) {
+ qliroDebug('Skipping checkout update polling.');
+
+ return true;
+ }
+
+ if (Math.abs(order.totalPrice - data.order.totalPrice) < 0.005) {
+ unmatchCount = 0;
+ window.q1.unlock();
+ } else {
+ unmatchCount++;
+
+ if (unmatchCount > 3) {
+ unmatchCount = 0;
+ showErrorMessage(__('Store and Qliro One totals don\'t match. Refresh the page.'));
+ }
+ }
+ })
+ },
+ function(response, state, reason) {
+ var data = response.responseJSON || {};
+
+ if (!config.isEagerCheckoutRefresh) {
+ window.q1.unlock();
+ } else {
+ qliroDebug('Skipping checkout unlock.');
+ }
+
+ showErrorMessage(data.error || reason);
+ }
+ );
+ },
+
+ onCheckoutLoaded: function() {
+ qliroSuccessDebug('onCheckoutLoaded', q1);
+ },
+
+ onCustomerInfoChanged: function(customer) {
+ sendAjaxAsJson(config.updateCustomerUrl, customer).then(
+ function(data) {
+ qliroSuccessDebug('onCustomerInfoChanged', data);
+ },
+ function(response) {
+ var data = response.responseJSON || {};
+ var error = data.error || __('Something went wrong while updating customer.');
+ showErrorMessage(error);
+ }
+ );
+ },
+
+ onPaymentDeclined: function(declineReason) {
+ $(".opc-block-summary").show();
+ $(".discount-code").show();
+ qliroSuccessDebug('onPaymentDeclined', declineReason);
+ },
+
+ onPaymentMethodChanged: function(paymentMethod) {
+ sendAjaxAsJson(config.updatePaymentMethodUrl, paymentMethod).then(
+ function(data) {
+ qliroSuccessDebug('onPaymentMethodChanged', data);
+ updateTotals();
+ },
+ function(response) {
+ var data = response.responseJSON || {};
+ var error = data.error || __('Something went wrong while updating payment method.');
+ showErrorMessage(error);
+ }
+ );
+ },
+
+ onPaymentProcess: function() {
+ $(".opc-block-summary").hide();
+ $(".discount-code").hide();
+ qliroSuccessDebug('onPaymentProcess', q1);
+ },
+
+ onSessionExpired: function() {
+ qliroSuccessDebug('onSessionExpired', q1);
+ },
+
+ onShippingMethodChanged: function(shipping) {
+ sendAjaxAsJson(config.updateShippingMethodUrl, shipping).then(
+ function(data) {
+ qliroSuccessDebug('onShippingMethodChanged', data);
+ updateTotals();
+ },
+ function(response) {
+ var data = response.responseJSON || {};
+ var error = data.error || __('Something went wrong while updating shipping method.');
+ showErrorMessage(error);
+ }
+ );
+ },
+
+ onShippingPriceChanged: function(newShippingPrice) {
+ sendAjaxAsJson(config.updateShippingPriceUrl, {newShippingPrice: newShippingPrice}).then(
+ function(data) {
+ qliroSuccessDebug('onShippingPriceChanged', data);
+ updateTotals();
+ },
+ function(response) {
+ var data = response.responseJSON || {};
+ var error = data.error || __('Something went wrong while updating shipping method options.');
+ showErrorMessage(error);
+ }
+ );
+ }
+ }
+});
+
diff --git a/view/frontend/web/js/poll-pending.js b/view/frontend/web/js/poll-pending.js
new file mode 100644
index 0000000..2e6ae57
--- /dev/null
+++ b/view/frontend/web/js/poll-pending.js
@@ -0,0 +1,112 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+// @codingStandardsIgnoreFile
+// phpcs:ignoreFile
+
+define([
+ 'jquery',
+ 'mage/url',
+ 'mage/translate',
+ 'Magento_Ui/js/model/messageList',
+ 'mage/cookies'
+], function ($, url, __, messageList) {
+ 'use strict';
+
+ $.widget('qliro.pollPending', {
+ waitingResponse: false,
+ counter: 0,
+ response: 'PENDING',
+ htmlSnippet: null,
+ errorMessage: null,
+ orderId: null,
+
+ _create: function () {
+ this.debug('Initialize polling...');
+
+ $('.qliroone-checkout-processed').trigger('processStart');
+
+ this.poll().then(function() {
+ $('.qliroone-checkout-processed').hide().trigger('processStop');
+ }.bind(this));
+ },
+
+ makeAjaxRequest: function() {
+ if (!this.waitingResponse) {
+ this.waitingResponse = true;
+
+ this.debug('Polling', this.options.pollPendingUrl);
+
+ $.ajax({
+ url: this.options.pollPendingUrl,
+ method: 'POST'
+ }).then(
+ function (data) {
+ this.debug('Polled data', data);
+
+ this.response = data.status || 'PENDING';
+
+ if (data.status === 'OK') {
+ this.orderId = data.orderIncrementId;
+ this.htmlSnippet = data.htmlSnippet;
+ }
+
+ this.waitingResponse = false;
+ }.bind(this),
+ function (response) {
+ var data = response.responseJSON || {};
+
+ this.debug('Poll has failed', data);
+
+ this.response = data.status || 'FAILED';
+ this.errorMessage = data.error;
+
+ this.waitingResponse = false;
+ }.bind(this)
+ );
+ }
+ },
+
+ poll: function() {
+ return new Promise(function(resolve) {
+ var polling = setInterval(function() {
+ switch(this.response) {
+ case 'OK':
+ this.debug('[DONE]');
+ clearInterval(polling);
+ resolve('DONE');
+ location = url.build('checkout/qliro/success');
+ break;
+ case 'FAILED':
+ this.debug('[FAILED]');
+ clearInterval(polling);
+ this.queueErrorMessage();
+ $.mage.cookies.clear('QOMR');
+ location = url.build('checkout/cart');
+ break;
+ case 'PENDING':
+ default:
+ this.debug('... still pending ...');
+ this.makeAjaxRequest();
+ }
+ }.bind(this), 1000)
+ }.bind(this))
+ },
+
+ debug: function(caption, data) {
+ if (this.options.isDebug) {
+ if (data) {
+ console.log(caption, data);
+ } else {
+ console.log(caption);
+ }
+ }
+ },
+
+ queueErrorMessage: function() {
+ messageList.addErrorMessage({ message: this.errorMessage });
+ }
+ });
+});
diff --git a/view/frontend/web/js/view/cart/summary/fee.js b/view/frontend/web/js/view/cart/summary/fee.js
new file mode 100644
index 0000000..dfe9823
--- /dev/null
+++ b/view/frontend/web/js/view/cart/summary/fee.js
@@ -0,0 +1,59 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+define(
+ [
+ 'Magento_Checkout/js/view/summary/abstract-total',
+ 'Magento_Checkout/js/model/quote',
+ 'Magento_Catalog/js/price-utils',
+ 'Magento_Checkout/js/model/totals'
+ ],
+ function (Component, quote, priceUtils, totals) {
+ "use strict";
+ return Component.extend({
+ defaults: {
+ isFullTaxSummaryDisplayed: window.checkoutConfig.isFullTaxSummaryDisplayed || false,
+ template: 'Qliro_QliroOne/cart/summary/fee'
+ },
+ totals: quote.getTotals(),
+ isTaxDisplayedInGrandTotal: window.checkoutConfig.includeTaxInGrandTotal || false,
+ isDisplayed: function() {
+ return this.isFullMode();
+ },
+ getValue: function() {
+ var price = null;
+ if (this.totals()) {
+ price = this.getTotalsFeeValue();
+ }
+ return (price || this.getTitle()) ? this.getFormattedPrice(price) : null;
+ },
+ getTotalsFeeValue: function() {
+ var totalsPaymentFeeSegment = totals.getSegment('qliroone_fee');
+ return totalsPaymentFeeSegment ? totalsPaymentFeeSegment.value : null;
+ },
+ getBaseValue: function() {
+ var price = null;
+ if (this.totals()) {
+ price = this.totals().base_payment_charge;
+ }
+ return (price || this.getTitle()) ? priceUtils.formatPrice(price, quote.getBasePriceFormat()) : null;
+ },
+ getTitle: function() {
+ if (this.totals()) {
+ var totalSegments = this.totals().total_segments;
+ if (totalSegments) {
+ for (var i = 0; i < totalSegments.length; i++) {
+ if (totalSegments[i].code == 'qliroone_fee' && totalSegments[i].title) {
+ return totalSegments[i].title;
+ }
+ }
+ }
+ }
+ var feeSection = window.checkoutConfig.qliro.qliroone_fee;
+ return feeSection.fee_setup ? feeSection.fee_setup.description : null;
+ },
+ });
+ }
+);
\ No newline at end of file
diff --git a/view/frontend/web/js/view/cart/totals/fee.js b/view/frontend/web/js/view/cart/totals/fee.js
new file mode 100644
index 0000000..fa92456
--- /dev/null
+++ b/view/frontend/web/js/view/cart/totals/fee.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+define(
+ [
+ 'Qliro_QliroOne/js/view/cart/summary/fee'
+ ],
+ function (Component) {
+ 'use strict';
+
+ return Component.extend({
+
+ /**
+ * @override
+ *
+ * @returns {boolean}
+ */
+ isDisplayed: function () {
+ return true;
+ }
+ });
+ }
+);
diff --git a/view/frontend/web/js/view/checkout/cart/totals/fee.js b/view/frontend/web/js/view/checkout/cart/totals/fee.js
new file mode 100644
index 0000000..162cda4
--- /dev/null
+++ b/view/frontend/web/js/view/checkout/cart/totals/fee.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+define(
+ [
+ 'Qliro_QliroOne/js/view/checkout/summary/fee',
+ 'Magento_Checkout/js/model/quote',
+ 'Magento_Catalog/js/price-utils',
+ 'ko'
+ ],
+ function (Component, quote, priceUtils, ko) {
+ 'use strict';
+
+ return Component.extend({
+ showPaymentFee: ko.observable(false),
+
+ getValue: function() {
+ var price = null,
+ paymentFee;
+
+ if (this.totals()) {
+ price = this.getTotalsFeeValue();
+ }
+
+ paymentFee = (price || this.getTitle()) ? priceUtils.formatPrice(price, quote.getBasePriceFormat()) : null;
+
+ if (paymentFee) {
+ this.showPaymentFee(true);
+ } else {
+ this.showPaymentFee(false);
+ }
+
+ return (price || this.getTitle()) ? this.getFormattedPrice(price) : null;
+ }
+ });
+ }
+);
diff --git a/view/frontend/web/js/view/checkout/summary/fee.js b/view/frontend/web/js/view/checkout/summary/fee.js
new file mode 100644
index 0000000..29281ba
--- /dev/null
+++ b/view/frontend/web/js/view/checkout/summary/fee.js
@@ -0,0 +1,59 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+define(
+ [
+ 'Magento_Checkout/js/view/summary/abstract-total',
+ 'Magento_Checkout/js/model/quote',
+ 'Magento_Catalog/js/price-utils',
+ 'Magento_Checkout/js/model/totals'
+ ],
+ function (Component, quote, priceUtils, totals) {
+ "use strict";
+ return Component.extend({
+ defaults: {
+ isFullTaxSummaryDisplayed: window.checkoutConfig.isFullTaxSummaryDisplayed || false,
+ template: 'Qliro_QliroOne/checkout/summary/fee'
+ },
+ totals: quote.getTotals(),
+ isTaxDisplayedInGrandTotal: window.checkoutConfig.includeTaxInGrandTotal || false,
+ isDisplayed: function() {
+ return this.isFullMode();
+ },
+ getValue: function() {
+ var price = null;
+ if (this.totals()) {
+ price = this.getTotalsFeeValue();
+ }
+ return (price || this.getTitle()) ? this.getFormattedPrice(price) : null;
+ },
+ getTotalsFeeValue: function() {
+ var totalsPaymentFeeSegment = totals.getSegment('qliroone_fee');
+ return totalsPaymentFeeSegment ? totalsPaymentFeeSegment.value : null;
+ },
+ getBaseValue: function() {
+ var price = null;
+ if (this.totals()) {
+ price = this.totals().base_payment_charge;
+ }
+ return (price || this.getTitle()) ? priceUtils.formatPrice(price, quote.getBasePriceFormat()) : null;
+ },
+ getTitle: function() {
+ if (this.totals()) {
+ var totalSegments = this.totals().total_segments;
+ if (totalSegments) {
+ for (var i = 0; i < totalSegments.length; i++) {
+ if (totalSegments[i].code == 'qliroone_fee' && totalSegments[i].title) {
+ return totalSegments[i].title;
+ }
+ }
+ }
+ }
+ var feeSection = window.checkoutConfig.qliro.qliroone_fee;
+ return feeSection.fee_setup ? feeSection.fee_setup.default.description : null;
+ },
+ });
+ }
+);
\ No newline at end of file
diff --git a/view/frontend/web/js/view/payment/method-renderer/qliroone-method.js b/view/frontend/web/js/view/payment/method-renderer/qliroone-method.js
new file mode 100644
index 0000000..abdc925
--- /dev/null
+++ b/view/frontend/web/js/view/payment/method-renderer/qliroone-method.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+define([
+ 'Magento_Checkout/js/view/payment/default'
+ ],
+ function (Component) {
+ 'use strict';
+
+ return Component.extend({
+ defaults: {
+ template: 'Qliro_QliroOne/payment/qliroone'
+ }
+ });
+ }
+);
diff --git a/view/frontend/web/js/view/payment/qliroone-payments.js b/view/frontend/web/js/view/payment/qliroone-payments.js
new file mode 100644
index 0000000..43b54d5
--- /dev/null
+++ b/view/frontend/web/js/view/payment/qliroone-payments.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+define([
+ 'uiComponent',
+ 'Magento_Checkout/js/model/payment/renderer-list'
+ ],
+ function (Component, rendererList) {
+ 'use strict';
+
+ rendererList.push(
+ {
+ type: 'qliroone',
+ component: 'Qliro_QliroOne/js/view/payment/method-renderer/qliroone-method'
+ }
+ );
+
+ /** Add view logic here if needed */
+ return Component.extend({});
+ });
+
diff --git a/view/frontend/web/js/view/qliroone-view.js b/view/frontend/web/js/view/qliroone-view.js
new file mode 100644
index 0000000..2409e88
--- /dev/null
+++ b/view/frontend/web/js/view/qliroone-view.js
@@ -0,0 +1,97 @@
+/**
+ * Copyright © Qliro AB. All rights reserved.
+ * See LICENSE.txt for license details.
+ */
+
+// @codingStandardsIgnoreFile
+// phpcs:ignoreFile
+
+define([
+ 'ko',
+ 'uiComponent',
+ 'underscore',
+ 'Magento_Checkout/js/model/step-navigator',
+ 'Magento_Checkout/js/model/shipping-service',
+ 'Magento_Customer/js/customer-data',
+ 'Magento_Checkout/js/model/quote',
+ 'Qliro_QliroOne/js/model/qliro',
+ 'Qliro_QliroOne/js/model/config'
+], function (
+ ko,
+ Component,
+ _,
+ stepNavigator,
+ shippingService,
+ customerData,
+ quote,
+ qliro,
+ config
+) {
+ 'use strict';
+ return Component.extend({
+ defaults: {
+ template: 'Qliro_QliroOne/checkout/onepage',
+ imports: {
+ discountApplied: 'checkout.steps.billing-step.discount:isApplied'
+ },
+ },
+ discountApplied: ko.observable(false),
+
+ isVisible: ko.observable(true),
+
+ initialize: function () {
+ this._super();
+
+ this.initializeSubscribers();
+ this.initializeQliro();
+
+ // if (!quote.isVirtual()) {
+ // stepNavigator.registerStep('shipping', null, 'Shipping', false, _.bind(this.navigate, this), 0);
+ // }
+
+ stepNavigator.registerStep(
+ 'qliroone-step',
+ null,
+ config.checkoutTitle,
+ this.isVisible,
+ _.bind(this.navigate, this),
+ 100
+ );
+
+ return this;
+ },
+
+ initializeSubscribers: function() {
+ this.discountApplied.subscribe(function() {
+ qliro.updateCart();
+ });
+ },
+
+ initializeQliro: function() {
+ window.q1Ready = function(q1) {
+ console.log(q1); // Debugging
+ q1.onCheckoutLoaded(qliro.onCheckoutLoaded);
+ q1.onCustomerInfoChanged(qliro.onCustomerInfoChanged);
+ q1.onPaymentDeclined(qliro.onPaymentDeclined);
+ q1.onPaymentMethodChanged(qliro.onPaymentMethodChanged);
+ q1.onPaymentProcess(qliro.onPaymentProcess);
+ q1.onSessionExpired(qliro.onSessionExpired);
+ q1.onShippingMethodChanged(qliro.onShippingMethodChanged);
+ q1.onShippingPriceChanged(qliro.onShippingPriceChanged);
+ }
+ },
+
+ /**
+ * The navigate() method is responsible for navigation between checkout step
+ * during checkout. You can add custom logic, for example some conditions
+ * for switching to your custom step. (This method is required even though it
+ * is blank, don't delete)
+ */
+ navigate: function () {
+ },
+
+ navigateToNextStep: function () {
+ stepNavigator.next();
+ }
+ });
+});
diff --git a/view/frontend/web/template/cart/summary/fee.html b/view/frontend/web/template/cart/summary/fee.html
new file mode 100644
index 0000000..da17195
--- /dev/null
+++ b/view/frontend/web/template/cart/summary/fee.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/frontend/web/template/cart/totals/fee.html b/view/frontend/web/template/cart/totals/fee.html
new file mode 100644
index 0000000..5d215a5
--- /dev/null
+++ b/view/frontend/web/template/cart/totals/fee.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
diff --git a/view/frontend/web/template/checkout/cart/totals/fee.html b/view/frontend/web/template/checkout/cart/totals/fee.html
new file mode 100644
index 0000000..a92f445
--- /dev/null
+++ b/view/frontend/web/template/checkout/cart/totals/fee.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/view/frontend/web/template/checkout/onepage.html b/view/frontend/web/template/checkout/onepage.html
new file mode 100644
index 0000000..ea7c73f
--- /dev/null
+++ b/view/frontend/web/template/checkout/onepage.html
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/view/frontend/web/template/checkout/summary/fee.html b/view/frontend/web/template/checkout/summary/fee.html
new file mode 100644
index 0000000..da17195
--- /dev/null
+++ b/view/frontend/web/template/checkout/summary/fee.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/view/frontend/web/template/payment/qliroone.html b/view/frontend/web/template/payment/qliroone.html
new file mode 100644
index 0000000..0c67de9
--- /dev/null
+++ b/view/frontend/web/template/payment/qliroone.html
@@ -0,0 +1,39 @@
+
+