From 6912c5f20510f03acb4c4c9b97cc180128faf547 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Wed, 6 May 2026 09:42:50 +0300 Subject: [PATCH 01/12] Fixed issue in one of the module unit tests that wasn't compatible with different Magento versions --- Test/Unit/Model/Cart/FullUpdateTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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([]); From 69b20cb6418af89804da7ea30d3a370cde8371a8 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Fri, 22 May 2026 14:32:32 +0300 Subject: [PATCH 02/12] KUSTOM-92: Prepared tests to verify some of the existing push controller logic flow --- Test/Integration/Controller/Api/PushTest.php | 416 +++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 Test/Integration/Controller/Api/PushTest.php diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php new file mode 100644 index 0000000..ac5179f --- /dev/null +++ b/Test/Integration/Controller/Api/PushTest.php @@ -0,0 +1,416 @@ +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 + * @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', + ], + 'shipping_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + ], + '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' => 'Klarna Checkout', + 'klarna_reference' => '12345', + ], + ] + ); + } + + /** + * @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 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', + ], + 'shipping_address' => [ + 'city' => 'City', + 'country' => 'US', + 'email' => 'customer@example.com', + 'family_name' => 'Lastname', + 'given_name' => 'Firstname', + 'phone' => '040123456', + 'postal_code' => '12345', + 'street_address' => 'Street', + ], + '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, + [], + [], + [] + ); + } + + /** + * @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'); + $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); + } +} From c2b4a81ac3dad2ca09bfe3bb51a368f21eee6c66 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Mon, 25 May 2026 15:08:48 +0300 Subject: [PATCH 03/12] KUSTOM-92: Added further test cases, this time just with replicating currently occurring errors related to premature order cancelling based on very generic exception throwing --- Test/Integration/Controller/Api/PushTest.php | 126 +++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index ac5179f..95ae03d 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -12,6 +12,7 @@ namespace Klarna\Base\Test\Integration\Controller; use Klarna\Backend\Model\Api\Rest\Service\Ordermanagement; +use Klarna\Base\Exception; use Klarna\Base\Model\OrderFactory as KlarnaOrderFactory; use Klarna\Kco\Model\Api\Rest\Service\Checkout; use Magento\Framework\App\Request\Http; @@ -386,6 +387,131 @@ public function testExecuteShouldNotCreateOrderWhenCartIsLocked(): void ); } + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ + public function testExecuteShouldThrowAnErrorWhenIdMatchesNothing(): void + { + $klarnaOrderId = '123456-1234-1234-1234-1234567890'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage( + 'No Klarna Kco quote could be found with the provided Klarna order id: ' . $klarnaOrderId + ); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + + $this->getRequest()->setMethod(Http::METHOD_POST); + $this->dispatch('kco/api/push/id/' . $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":"Test error"}'; + $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":"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_POST); + $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); + } + /** * @param string $klarnaOrderId * @param mixed[] $expectedKlarnaOrder From 2e62f363b2b1ce9bb5cd5b42a2ddbe831c5f4bbd Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Mon, 25 May 2026 15:42:50 +0300 Subject: [PATCH 04/12] KUSTOM-92: Starting to refactor the push controller to clearly separate order state updates and order creation flows, removed also cancelling of orders when encountering LocalizedException --- Controller/Api/Push.php | 160 ++++++++++--------- Test/Integration/Controller/Api/PushTest.php | 4 +- 2 files changed, 86 insertions(+), 78 deletions(-) diff --git a/Controller/Api/Push.php b/Controller/Api/Push.php index 43c1c9f..8dc0ac6 100644 --- a/Controller/Api/Push.php +++ b/Controller/Api/Push.php @@ -1,10 +1,12 @@ request->getParam('id'); $this->workflowProvider->setKlarnaOrderId($klarnaOrderId); + $this->logger->debug('Push: klarna order id: ' . $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 + if ($this->canUpdateOrderState()) { + return $this->updateOrderState($klarnaOrderId); } - $this->logger->debug('Push: klarna order id: ' . $klarnaOrderId); + return $this->createOrder($klarnaOrderId); + } + + /** + * @return bool + */ + private function canUpdateOrderState(): bool + { + // 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 + + try { + $this->workflowProvider->getMagentoOrder(); + $this->workflowProvider->getKlarnaOrder(); + return true; + } catch (KlarnaException $exception) { + return false; + } + } + + /** + * @param string $klarnaOrderId + * + * @return Json + */ + private function updateOrderState(string $klarnaOrderId): Json + { try { $this->checkoutOrder->updateOrderState($klarnaOrderId); - } catch (KlarnaException $e) { - return $this->createOrder($klarnaOrderId); } catch (LocalizedException $e) { $this->apiLogger->logCallbackException( $this->container, @@ -137,7 +160,11 @@ public function execute() $this->request, $e ); - return $this->cancelKlarnaOrder($klarnaOrderId, $e); + + return $this->result->getJsonResult( + 500, + ['error' => 'Failed to update order state'] + ); } $magentoOrder = $this->checkoutOrder->getMagentoOrder(); @@ -149,23 +176,48 @@ public function execute() } /** - * Canceling the Klarna order + * 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 - * @param LocalizedException $e + * @param string $klarnaOrderId * @return Json * @throws KlarnaException */ - private function cancelKlarnaOrder(string $klarnaOrderId, LocalizedException $e): Json + private function createOrder(string $klarnaOrderId): Json { - $this->logger->critical('Push: Cancelling order. Error occured: ' . $e->getMessage()); - $responseCodeObject = $this->getFailureResponseObject(500); - $this->checkoutOrder->cancelKlarnaOrder($klarnaOrderId, $e->getMessage()); + 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->result->getJsonResult( - (int)$responseCodeObject->getResponseCode(), - ['error' => $e->getMessage()] - ); + return $this->getSuccessResponse(); + } + + $this->apiLogger->logCallbackException( + $this->container, + ApiInterface::ACTIONS['push'], + $this->request, + $e + ); + + return $this->result->getJsonResult( + 500, + ['error' => 'Failed to create order'] + ); + } + + return $this->getSuccessResponse(); } /** @@ -176,21 +228,8 @@ private function cancelKlarnaOrder(string $klarnaOrderId, LocalizedException $e) private function getSuccessResponse(): Json { $this->logger->debug('Push: success'); - return $this->result->getJsonResult(200); - } - - /** - * 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); - return $object; + return $this->result->getJsonResult(200); } /** @@ -204,41 +243,10 @@ private function getCartLockedResponse(string $klarnaOrderId, CartLockedExceptio $this->logger->debug( 'Push: Retry order ' . $klarnaOrderId . ' - Exception: ' . $e->getMessage() ); + return $this->result->getJsonResult( 503, ['error' => $e->getMessage()] ); } - - /** - * 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 - { - 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); - } - return $this->getSuccessResponse(); - } } diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index 95ae03d..c3f53fd 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -418,7 +418,7 @@ public function testExecuteShouldThrowAnErrorWhenIdMatchesNothing(): void */ public function testExecuteShouldNotCancelOrderDueToLocalizedException(): void { - $expectedResponse = '{"error":"Test error"}'; + $expectedResponse = '{"error":"Failed to update order state"}'; $klarnaOrderId = '123456-1234-1234-1234-1234567890'; $this->assertOrderData( @@ -480,7 +480,7 @@ public function testExecuteShouldNotCancelOrderDueToLocalizedException(): void */ public function testExecuteShouldNotCancelOrderDueToLocalizedExceptionWhenCreatingOneWithCheckoutApiDetails(): void { - $expectedResponse = '{"error":"Test error"}'; + $expectedResponse = '{"error":"Failed to create order"}'; $klarnaOrderId = '123456-1234-1234-1234-1234567890'; $this->assertOrderData( From 251efabcc33606d176f9e7f453078c33612092fc Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Tue, 26 May 2026 09:51:35 +0300 Subject: [PATCH 05/12] KUSTOM-92: Noticed some overlap in order creation and order state flows, refactored the logic to remove the overlap --- Controller/Api/Push.php | 114 +++++++------------ Test/Integration/Controller/Api/PushTest.php | 15 ++- 2 files changed, 51 insertions(+), 78 deletions(-) diff --git a/Controller/Api/Push.php b/Controller/Api/Push.php index 8dc0ac6..469587f 100644 --- a/Controller/Api/Push.php +++ b/Controller/Api/Push.php @@ -47,12 +47,6 @@ class Push extends CsrfAbstract implements HttpPostActionInterface */ private $checkoutOrder; - /** - * @var DataObjectFactory - * @deprecated To be removed as unused in next major release, alongside the constructor argument - */ - private $dataObjectFactory; - /** * @var Result */ @@ -91,7 +85,7 @@ class Push extends CsrfAbstract implements HttpPostActionInterface public function __construct( LoggerInterface $logger, CheckoutOrder $checkoutOrder, - DataObjectFactory $dataObjectFactory, + DataObjectFactory $dataObjectFactory, // To be removed as unused in next major release! Result $result, Logger $apiLogger, Container $container, @@ -100,7 +94,6 @@ public function __construct( ) { $this->logger = $logger; $this->checkoutOrder = $checkoutOrder; - $this->dataObjectFactory = $dataObjectFactory; $this->result = $result; $this->apiLogger = $apiLogger; $this->container = $container; @@ -119,17 +112,18 @@ public function execute() $this->workflowProvider->setKlarnaOrderId($klarnaOrderId); $this->logger->debug('Push: klarna order id: ' . $klarnaOrderId); - if ($this->canUpdateOrderState()) { - return $this->updateOrderState($klarnaOrderId); + $createOrderStatus = $this->canCreateOrder() ? $this->createOrder($klarnaOrderId) : true; + if ($createOrderStatus instanceof Json) { + return $createOrderStatus; } - return $this->createOrder($klarnaOrderId); + return $this->updateOrderState($klarnaOrderId); } /** * @return bool */ - private function canUpdateOrderState(): bool + private function canCreateOrder(): bool { // 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 @@ -138,22 +132,43 @@ private function canUpdateOrderState(): bool $this->workflowProvider->getMagentoOrder(); $this->workflowProvider->getKlarnaOrder(); - return true; - } catch (KlarnaException $exception) { return false; + } catch (KlarnaException $exception) { + return true; } } /** * @param string $klarnaOrderId * - * @return Json + * @return Json|true */ - private function updateOrderState(string $klarnaOrderId): Json + private function createOrder(string $klarnaOrderId) { + $this->logger->debug('Push: Attempting to create order by id ' . $klarnaOrderId); + try { - $this->checkoutOrder->updateOrderState($klarnaOrderId); + $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) { + // 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 true; + } + $this->apiLogger->logCallbackException( $this->container, ApiInterface::ACTIONS['push'], @@ -163,47 +178,25 @@ private function updateOrderState(string $klarnaOrderId): Json return $this->result->getJsonResult( 500, - ['error' => 'Failed to update order state'] + ['error' => 'Failed to create order'] ); } - $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: Order created successfully by id ' . $klarnaOrderId); - return $this->getSuccessResponse(); + 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(); - } - $this->apiLogger->logCallbackException( $this->container, ApiInterface::ACTIONS['push'], @@ -213,40 +206,17 @@ private function createOrder(string $klarnaOrderId): Json return $this->result->getJsonResult( 500, - ['error' => 'Failed to create order'] + ['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, []); - /** - * Getting back the success response - * - * @return Json - */ - private function getSuccessResponse(): Json - { $this->logger->debug('Push: success'); return $this->result->getJsonResult(200); } - - /** - * @param string $klarnaOrderId - * @param CartLockedException $e - * - * @return Json - */ - private function getCartLockedResponse(string $klarnaOrderId, CartLockedException $e): Json - { - $this->logger->debug( - 'Push: Retry order ' . $klarnaOrderId . ' - Exception: ' . $e->getMessage() - ); - - return $this->result->getJsonResult( - 503, - ['error' => $e->getMessage()] - ); - } } diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index c3f53fd..be69b07 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -12,7 +12,6 @@ namespace Klarna\Base\Test\Integration\Controller; use Klarna\Backend\Model\Api\Rest\Service\Ordermanagement; -use Klarna\Base\Exception; use Klarna\Base\Model\OrderFactory as KlarnaOrderFactory; use Klarna\Kco\Model\Api\Rest\Service\Checkout; use Magento\Framework\App\Request\Http; @@ -393,13 +392,9 @@ public function testExecuteShouldNotCreateOrderWhenCartIsLocked(): void */ public function testExecuteShouldThrowAnErrorWhenIdMatchesNothing(): void { + $expectedResponse = '{"error":"Failed to create order"}'; $klarnaOrderId = '123456-1234-1234-1234-1234567890'; - $this->expectException(Exception::class); - $this->expectExceptionMessage( - 'No Klarna Kco quote could be found with the provided Klarna order id: ' . $klarnaOrderId - ); - $this->assertOrderData( $klarnaOrderId, [], @@ -409,6 +404,14 @@ public function testExecuteShouldThrowAnErrorWhenIdMatchesNothing(): void $this->getRequest()->setMethod(Http::METHOD_POST); $this->dispatch('kco/api/push/id/' . $klarnaOrderId); + $this->assertEquals($expectedResponse, $this->getResponse()->getBody()); + + $this->assertOrderData( + $klarnaOrderId, + [], + [], + [] + ); } /** From 7fd143e20e7fbbeb79ed9bc9ea838cc7ef2361d9 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Wed, 27 May 2026 10:54:59 +0300 Subject: [PATCH 06/12] KUSTOM-92: Updated logging for order creation failures to be more verbose, as the API logger only adds data to database which we can't necessarily grab in scope of test pipelines --- Controller/Api/Push.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Controller/Api/Push.php b/Controller/Api/Push.php index 469587f..f53b879 100644 --- a/Controller/Api/Push.php +++ b/Controller/Api/Push.php @@ -169,6 +169,8 @@ private function createOrder(string $klarnaOrderId) return true; } + $this->logger->debug('Push: Order creation failed: ' . $e->getMessage()); + $this->apiLogger->logCallbackException( $this->container, ApiInterface::ACTIONS['push'], From b05434645ffd0ae7612e493c3372b7ca77618ac0 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Wed, 27 May 2026 12:08:11 +0300 Subject: [PATCH 07/12] KUSTOM-92: Updated one of the tests with debug flag to get logs about the logic flow in 2.4.5 and 2.4.6 test executions --- Test/Integration/Controller/Api/PushTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index be69b07..24d8c1e 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -190,6 +190,7 @@ public function testExecuteShouldSuccessfullyCancelOrderByCancelStatusInOrderDat * @magentoAppIsolation enabled * @magentoDbIsolation enabled * @magentoConfigFixture current_store payment/klarna_kco/active 1 + * @magentoConfigFixture current_store klarna/api/debug 1 * @magentoDataFixture Klarna_Base::Test/Integration/_files/fixtures/quote_setup1_single_simple_product.php */ public function testExecuteShouldSuccessfullyCreateOrderByCheckoutApiResponse(): void From 3dbf31f3d6ff2ec2e0225b4be250b4b7484c9b54 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Wed, 27 May 2026 13:26:02 +0300 Subject: [PATCH 08/12] KUSTOM-92: Added region information in mocked checkout API responses, in order to get around the issue in more strict region validations in 2.4.5 and 2.4.6 in tests --- Test/Integration/Controller/Api/PushTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index 24d8c1e..c25ab8c 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -216,6 +216,7 @@ public function testExecuteShouldSuccessfullyCreateOrderByCheckoutApiResponse(): 'phone' => '040123456', 'postal_code' => '12345', 'street_address' => 'Street', + 'region' => 'California', ], 'shipping_address' => [ 'city' => 'City', @@ -226,6 +227,7 @@ public function testExecuteShouldSuccessfullyCreateOrderByCheckoutApiResponse(): 'phone' => '040123456', 'postal_code' => '12345', 'street_address' => 'Street', + 'region' => 'California', ], 'order_id' => $klarnaOrderId, 'is_successful' => true, @@ -327,6 +329,7 @@ public function testExecuteShouldNotCreateOrderWhenCartIsLocked(): void 'phone' => '040123456', 'postal_code' => '12345', 'street_address' => 'Street', + 'region' => 'California', ], 'shipping_address' => [ 'city' => 'City', @@ -337,6 +340,7 @@ public function testExecuteShouldNotCreateOrderWhenCartIsLocked(): void 'phone' => '040123456', 'postal_code' => '12345', 'street_address' => 'Street', + 'region' => 'California', ], 'order_id' => $klarnaOrderId, 'is_successful' => true, From 40be504eb77bbb1c40eaa1850b3521fe01244f9b Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Wed, 27 May 2026 13:38:04 +0300 Subject: [PATCH 09/12] KUSTOM-92: Updated configs in tests to disable region requirement since we may not at the moment support region_id information in the first place, only region field --- Test/Integration/Controller/Api/PushTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index c25ab8c..b1c0f06 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -191,6 +191,7 @@ public function testExecuteShouldSuccessfullyCancelOrderByCancelStatusInOrderDat * @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 @@ -294,6 +295,7 @@ public function testExecuteShouldSuccessfullyCreateOrderByCheckoutApiResponse(): * @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 From ee7fba673c30dfea594dd521d5ee3d94d37061d0 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Wed, 27 May 2026 13:44:30 +0300 Subject: [PATCH 10/12] KUSTOM-92: Adjusted the push controller test namespace --- Test/Integration/Controller/Api/PushTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index b1c0f06..119cdd4 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -9,7 +9,7 @@ declare(strict_types=1); -namespace Klarna\Base\Test\Integration\Controller; +namespace Klarna\Kco\Test\Integration\Controller; use Klarna\Backend\Model\Api\Rest\Service\Ordermanagement; use Klarna\Base\Model\OrderFactory as KlarnaOrderFactory; From bba797c73972313ecc0dfefb378fd4422a828111 Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Mon, 8 Jun 2026 09:55:21 +0300 Subject: [PATCH 11/12] KUSTOM-92: Adjusted changes based on another branch that will be also included in next release --- Test/Integration/Controller/Api/PushTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Test/Integration/Controller/Api/PushTest.php b/Test/Integration/Controller/Api/PushTest.php index 119cdd4..9ea9b11 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -284,7 +284,7 @@ public function testExecuteShouldSuccessfullyCreateOrderByCheckoutApiResponse(): ], [ 'additional_information' => [ - 'method_title' => 'Klarna Checkout', + 'method_title' => 'Kustom Checkout', 'klarna_reference' => '12345', ], ] From 4a5c1a61955b116aed85ad7f94da0804c8050cca Mon Sep 17 00:00:00 2001 From: Joona Melartin Date: Wed, 10 Jun 2026 12:49:59 +0300 Subject: [PATCH 12/12] KUSTOM-92: Applied similar fix to checkout/klarna/confirmation controller as in the push controller to solve issue with unnecessary cancellations. Updated also the checks for already created orders in case of hitting LocalizedException to be more certain about correctly recognizing the created order, as previously we relied on a class property value which wasn't necessarily even set. --- Controller/Api/Push.php | 5 +- Controller/Klarna/Confirmation.php | 48 +- Model/Order/Order.php | 43 +- Test/Integration/Controller/Api/PushTest.php | 8 +- .../Controller/Klarna/ConfirmationTest.php | 473 ++++++++++++++++++ 5 files changed, 531 insertions(+), 46 deletions(-) create mode 100644 Test/Integration/Controller/Klarna/ConfirmationTest.php diff --git a/Controller/Api/Push.php b/Controller/Api/Push.php index f53b879..7db16ad 100644 --- a/Controller/Api/Push.php +++ b/Controller/Api/Push.php @@ -160,10 +160,7 @@ private function createOrder(string $klarnaOrderId) ['error' => $exception->getMessage()] ); } 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('Push: Order already created by concurrent request: ' . $klarnaOrderId); return true; 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 index 9ea9b11..c291ee2 100644 --- a/Test/Integration/Controller/Api/PushTest.php +++ b/Test/Integration/Controller/Api/PushTest.php @@ -9,7 +9,7 @@ declare(strict_types=1); -namespace Klarna\Kco\Test\Integration\Controller; +namespace Klarna\Kco\Test\Integration\Controller\Api; use Klarna\Backend\Model\Api\Rest\Service\Ordermanagement; use Klarna\Base\Model\OrderFactory as KlarnaOrderFactory; @@ -538,6 +538,12 @@ private function assertOrderData( 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); 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); + } +}