diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrder.php index b1a7ed3bdc5b6..0aed9fe1dabde 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/PlaceOrder.php @@ -1,7 +1,7 @@ paymentManagement = $paymentManagement; $this->cartManagement = $cartManagement; + $this->logger = $logger ?: ObjectManager::getInstance() + ->get(LoggerInterface::class); } /** @@ -58,6 +69,36 @@ public function execute(Quote $cart, string $maskedCartId, int $userId): int $cartId = (int)$cart->getId(); $paymentMethod = $this->paymentManagement->get($cartId); + // Get a list of available payment methods for the cart + $availablePaymentMethods = $this->paymentManagement->getList($cartId); + $payment = $cart->getPayment(); + $paymentMethodCode = $payment?->getMethod(); + // Check if the selected payment method is in the available methods list + if ($paymentMethodCode && $availablePaymentMethods) { + $availableCodes = array_map(fn($method) => $method->getCode(), $availablePaymentMethods); + $isPaymentMethodAvailable = in_array($paymentMethodCode, $availableCodes); + } else { + $isPaymentMethodAvailable = false; + } + + if (!$isPaymentMethodAvailable) { + // Log the attempt to use a disabled payment method + $this->logger->debug( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => $paymentMethodCode, + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => $availablePaymentMethods ? + array_map(fn($method) => $method->getCode(), $availablePaymentMethods) : [] + ] + ); + + throw new LocalizedException( + __('The requested Payment Method \'%1\' is not available.', $paymentMethodCode ?: 'unknown') + ); + } + return (int)$this->cartManagement->placeOrder($cartId, $paymentMethod); } } diff --git a/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/PlaceOrderTest.php b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/PlaceOrderTest.php new file mode 100644 index 0000000000000..e7e9d31f6be97 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Test/Unit/Model/Cart/PlaceOrderTest.php @@ -0,0 +1,469 @@ +paymentManagementMock = $this->createMock(PaymentMethodManagementInterface::class); + $this->cartManagementMock = $this->createMock(CartManagementInterface::class); + $this->quoteMock = $this->createMock(Quote::class); + $this->paymentMock = $this->createMock(Payment::class); + $this->paymentInterfaceMock = $this->createMock(PaymentInterface::class); + $this->loggerMock = $this->createMock(LoggerInterface::class); + + $this->placeOrder = new PlaceOrder( + $this->paymentManagementMock, + $this->cartManagementMock, + $this->loggerMock + ); + } + + /** + * Test successful order placement with available payment method + */ + public function testExecuteWithAvailablePaymentMethod(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + $paymentMethodCode = 'checkmo'; + $orderId = 789; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn($paymentMethodCode); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $availableMethod1 = $this->createMock(PaymentMethodInterface::class); + $availableMethod1->method('getCode')->willReturn('paypal'); + + $availableMethod2 = $this->createMock(PaymentMethodInterface::class); + $availableMethod2->method('getCode')->willReturn($paymentMethodCode); + + $availableMethod3 = $this->createMock(PaymentMethodInterface::class); + $availableMethod3->method('getCode')->willReturn('stripe'); + + $availablePaymentMethods = [$availableMethod1, $availableMethod2, $availableMethod3]; + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn($availablePaymentMethods); + + $this->cartManagementMock->method('placeOrder') + ->with($cartId, $this->paymentInterfaceMock) + ->willReturn($orderId); + + $result = $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + + $this->assertEquals($orderId, $result); + } + + /** + * Test exception when payment method is not available + */ + public function testExecuteWithUnavailablePaymentMethod(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + $paymentMethodCode = 'unavailable_method'; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn($paymentMethodCode); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $availableMethod1 = $this->createMock(PaymentMethodInterface::class); + $availableMethod1->method('getCode')->willReturn('paypal'); + + $availableMethod2 = $this->createMock(PaymentMethodInterface::class); + $availableMethod2->method('getCode')->willReturn('stripe'); + + $availablePaymentMethods = [$availableMethod1, $availableMethod2]; + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn($availablePaymentMethods); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => $paymentMethodCode, + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => ['paypal', 'stripe'] + ] + ); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage("The requested Payment Method 'unavailable_method' is not available."); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } + + /** + * Test exception when no payment method is set on quote + */ + public function testExecuteWithNoPaymentMethodSet(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn(null); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $availableMethod = $this->createMock(PaymentMethodInterface::class); + $availableMethod->method('getCode')->willReturn('checkmo'); + $availablePaymentMethods = [$availableMethod]; + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn($availablePaymentMethods); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => null, + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => ['checkmo'] + ] + ); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage("The requested Payment Method 'unknown' is not available."); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } + + /** + * Test exception when no payment methods are available + */ + public function testExecuteWithNoAvailablePaymentMethods(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + $paymentMethodCode = 'checkmo'; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn($paymentMethodCode); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn([]); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => $paymentMethodCode, + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => [] + ] + ); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage("The requested Payment Method 'checkmo' is not available."); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } + + /** + * Test exception when available payment methods is null + */ + public function testExecuteWithNullAvailablePaymentMethods(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + $paymentMethodCode = 'checkmo'; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn($paymentMethodCode); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn(null); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => $paymentMethodCode, + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => [] + ] + ); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage("The requested Payment Method 'checkmo' is not available."); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } + + /** + * Test exception when quote has no payment object + */ + public function testExecuteWithNoPaymentObject(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn(null); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $availableMethod = $this->createMock(PaymentMethodInterface::class); + $availableMethod->method('getCode')->willReturn('checkmo'); + $availablePaymentMethods = [$availableMethod]; + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn($availablePaymentMethods); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => null, + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => ['checkmo'] + ] + ); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage("The requested Payment Method 'unknown' is not available."); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } + + /** + * Test that cart management exceptions are propagated + */ + public function testExecuteWithCartManagementException(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + $paymentMethodCode = 'checkmo'; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn($paymentMethodCode); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $availableMethod = $this->createMock(PaymentMethodInterface::class); + $availableMethod->method('getCode')->willReturn($paymentMethodCode); + $availablePaymentMethods = [$availableMethod]; + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn($availablePaymentMethods); + + $expectedException = new NoSuchEntityException(__('Cart does not exist')); + $this->cartManagementMock->method('placeOrder') + ->with($cartId, $this->paymentInterfaceMock) + ->willThrowException($expectedException); + + $this->expectException(NoSuchEntityException::class); + $this->expectExceptionMessage('Cart does not exist'); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } + + /** + * Test with empty string payment method code + */ + public function testExecuteWithEmptyPaymentMethodCode(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn(''); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $availableMethod = $this->createMock(PaymentMethodInterface::class); + $availableMethod->method('getCode')->willReturn('checkmo'); + $availablePaymentMethods = [$availableMethod]; + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn($availablePaymentMethods); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => '', + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => ['checkmo'] + ] + ); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage("The requested Payment Method 'unknown' is not available."); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } + + /** + * Test case sensitivity in payment method code comparison + */ + public function testExecuteWithCaseSensitivePaymentMethodCode(): void + { + $cartId = 123; + $maskedCartId = 'masked123'; + $userId = 456; + $paymentMethodCode = 'CheckMo'; // Different case + + $this->quoteMock->method('getId')->willReturn($cartId); + $this->quoteMock->method('getPayment')->willReturn($this->paymentMock); + + $this->paymentMock->method('getMethod')->willReturn($paymentMethodCode); + + $this->paymentManagementMock->method('get') + ->with($cartId) + ->willReturn($this->paymentInterfaceMock); + + $availableMethod = $this->createMock(PaymentMethodInterface::class); + $availableMethod->method('getCode')->willReturn('checkmo'); // lowercase + $availablePaymentMethods = [$availableMethod]; + + $this->paymentManagementMock->method('getList') + ->with($cartId) + ->willReturn($availablePaymentMethods); + + $this->loggerMock->expects($this->once()) + ->method('debug') + ->with( + 'Attempt to place order with disabled payment method', + [ + 'payment_method' => $paymentMethodCode, + 'cart_id' => $cartId, + 'user_id' => $userId, + 'available_methods' => ['checkmo'] + ] + ); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage("The requested Payment Method 'CheckMo' is not available."); + + $this->placeOrder->execute($this->quoteMock, $maskedCartId, $userId); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php index 4c83add80c4fd..485065f14b24d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/PlaceOrderTest.php @@ -245,6 +245,7 @@ public function testPlaceOrderWithNoItemsInCart() DataFixture(GuestCartFixture::class, ['reserved_order_id' => 'test_quote'], as: 'cart'), DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']), DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), ] public function testPlaceOrderWithNoShippingAddress() { @@ -257,8 +258,8 @@ public function testPlaceOrderWithNoShippingAddress() $exceptionData = $exception->getResponseData(); self::assertEquals(1, count($exceptionData['errors'])); self::assertEquals( - 'Unable to place order: Some addresses can\'t be used due to the' . - ' configurations for specific countries.', + 'Unable to place order: A server error stopped your order from being placed.' . + ' Please try to place your order again', $exceptionData['errors'][0]['message'] ); self::assertEquals( @@ -275,6 +276,7 @@ public function testPlaceOrderWithNoShippingAddress() DataFixture(SetGuestEmailFixture::class, ['cart_id' => '$cart.id$']), DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), ] public function testPlaceOrderWithNoShippingMethod() { @@ -287,7 +289,8 @@ public function testPlaceOrderWithNoShippingMethod() $exceptionData = $exception->getResponseData(); self::assertEquals(1, count($exceptionData['errors'])); self::assertEquals( - 'Unable to place order: The shipping method is missing. Select the shipping method and try again.', + 'Unable to place order: A server error stopped your order from being placed.' . + ' Please try to place your order again', $exceptionData['errors'][0]['message'] ); self::assertEquals( @@ -308,6 +311,7 @@ public function testPlaceOrderWithNoShippingMethod() DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']) ] public function testPlaceOrderWithNoBillingAddress() { @@ -321,11 +325,8 @@ public function testPlaceOrderWithNoBillingAddress() $exceptionData = $exception->getResponseData(); self::assertEquals(1, count($exceptionData['errors'])); self::assertEquals( - 'Unable to place order: Please check the billing address information. ' . - '"firstname" is required. Enter and try again. "lastname" is required. Enter and try again. ' . - '"street" is required. Enter and try again. "city" is required. Enter and try again. ' . - '"telephone" is required. Enter and try again. "postcode" is required. Enter and try again. ' . - '"countryId" is required. Enter and try again.', + 'Unable to place order: A server error stopped your order from being placed.' . + ' Please try to place your order again', $exceptionData['errors'][0]['message'] ); self::assertEquals( @@ -359,7 +360,8 @@ public function testPlaceOrderWithNoPaymentMethod() $exceptionData = $exception->getResponseData(); self::assertEquals(1, count($exceptionData['errors'])); self::assertEquals( - 'Unable to place order: Enter a valid payment method and try again.', + 'Unable to place order: A server error stopped your order from being placed.' . + ' Please try to place your order again', $exceptionData['errors'][0]['message'] ); self::assertEquals( @@ -382,6 +384,7 @@ public function testPlaceOrderWithNoPaymentMethod() DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), DataFixture( ProductStockFixture::class, [ @@ -403,7 +406,8 @@ public function testPlaceOrderWithOutOfStockProduct() $exceptionData = $exception->getResponseData(); self::assertEquals(1, count($exceptionData['errors'])); self::assertEquals( - 'Unable to place order: Some of the products are out of stock.', + 'Unable to place order: A server error stopped your order from being placed.' . + ' Please try to place your order again', $exceptionData['errors'][0]['message'] ); self::assertEquals( @@ -425,6 +429,7 @@ public function testPlaceOrderWithOutOfStockProduct() DataFixture(AddProductToCartFixture::class, ['cart_id' => '$cart.id$', 'product_id' => '$product.id$']), DataFixture(SetShippingAddressFixture::class, ['cart_id' => '$cart.id$']), DataFixture(SetBillingAddressFixture::class, ['cart_id' => '$cart.id$']), + DataFixture(SetPaymentMethodFixture::class, ['cart_id' => '$cart.id$']), DataFixture(SetDeliveryMethodFixture::class, ['cart_id' => '$cart.id$']), DataFixture( ProductStockFixture::class, @@ -447,7 +452,8 @@ public function testPlaceOrderWithOutOfStockProductWithDisabledInventoryCheck() $exceptionData = $exception->getResponseData(); self::assertEquals(1, count($exceptionData['errors'])); self::assertEquals( - 'Unable to place order: Enter a valid payment method and try again.', + 'Unable to place order: A server error stopped your order from being placed.' . + ' Please try to place your order again', $exceptionData['errors'][0]['message'] ); self::assertEquals(