diff --git a/Controller/Api/Push.php b/Controller/Api/Push.php index 43c1c9f..7db16ad 100644 --- a/Controller/Api/Push.php +++ b/Controller/Api/Push.php @@ -1,10 +1,12 @@ logger = $logger; $this->checkoutOrder = $checkoutOrder; - $this->dataObjectFactory = $dataObjectFactory; $this->result = $result; $this->apiLogger = $apiLogger; $this->container = $container; @@ -104,141 +104,118 @@ public function __construct( /** * Performing the push action logic * - * @return Json|ResultInterface - * @throws KlarnaException - * @throws LocalizedException - * phpcs:disable Commenting.EmptyCatchComment + * @inheritDoc */ public function execute() { $klarnaOrderId = $this->request->getParam('id'); $this->workflowProvider->setKlarnaOrderId($klarnaOrderId); - - try { - $magentoOrder = $this->workflowProvider->getMagentoOrder(); - // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch - } catch (KlarnaException $e) { - $this->logger->debug( - 'No order is created because a payment method is selected with an external redirect.' - ); - // We do nothing since when using for example the IDEAL payment no order is created at this point - } - $this->logger->debug('Push: klarna order id: ' . $klarnaOrderId); - try { - $this->checkoutOrder->updateOrderState($klarnaOrderId); - } catch (KlarnaException $e) { - return $this->createOrder($klarnaOrderId); - } catch (LocalizedException $e) { - $this->apiLogger->logCallbackException( - $this->container, - ApiInterface::ACTIONS['push'], - $this->request, - $e - ); - return $this->cancelKlarnaOrder($klarnaOrderId, $e); + $createOrderStatus = $this->canCreateOrder() ? $this->createOrder($klarnaOrderId) : true; + if ($createOrderStatus instanceof Json) { + return $createOrderStatus; } - $magentoOrder = $this->checkoutOrder->getMagentoOrder(); - $this->container->setIncrementId($magentoOrder->getIncrementId()); - $this->container->setService(ServiceInterface::SERVICE_KCO); - $this->apiLogger->logCallback($this->container, ApiInterface::ACTIONS['push'], $this->request, []); - - return $this->getSuccessResponse(); - } - - /** - * Canceling the Klarna order - * - * @param string $klarnaOrderId - * @param LocalizedException $e - * @return Json - * @throws KlarnaException - */ - private function cancelKlarnaOrder(string $klarnaOrderId, LocalizedException $e): Json - { - $this->logger->critical('Push: Cancelling order. Error occured: ' . $e->getMessage()); - $responseCodeObject = $this->getFailureResponseObject(500); - $this->checkoutOrder->cancelKlarnaOrder($klarnaOrderId, $e->getMessage()); - - return $this->result->getJsonResult( - (int)$responseCodeObject->getResponseCode(), - ['error' => $e->getMessage()] - ); + return $this->updateOrderState($klarnaOrderId); } /** - * Getting back the success response - * - * @return Json + * @return bool */ - private function getSuccessResponse(): Json + private function canCreateOrder(): bool { - $this->logger->debug('Push: success'); - return $this->result->getJsonResult(200); - } + // TODO: We shouldn't need to rely on exception + it can result in false positives, let's eventually add + // possibility to figure out existence of these instances by something more simplified - /** - * Getting back the failure response object with the given response code - * - * @param int $responseCode - * @return DataObject - */ - private function getFailureResponseObject(int $responseCode): DataObject - { - $object = $this->dataObjectFactory->create(); - $object->setResponseCode($responseCode); + try { + $this->workflowProvider->getMagentoOrder(); + $this->workflowProvider->getKlarnaOrder(); - return $object; + return false; + } catch (KlarnaException $exception) { + return true; + } } /** * @param string $klarnaOrderId - * @param CartLockedException $e * - * @return Json + * @return Json|true */ - private function getCartLockedResponse(string $klarnaOrderId, CartLockedException $e): Json + private function createOrder(string $klarnaOrderId) { - $this->logger->debug( - 'Push: Retry order ' . $klarnaOrderId . ' - Exception: ' . $e->getMessage() - ); - return $this->result->getJsonResult( - 503, - ['error' => $e->getMessage()] - ); + $this->logger->debug('Push: Attempting to create order by id ' . $klarnaOrderId); + + try { + $this->checkoutOrder->createMagentoOrder($klarnaOrderId); + $this->checkoutOrder->sendCustomerMail(); + } catch (AlreadyExistsException $exception) { + $this->logger->debug('Push: Order already exists for this Klarna order id: ' . $klarnaOrderId); + } catch (CartLockedException $exception) { + $this->logger->debug('Push: Retry order ' . $klarnaOrderId . ' - Exception: ' . $exception->getMessage()); + + return $this->result->getJsonResult( + 503, + ['error' => $exception->getMessage()] + ); + } catch (LocalizedException $e) { + if ($this->checkoutOrder->isMagentoOrderExists($klarnaOrderId)) { + $this->logger->debug('Push: Order already created by concurrent request: ' . $klarnaOrderId); + + return true; + } + + $this->logger->debug('Push: Order creation failed: ' . $e->getMessage()); + + $this->apiLogger->logCallbackException( + $this->container, + ApiInterface::ACTIONS['push'], + $this->request, + $e + ); + + return $this->result->getJsonResult( + 500, + ['error' => 'Failed to create order'] + ); + } + + $this->logger->debug('Push: Order created successfully by id ' . $klarnaOrderId); + + return true; } /** - * Create order in Magento if it doesn't currently exist. - * - * This is the case when the customer selected a payment gateway method (for example "iDeal"). - * * @param string $klarnaOrderId + * * @return Json - * @throws KlarnaException */ - private function createOrder(string $klarnaOrderId): Json + private function updateOrderState(string $klarnaOrderId): Json { try { - $this->checkoutOrder->createMagentoOrder($klarnaOrderId); - $this->checkoutOrder->sendCustomerMail(); $this->checkoutOrder->updateOrderState($klarnaOrderId); - } catch (AlreadyExistsException $e) { - $this->logger->debug('Push: Order already exists for this Klarna order id: ' . $klarnaOrderId); - } catch (CartLockedException $e) { - return $this->getCartLockedResponse($klarnaOrderId, $e); } catch (LocalizedException $e) { - // Before cancelling, check if a concurrent push already created the order successfully. - // If the Magento order exists, return success to avoid voiding a valid Klarna authorization. - $magentoOrder = $this->checkoutOrder->getMagentoOrder(); - if ($magentoOrder !== null && $magentoOrder->getId()) { - $this->logger->debug('Push: Order already created by concurrent request: ' . $klarnaOrderId); - return $this->getSuccessResponse(); - } - return $this->cancelKlarnaOrder($klarnaOrderId, $e); + $this->apiLogger->logCallbackException( + $this->container, + ApiInterface::ACTIONS['push'], + $this->request, + $e + ); + + return $this->result->getJsonResult( + 500, + ['error' => 'Failed to update order state'] + ); } - return $this->getSuccessResponse(); + + $magentoOrder = $this->checkoutOrder->getMagentoOrder(); + $this->container->setIncrementId($magentoOrder->getIncrementId()); + $this->container->setService(ServiceInterface::SERVICE_KCO); + $this->apiLogger->logCallback($this->container, ApiInterface::ACTIONS['push'], $this->request, []); + + $this->logger->debug('Push: success'); + + return $this->result->getJsonResult(200); } } diff --git a/Controller/Klarna/Confirmation.php b/Controller/Klarna/Confirmation.php index 1f8660d..dde10df 100644 --- a/Controller/Klarna/Confirmation.php +++ b/Controller/Klarna/Confirmation.php @@ -1,17 +1,18 @@ request->getParam('id'); - $this->logger->debug('Klarna order id: ' . $klarnaOrderId); + $this->logger->debug('Kustom order id: ' . $klarnaOrderId); if (!$klarnaOrderId) { return $this->getInvalidOrderIdResponse(); @@ -102,18 +104,20 @@ public function execute() $this->checkoutOrder->sendCustomerMail(); } catch (CartLockedException $e) { $this->logger->debug('Confirmation: push running concurrently: ' . $klarnaOrderId . ' - Exception: ' . $e->getMessage()); + return $this->getSuccessResponse(); } catch (AlreadyExistsException $e) { - return $this->getOrderAlreadyExistsResponse(); + $this->logger->debug('Confirmation: push running concurrently: ' . $klarnaOrderId . ' - Exception: ' . $e->getMessage()); + + return $this->getSuccessResponse(); } catch (LocalizedException $e) { - // Before cancelling, check if a concurrent push already created the order successfully. - // If the Magento order exists, return success to avoid voiding a valid Klarna authorization. - $magentoOrder = $this->checkoutOrder->getMagentoOrder(); - if ($magentoOrder !== null && $magentoOrder->getId()) { + if ($this->checkoutOrder->isMagentoOrderExists($klarnaOrderId)) { $this->logger->debug('Confirmation: Order already created by concurrent request: ' . $klarnaOrderId); + return $this->getSuccessResponse(); } - return $this->getErrorResponse($e, $klarnaOrderId); + + return $this->getErrorResponse($e); } return $this->getSuccessResponse(); @@ -127,6 +131,7 @@ public function execute() private function getSuccessResponse(): Redirect { $this->logger->debug('Confirmation: Success'); + return $this->redirectFactory->create()->setPath(Url::CHECKOUT_ACTION_PREFIX . '/success'); } @@ -134,13 +139,12 @@ private function getSuccessResponse(): Redirect * Returning a general error response * * @param KlarnaException|NoSuchEntityException|CouldNotSaveException|LocalizedException $e - * @param string $klarnaOrderId + * * @return Redirect */ - private function getErrorResponse($e, string $klarnaOrderId): Redirect + private function getErrorResponse($e): Redirect { $this->logger->critical($e); - $this->checkoutOrder->cancelKlarnaOrder($klarnaOrderId, $e->getMessage()); $this->messageManager->addErrorMessage($e->getMessage()); return $this->redirectFactory->create()->setUrl($this->url->getFailureUrl()); @@ -154,19 +158,7 @@ private function getErrorResponse($e, string $klarnaOrderId): Redirect private function getInvalidOrderIdResponse(): Redirect { $this->messageManager->addErrorMessage(__('Unable to process order. Please try again')); - return $this->redirectFactory->create()->setUrl($this->url->getFailureUrl()); - } - - /** - * Returning a response for the case the order already exists - * - * @return Redirect - */ - private function getOrderAlreadyExistsResponse(): Redirect - { - $this->logger->debug('Confirmation: Order already exist'); - $this->messageManager->addErrorMessage(__('Order already exist.')); return $this->redirectFactory->create()->setUrl($this->url->getFailureUrl()); } } diff --git a/Model/Order/Order.php b/Model/Order/Order.php index 30b262e..2d48306 100644 --- a/Model/Order/Order.php +++ b/Model/Order/Order.php @@ -253,7 +253,7 @@ public function createMagentoOrder(string $klarnaOrderId): MagentoOrderInterface $this->klarnaOrder = $this->workflowProvider->getKlarnaOrder(); $this->klarnaOrder->setReservationId($reservationId); $this->orderRepository->save($this->klarnaOrder); - $this->logger->debug('Saved the klarna order'); + $this->logger->debug('Saved the Kustom order'); return $this->mageOrder; } @@ -295,7 +295,7 @@ public function setOrderStatus(MagentoOrderInterface $order, string $status = '' } if (SalesOrder::STATE_PROCESSING === $order->getState()) { - $order->addStatusHistoryComment(__('Order processed by Klarna.'), $status); + $order->addStatusHistoryComment(__('Order processed by Kustom.'), $status); } } @@ -332,7 +332,7 @@ public function cancelKlarnaOrder(string $klarnaOrderId, string $cancelReason): } if ($order->getStatus() !== 'CANCELLED') { $orderManagement->cancel($klarnaId); - $this->logger->info('Canceled order with Klarna - ' . $cancelReason); + $this->logger->info('Canceled order with Kustom - ' . $cancelReason); } if ($magentoOrder !== null && !$magentoOrder->isCanceled()) { @@ -369,7 +369,7 @@ public function updateOrderState(string $klarnaOrderId): void // TODO: Consider saving cancel status in database klarna table if ($klarnaStatus === KcoApiInterface::ORDER_STATUS_CANCELLED) { $this->logger->info( - 'Klarna order is ' . $klarnaStatus . '. Cancelling Magento order: ' + 'Kustom order is ' . $klarnaStatus . '. Cancelling Magento order: ' . $this->mageOrder->getIncrementId() ); $this->cancelMagentoOrder($this->mageOrder, $klarnaStatus); @@ -396,7 +396,7 @@ public function updateOrderState(string $klarnaOrderId): void private function cancelOrder(MagentoOrderInterface $order, string $klarnaOrderId): void { if ($order->isCanceled()) { - $this->logger->debug('Cancel the order on the klarna side because it is canceled in the shop'); + $this->logger->debug('Cancel the order on the Kustom side because it is canceled in the shop'); $this->cancelKlarnaOrder($klarnaOrderId, 'Order Canceled in Magento'); } } @@ -432,12 +432,12 @@ private function cancelMagentoOrder(MagentoOrderInterface $order, string $klarna $order->setState(SalesOrder::STATE_CANCELED); $order->setStatus(SalesOrder::STATE_CANCELED); $order->addStatusHistoryComment( - __('Order automatically cancelled because Klarna status is: %1', $klarnaStatus) + __('Order automatically cancelled because Kustom status is: %1', $klarnaStatus) ); $this->mageOrderRepository->save($order); $this->logger->info( - 'Magento order ' . $order->getIncrementId() . ' cancelled due to Klarna status: ' . $klarnaStatus + 'Magento order ' . $order->getIncrementId() . ' cancelled due to Kustom status: ' . $klarnaStatus ); } @@ -469,7 +469,7 @@ private function updateOrderWithKlarnaReference( } } - $this->logger->debug('Updated the order with the klarna reference'); + $this->logger->debug('Updated the order with the Kustom reference'); } /** @@ -503,7 +503,7 @@ private function acknowledgeOrder( // TODO: Consider: Should we cancel order in Magento here? throw new KlarnaException(__('Acknowledge call failed. Check log for details.')); } - $order->addStatusHistoryComment('Acknowledged request sent to Klarna'); + $order->addStatusHistoryComment('Acknowledged request sent to Kustom'); $klarnaOrder->setIsAcknowledged(1); $this->orderRepository->save($klarnaOrder); } @@ -544,7 +544,7 @@ public function checkAndUpdateOrderState(string $orderId): void $orderDetails = $this->paymentStatus->getStatusUpdate($klarnaOrder); if (!$orderDetails->getIsSuccessful()) { - throw new LocalizedException(__('An error happened when retrieving the status of the order from Klarna')); + throw new LocalizedException(__('An error happened when retrieving the status of the order from Kustom')); } $this->checkOrderState($mageOrder, $orderDetails->getStatus()); @@ -552,7 +552,7 @@ public function checkAndUpdateOrderState(string $orderId): void return; } - throw new LocalizedException(__('Order is still PENDING with Klarna')); + throw new LocalizedException(__('Order is still PENDING with Kustom')); } /** @@ -570,7 +570,7 @@ private function getKlarnaOrderByMagentoOrder(MagentoOrderInterface $mageOrder): $this->denyPayment( $mageOrder, __( - 'Canceled the order since no Klarna information could ' . + 'Canceled the order since no Kustom information could ' . 'be found in the Magento database for the order.' ) ); @@ -614,7 +614,7 @@ private function checkOrderState(MagentoOrderInterface $mageOrder, string $statu if (in_array($status, $stopStatuses)) { $this->denyPayment( $mageOrder, - __('Canceled the order as Klarna shows it as %1', $status) + __('Canceled the order as Kustom shows it as %1', $status) ); } } @@ -652,4 +652,21 @@ public function getMagentoOrder(): ?MagentoOrderInterface { return $this->mageOrder; } + + /** + * @param string $klarnaOrderId + * + * @return bool + */ + public function isMagentoOrderExists(string $klarnaOrderId): bool + { + try { + $this->workflowProvider->setKlarnaOrderId($klarnaOrderId); + $order = $this->workflowProvider->getMagentoOrder(); + + return (bool) $order->getId(); + } catch (KlarnaException $exception) { + return false; + } + } } diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php new file mode 100644 index 0000000..c291ee2 --- /dev/null +++ b/Test/Integration/Controller/Api/PushTest.php @@ -0,0 +1,558 @@ +kOrderFactory = $this->_objectManager->create(KlarnaOrderFactory::class); + $this->mOrderFactory = $this->_objectManager->create(MagentoOrderFactory::class); + $this->checkoutMock = $this->createMock(Checkout::class); + $this->_objectManager->addSharedInstance($this->checkoutMock, Checkout::class); + $this->orderManagementMock = $this->createMock(Ordermanagement::class); + $this->_objectManager->addSharedInstance($this->orderManagementMock, Ordermanagement::class); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/klarna_order_setup1_single_simple_product.php + */ + public function testExecuteShouldSuccessfullyAcknowledgeUnacknowledgedOrder(): void + { + $expectedResponse = '[]'; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'new', + 'status' => 'pending', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + ], + ] + ); + + $this->orderManagementMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'captured_amount' => 0, + 'captures' => [], + 'klarna_reference' => '12345', + ]); + $this->orderManagementMock->expects($this->any())->method('updateMerchantReferences') + ->willReturn([]); + $this->orderManagementMock->expects($this->any())->method('acknowledgeOrder') + ->willReturn(['is_successful' => true]); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '1', + ], + [ + 'state' => 'new', + 'status' => 'pending', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + 'klarna_reference' => '12345', + ], + ] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/klarna_order_setup1_single_simple_product.php + */ + public function testExecuteShouldSuccessfullyCancelOrderByCancelStatusInOrderData(): void + { + $expectedResponse = '[]'; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'new', + 'status' => 'pending', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + ], + ] + ); + + $this->orderManagementMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'captured_amount' => 0, + 'captures' => [], + 'klarna_reference' => '12345', + 'status' => 'CANCELLED', + ]); + $this->orderManagementMock->expects($this->any())->method('updateMerchantReferences') + ->willReturn([]); + $this->orderManagementMock->expects($this->any())->method('acknowledgeOrder') + ->willReturn(['is_successful' => true]); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'canceled', + 'status' => 'canceled', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + ], + ] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoConfigFixture current_store klarna/api/debug 1 + * @magentoConfigFixture current_store general/region/state_required '' + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/quote_setup1_single_simple_product.php + */ + public function testExecuteShouldSuccessfullyCreateOrderByCheckoutApiResponse(): void + { + $expectedResponse = '[]'; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + $this->checkoutMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'billing_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'shipping_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'order_id' => $klarnaOrderId, + 'is_successful' => true, + 'order_lines' => [ + [ + 'image_url' => '', + 'name' => 'Simple Product', + 'product_url' => 'http://localhost/index.php/simple-product.html', + 'quantity' => 1, + 'reference' => 'simple', + 'tax_rate' => 0, + 'total_amount' => 1000, + 'total_discount_amount' => 0, + 'total_tax_amount' => 0, + 'type' => 'physical', + 'unit_price' => 1000, + ] + ], + 'selected_shipping_option' => [ + 'id' => 'flatrate_flatrate', + 'price' => 500, + 'tax_amount' => 0, + 'tax_rate' => 0, + ], + 'order_amount' => 1500, + 'status' => 'checkout_complete', + ]); + + $this->orderManagementMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'order_id' => $klarnaOrderId, + 'klarna_reference' => '12345', + ]); + $this->orderManagementMock->expects($this->any())->method('updateMerchantReferences') + ->willReturn([]); + $this->orderManagementMock->expects($this->any())->method('acknowledgeOrder') + ->willReturn(['is_successful' => true]); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '1', + ], + [ + 'state' => 'processing', + 'status' => 'processing', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Kustom Checkout', + 'klarna_reference' => '12345', + ], + ] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoConfigFixture current_store general/region/state_required '' + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/quote_setup1_single_simple_product.php + */ + public function testExecuteShouldNotCreateOrderWhenCartIsLocked(): void + { + $expectedResponse = '{"error":"The cart is locked for processing. Please try again later."}'; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + // MTF overrides lockers with a dummy so this is one way to trigger lock error + $lockManagerMock = $this->createMock(LockManagerInterface::class); + $cartMutex = $this->_objectManager->create(CartMutex::class, ['lockManager' => $lockManagerMock]); + $this->_objectManager->addSharedInstance($cartMutex, CartMutex::class); + $quoteManagement = $this->_objectManager->create(QuoteManagement::class, [ + 'cartMutex' => $cartMutex, + ]); + $this->_objectManager->addSharedInstance($quoteManagement, QuoteManagement::class); + $lockManagerMock->expects($this->any())->method('lock')->willReturn(false); + + $this->checkoutMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'billing_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'shipping_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'order_id' => $klarnaOrderId, + 'is_successful' => true, + 'order_lines' => [ + [ + 'image_url' => '', + 'name' => 'Simple Product', + 'product_url' => 'http://localhost/index.php/simple-product.html', + 'quantity' => 1, + 'reference' => 'simple', + 'tax_rate' => 0, + 'total_amount' => 1000, + 'total_discount_amount' => 0, + 'total_tax_amount' => 0, + 'type' => 'physical', + 'unit_price' => 1000, + ] + ], + 'selected_shipping_option' => [ + 'id' => 'flatrate_flatrate', + 'price' => 500, + 'tax_amount' => 0, + 'tax_rate' => 0, + ], + 'order_amount' => 1500, + 'status' => 'checkout_complete', + ]); + + $this->orderManagementMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'order_id' => $klarnaOrderId, + 'klarna_reference' => '12345', + ]); + $this->orderManagementMock->expects($this->any())->method('updateMerchantReferences') + ->willReturn([]); + $this->orderManagementMock->expects($this->any())->method('acknowledgeOrder') + ->willReturn(['is_successful' => true]); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testExecuteShouldThrowAnErrorWhenIdMatchesNothing(): void + { + $expectedResponse = '{"error":"Failed to create order"}'; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/klarna_order_setup1_single_simple_product.php + */ + public function testExecuteShouldNotCancelOrderDueToLocalizedException(): void + { + $expectedResponse = '{"error":"Failed to update order state"}'; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'new', + 'status' => 'pending', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + ], + ] + ); + + $this->orderManagementMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'captured_amount' => 0, + 'captures' => [], + 'klarna_reference' => '12345', + ]); + $this->orderManagementMock->expects($this->any())->method('updateMerchantReferences') + ->willThrowException(new LocalizedException(__('Test error'))); + $this->orderManagementMock->expects($this->never())->method('cancelOrder'); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'new', + 'status' => 'pending', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + ], + ] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/quote_setup1_single_simple_product.php + */ + public function testExecuteShouldNotCancelOrderDueToLocalizedExceptionWhenCreatingOneWithCheckoutApiDetails(): void + { + $expectedResponse = '{"error":"Failed to create order"}'; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + $this->checkoutMock->expects($this->any())->method('getOrder') + ->willThrowException(new LocalizedException(__('Test error'))); + $this->orderManagementMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'captured_amount' => 0, + 'captures' => [], + 'klarna_reference' => '12345', + ]); + $this->orderManagementMock->expects($this->never())->method('cancelOrder'); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + } + + /** + * @param string $klarnaOrderId + * @param mixed[] $expectedKlarnaOrder + * @param mixed[] $expectedOrder + * @param mixed[] $expectedPayment + * + * @return void + * @throws LocalizedException + */ + private function assertOrderData( + string $klarnaOrderId, + array $expectedKlarnaOrder, + array $expectedOrder, + array $expectedPayment + ): void { + $klarnaOrder = $this->kOrderFactory->create()->load($klarnaOrderId, 'klarna_order_id'); + if (!$expectedKlarnaOrder) { + $this->assertNull($klarnaOrder->getId(), 'Assert that order does not exist'); + + return; + } + + $klarnaOrderData = array_intersect_key($klarnaOrder->getData(), $expectedKlarnaOrder); + $this->assertEquals($expectedKlarnaOrder, $klarnaOrderData); + + $magentoOrder = $this->mOrderFactory->create()->load($klarnaOrder->getOrderId()); + $magentoOrderData = array_intersect_key($magentoOrder->getData(), $expectedOrder); + $this->assertEquals($expectedOrder, $magentoOrderData); + + $paymentData = $magentoOrder->getId() ? $magentoOrder->getPayment()->getData() : []; + $paymentData = array_intersect_key($paymentData, $expectedPayment); + $this->assertEquals($expectedPayment, $paymentData); + } +} diff --git a/Test/Integration/Controller/Klarna/ConfirmationTest.php b/Test/Integration/Controller/Klarna/ConfirmationTest.php new file mode 100644 index 0000000..501479f --- /dev/null +++ b/Test/Integration/Controller/Klarna/ConfirmationTest.php @@ -0,0 +1,473 @@ +kOrderFactory = $this->_objectManager->create(KlarnaOrderFactory::class); + $this->mOrderFactory = $this->_objectManager->create(MagentoOrderFactory::class); + $this->checkoutMock = $this->createMock(Checkout::class); + $this->_objectManager->addSharedInstance($this->checkoutMock, Checkout::class); + $this->orderManagementMock = $this->createMock(Ordermanagement::class); + $this->_objectManager->addSharedInstance($this->orderManagementMock, Ordermanagement::class); + } + + /** + * @magentoAppArea frontend + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoConfigFixture current_store klarna/api/debug 1 + * @magentoConfigFixture current_store general/region/state_required '' + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/quote_setup1_single_simple_product.php + */ + public function testExecuteShouldSuccessfullyCreateOrderByCheckoutApiResponse(): void + { + $expectedRedirect = 'checkout/klarna/success'; + $expectedMessages = []; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + $this->checkoutMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'billing_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'shipping_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'order_id' => $klarnaOrderId, + 'is_successful' => true, + 'order_lines' => [ + [ + 'image_url' => '', + 'name' => 'Simple Product', + 'product_url' => 'http://localhost/index.php/simple-product.html', + 'quantity' => 1, + 'reference' => 'simple', + 'tax_rate' => 0, + 'total_amount' => 1000, + 'total_discount_amount' => 0, + 'total_tax_amount' => 0, + 'type' => 'physical', + 'unit_price' => 1000, + ] + ], + 'selected_shipping_option' => [ + 'id' => 'flatrate_flatrate', + 'price' => 500, + 'tax_amount' => 0, + 'tax_rate' => 0, + ], + 'order_amount' => 1500, + 'status' => 'checkout_complete', + ]); + + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->dispatch('checkout/klarna/confirmation/id/' . $klarnaOrderId); + $this->assertRedirect($this->stringContains($expectedRedirect)); + $this->assertSessionMessages($this->equalTo($expectedMessages)); + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'processing', + 'status' => 'processing', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Kustom Checkout', + ], + ] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoConfigFixture current_store general/region/state_required '' + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/quote_setup1_single_simple_product.php + */ + public function testExecuteShouldJustRedirectUserToSuccessPageWhenCartIsLocked(): void + { + $expectedRedirect = 'checkout/klarna/success'; + $expectedMessages = []; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + // MTF overrides lockers with a dummy so this is one way to trigger lock error + $lockManagerMock = $this->createMock(LockManagerInterface::class); + $cartMutex = $this->_objectManager->create(CartMutex::class, ['lockManager' => $lockManagerMock]); + $this->_objectManager->addSharedInstance($cartMutex, CartMutex::class); + $quoteManagement = $this->_objectManager->create(QuoteManagement::class, [ + 'cartMutex' => $cartMutex, + ]); + $this->_objectManager->addSharedInstance($quoteManagement, QuoteManagement::class); + $lockManagerMock->expects($this->any())->method('lock')->willReturn(false); + + $this->checkoutMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'billing_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'shipping_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'order_id' => $klarnaOrderId, + 'is_successful' => true, + 'order_lines' => [ + [ + 'image_url' => '', + 'name' => 'Simple Product', + 'product_url' => 'http://localhost/index.php/simple-product.html', + 'quantity' => 1, + 'reference' => 'simple', + 'tax_rate' => 0, + 'total_amount' => 1000, + 'total_discount_amount' => 0, + 'total_tax_amount' => 0, + 'type' => 'physical', + 'unit_price' => 1000, + ] + ], + 'selected_shipping_option' => [ + 'id' => 'flatrate_flatrate', + 'price' => 500, + 'tax_amount' => 0, + 'tax_rate' => 0, + ], + 'order_amount' => 1500, + 'status' => 'checkout_complete', + ]); + + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->dispatch('checkout/klarna/confirmation/id/' . $klarnaOrderId); + $this->assertRedirect($this->stringContains($expectedRedirect)); + $this->assertSessionMessages($this->equalTo($expectedMessages)); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/klarna_order_setup1_single_simple_product.php + */ + public function testExecuteShouldJustRedirectUserToSuccessPageWhenOrderAlreadyExists(): void + { + $expectedRedirect = 'checkout/klarna/success'; + $expectedMessages = []; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'new', + 'status' => 'pending', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + ], + ] + ); + + $this->checkoutMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'billing_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'shipping_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + 'region' => 'California', + ], + 'order_id' => $klarnaOrderId, + 'is_successful' => true, + 'order_lines' => [ + [ + 'image_url' => '', + 'name' => 'Simple Product', + 'product_url' => 'http://localhost/index.php/simple-product.html', + 'quantity' => 1, + 'reference' => 'simple', + 'tax_rate' => 0, + 'total_amount' => 1000, + 'total_discount_amount' => 0, + 'total_tax_amount' => 0, + 'type' => 'physical', + 'unit_price' => 1000, + ] + ], + 'selected_shipping_option' => [ + 'id' => 'flatrate_flatrate', + 'price' => 500, + 'tax_amount' => 0, + 'tax_rate' => 0, + ], + 'order_amount' => 1500, + 'status' => 'checkout_complete', + ]); + + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->dispatch('checkout/klarna/confirmation/id/' . $klarnaOrderId); + $this->assertRedirect($this->stringContains($expectedRedirect)); + $this->assertSessionMessages($this->equalTo($expectedMessages)); + + $this->assertOrderData( + $klarnaOrderId, + [ + 'klarna_order_id' => $klarnaOrderId, + 'is_acknowledged' => '0', + ], + [ + 'state' => 'new', + 'status' => 'pending', + 'increment_id' => '100000001', + ], + [ + 'additional_information' => [ + 'method_title' => 'Check / Money order', + ], + ] + ); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testExecuteShouldThrowAnErrorWhenIdMatchesNothing(): void + { + $expectedRedirect = 'checkout/cart'; + $expectedMessages = ['No Kustom Kco quote could be found with the provided Kustom order id: 123456-1234-1234-1234-1234567890']; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->dispatch('checkout/klarna/confirmation/id/' . $klarnaOrderId); + $this->assertRedirect($this->stringContains($expectedRedirect)); + $this->assertSessionMessages($this->equalTo($expectedMessages)); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testExecuteShouldThrowAnErrorWhenNoIdIsGiven(): void + { + $expectedRedirect = 'checkout/cart'; + $expectedMessages = ['Unable to process order. Please try again']; + + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->dispatch('checkout/klarna/confirmation/id/'); + $this->assertRedirect($this->stringContains($expectedRedirect)); + $this->assertSessionMessages($this->equalTo($expectedMessages)); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/quote_setup1_single_simple_product.php + */ + public function testExecuteShouldNotCancelOrderDueToLocalizedException(): void + { + $expectedRedirect = 'checkout/cart'; + $expectedMessages = ['Test error']; + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + $this->checkoutMock->expects($this->any())->method('getOrder') + ->willThrowException(new LocalizedException(__('Test error'))); + $this->orderManagementMock->expects($this->any())->method('getOrder') + ->willReturn([ + 'captured_amount' => 0, + 'captures' => [], + 'klarna_reference' => '12345', + ]); + $this->orderManagementMock->expects($this->never())->method('cancelOrder'); + + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->dispatch('checkout/klarna/confirmation/id/' . $klarnaOrderId); + $this->assertRedirect($this->stringContains($expectedRedirect)); + $this->assertSessionMessages($this->equalTo($expectedMessages)); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + } + + /** + * @param string $klarnaOrderId + * @param mixed[] $expectedKlarnaOrder + * @param mixed[] $expectedOrder + * @param mixed[] $expectedPayment + * + * @return void + * @throws LocalizedException + */ + private function assertOrderData( + string $klarnaOrderId, + array $expectedKlarnaOrder, + array $expectedOrder, + array $expectedPayment + ): void { + $klarnaOrder = $this->kOrderFactory->create()->load($klarnaOrderId, 'klarna_order_id'); + if (!$expectedKlarnaOrder) { + $this->assertNull($klarnaOrder->getId(), 'Assert that order does not exist'); + + return; + } + + $klarnaOrderData = array_intersect_key($klarnaOrder->getData(), $expectedKlarnaOrder); + $this->assertEquals($expectedKlarnaOrder, $klarnaOrderData); + + $magentoOrder = $this->mOrderFactory->create()->load($klarnaOrder->getOrderId()); + $magentoOrderData = array_intersect_key($magentoOrder->getData(), $expectedOrder); + $this->assertEquals($expectedOrder, $magentoOrderData); + + $paymentData = $magentoOrder->getId() ? $magentoOrder->getPayment()->getData() : []; + $paymentData = array_intersect_key($paymentData, $expectedPayment); + $this->assertEquals($expectedPayment, $paymentData); + } +} diff --git a/Test/Unit/Model/Cart/FullUpdateTest.php b/Test/Unit/Model/Cart/FullUpdateTest.php index a646471..0877f62 100644 --- a/Test/Unit/Model/Cart/FullUpdateTest.php +++ b/Test/Unit/Model/Cart/FullUpdateTest.php @@ -6,7 +6,7 @@ * and LICENSE files that were distributed with this source code. */ -namespace Klarna\Kco\Test\Unit\Model\Checkout; +namespace Klarna\Kco\Test\Unit\Model\Cart; use Klarna\Kco\Model\Cart\FullUpdate; use Klarna\Base\Test\Unit\Mock\MockFactory; @@ -112,7 +112,12 @@ protected function setUp(): void $this->quote->method('getShippingAddress') ->willReturn($quoteShippingAddress); - $extensionAttributes = $this->mockFactory->create(CartExtension::class, [], ['getShippingAssignments']); + if (method_exists(CartExtension::class, 'getShippingAssignments')) { + $extensionAttributes = $this->mockFactory->create(CartExtension::class, ['getShippingAssignments']); + } else { + $extensionAttributes = $this->mockFactory->create(CartExtension::class, [], ['getShippingAssignments']); + } + $extensionAttributes->method('getShippingAssignments') ->willReturn([]);