diff --git a/Model/Order/PlaceOrder.php b/Model/Order/PlaceOrder.php index 90f1678..8333ef7 100644 --- a/Model/Order/PlaceOrder.php +++ b/Model/Order/PlaceOrder.php @@ -23,7 +23,7 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\MaskedQuoteIdToQuoteId; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Api\Data\OrderPaymentInterface; use Magento\Sales\Api\Data\OrderPaymentInterfaceFactory; @@ -84,7 +84,7 @@ class PlaceOrder implements PlaceOrderInterface * @var LoadAndValidate */ private $loadAndValidate; - private MaskedQuoteIdToQuoteId $maskedQuoteIdToQuoteId; + private MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId; private StoreManagerInterface $storeManager; private ClientInterface $client; private OrderDataInterfaceFactory $orderDataFactory; @@ -110,7 +110,7 @@ public function __construct( CreateOrderFromPayload $createOrderFromPayload, ProcessOrder $processOrder, Progress $progress, - MaskedQuoteIdToQuoteId $maskedQuoteIdToQuoteId, + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, LoadAndValidate $loadAndValidate, StoreManagerInterface $storeManager, ClientInterface $client, @@ -237,12 +237,20 @@ public function authorizeAndPlace(string $publicOrderId, string $quoteMaskId): R * code?: string, * transactions?: array{ * gateway: string, - * gateway_id: string, + * payment_id: string, * amount: int, * transaction_id: string, - * reference_transaction_id: string|null, - * response_code: string, - * status: 'success'|'failure' + * currency: string, + * step: string, + * status: 'success'|'failed'|'', + * tender_type: string, + * tender_details: array{ + * brand: string, + * last_four: string, + * bin: string, + * expiration: string + * }, + * gateway_response_data: string[] * }[] * } $error */ @@ -290,7 +298,7 @@ function (array $error): ErrorInterface { * transaction_id: string, * currency: string, * step: string, - * status: 'success'|'failure'|'', + * status: 'success'|'failed'|'', * tender_type: string, * tender_details: array{ * brand: string, @@ -343,37 +351,7 @@ function (array $error): ErrorInterface { * } $firstTransaction */ $firstTransaction = array_shift($transactions); - /** @var OrderPaymentInterface $orderPayment */ - $orderPayment = $this->paymentFactory->create(); - /** @var TransactionInterface $transaction */ - $transaction = $this->transactionFactory->create(); - /** @var OrderDataInterface $orderData */ - $orderData = $this->orderDataFactory->create(); - [$cardExpirationMonth, $cardExpirationYear] = explode( - '/', - $firstTransaction['tender_details']['expiration'], - 2 - ); - - $orderPayment->setBaseAmountPaid($firstTransaction['amount'] / 100); - $orderPayment->setAmountPaid($firstTransaction['amount'] / 100); - $orderPayment->setCcLast4($firstTransaction['tender_details']['last_four']); - $orderPayment->setCcType($firstTransaction['tender_details']['brand']); - $orderPayment->setCcExpMonth($cardExpirationMonth); - $orderPayment->setCcExpYear($cardExpirationYear); - - $transaction->setTxnId($firstTransaction['transaction_id']); - $transaction->setTxnType(TransactionInterface::TYPE_PAYMENT); // TODO: verify this transaction type is correct - /** @noinspection PhpUnhandledExceptionInspection */ - $transaction->setAdditionalInformation('gateway', $firstTransaction['gateway']); - /** @noinspection PhpUnhandledExceptionInspection */ - $transaction->setAdditionalInformation('payment_id', $firstTransaction['payment_id']); - $transaction->setIsClosed(1); - - $orderData->setQuoteId((int)$quoteId); - $orderData->setPublicId($publicOrderId); - $orderData->setPayment($orderPayment); - $orderData->setTransaction($transaction); + $orderData = $this->buildOrderData($firstTransaction, (int)$quoteId, $publicOrderId); try { $order = $this->createOrderFromPayload->createOrder($orderData, $quote); @@ -426,7 +404,7 @@ private function getValidationErrorResponse(string $message): ResultInterface } /** - * @return null|array{ + * @return array{ * data?: array{ * total: int, * transactions: array{ @@ -436,7 +414,7 @@ private function getValidationErrorResponse(string $message): ResultInterface * transaction_id: string, * currency: string, * step: string, - * status: 'success'|'failure'|'', + * status: 'success'|'failed'|'', * tender_type: string, * tender_details: array{ * brand: string, @@ -461,7 +439,7 @@ private function getValidationErrorResponse(string $message): ResultInterface * transaction_id: string, * currency: string, * step: string, - * status: 'success'|'failure'|'', + * status: 'success'|'failed'|'', * tender_type: string, * tender_details: array{ * brand: string, @@ -471,7 +449,7 @@ private function getValidationErrorResponse(string $message): ResultInterface * }, * gateway_response_data: string[] * }[] - * } + * }[] * } * @throws Exception */ @@ -495,12 +473,71 @@ private function getAuthorizedPayments(string $publicOrderId, int $websiteId): a return array_merge($result->getBody(), ['errors' => $errors]); } + // phpcs:ignore Magento2.Annotation.MethodAnnotationStructure.NoCommentBlock private function updateCheckoutSession(CartInterface $quote, OrderInterface $order): void { - $this->checkoutSession->setLastQuoteId($quote->getId()); - $this->checkoutSession->setLastSuccessQuoteId($quote->getId()); - $this->checkoutSession->setLastOrderId($order->getId()); - $this->checkoutSession->setLastRealOrderId($order->getIncrementId()); - $this->checkoutSession->setLastOrderStatus($order->getStatus()); + $this->checkoutSession->setLastQuoteId($quote->getId()); // @phpstan-ignore method.notFound + $this->checkoutSession->setLastSuccessQuoteId($quote->getId()); // @phpstan-ignore method.notFound + $this->checkoutSession->setLastOrderId($order->getEntityId()); // @phpstan-ignore method.notFound + $this->checkoutSession->setLastRealOrderId($order->getIncrementId()); // @phpstan-ignore method.notFound + $this->checkoutSession->setLastOrderStatus($order->getStatus()); // @phpstan-ignore method.notFound + } + + /** + * @param array{ + * gateway: string, + * payment_id: string, + * amount: int, + * transaction_id: string, + * currency: string, + * step: string, + * status: 'success'|'', + * tender_type: string, + * tender_details: array{ + * brand: string, + * last_four: string, + * bin: string, + * expiration: string + * }, + * gateway_response_data: string[] + * } $firstTransaction + */ + private function buildOrderData(array $firstTransaction, int $quoteId, string $publicOrderId): OrderDataInterface + { + /** @var OrderPaymentInterface $orderPayment */ + $orderPayment = $this->paymentFactory->create(); + /** @var TransactionInterface $transaction */ + $transaction = $this->transactionFactory->create(); + /** @var OrderDataInterface $orderData */ + $orderData = $this->orderDataFactory->create(); + [$cardExpirationMonth, $cardExpirationYear] = explode( + '/', + $firstTransaction['tender_details']['expiration'], + 2 + ); + + $orderPayment->setBaseAmountPaid($firstTransaction['amount'] / 100); + $orderPayment->setAmountPaid($firstTransaction['amount'] / 100); + $orderPayment->setCcLast4($firstTransaction['tender_details']['last_four']); + $orderPayment->setCcType($firstTransaction['tender_details']['brand']); + $orderPayment->setCcExpMonth($cardExpirationMonth); + $orderPayment->setCcExpYear($cardExpirationYear); + $orderPayment->setAdditionalInformation( + [ + 'transaction_gateway' => $firstTransaction['gateway'], + 'transaction_payment_id' => $firstTransaction['payment_id'] + ] + ); + $orderPayment->setIsTransactionClosed(true); // @phpstan-ignore method.notFound + + $transaction->setTxnId($firstTransaction['transaction_id']); + $transaction->setTxnType(TransactionInterface::TYPE_PAYMENT); // TODO: verify this transaction type is correct + + $orderData->setQuoteId($quoteId); + $orderData->setPublicId($publicOrderId); + $orderData->setPayment($orderPayment); + $orderData->setTransaction($transaction); + + return $orderData; } } diff --git a/Test/Integration/Model/Order/PlaceOrderTest.php b/Test/Integration/Model/Order/PlaceOrderTest.php index 80009d2..1d46f1c 100644 --- a/Test/Integration/Model/Order/PlaceOrderTest.php +++ b/Test/Integration/Model/Order/PlaceOrderTest.php @@ -11,20 +11,25 @@ use Bold\Checkout\Model\Order\PlaceOrder; use Bold\Checkout\Model\Quote\LoadAndValidate; use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; +use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderPaymentInterface; +use Magento\Sales\Model\Order\Payment; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; use function reset; -final class PlaceOrderTest extends TestCase // phpcs-ignore: Magento2.PHP.FinalImplementation.FoundFinal +final class PlaceOrderTest extends TestCase // phpcs:ignore Magento2.PHP.FinalImplementation.FoundFinal { use ArraySubsetAsserts; - private CartInterface|null $quote; + private CartInterface $quote; /** * @magentoDataFixture Magento/Checkout/_files/quote_with_shipping_method.php @@ -88,8 +93,35 @@ public function testAuthorizesAndPlacesOrderSuccessfully(): void $this->getQuoteMaskId() ); + /** @var OrderInterface|null $order */ + $order = $response->getOrder(); + /** @var OrderPaymentInterface|Payment $payment */ + $payment = $order?->getPayment() ?? $objectManager->create(OrderPaymentInterface::class); + /** @var CheckoutSession $checkoutSession */ + $checkoutSession = $objectManager->get(CheckoutSession::class); + self::assertEmpty($response->getErrors()); - self::assertNotNull($response->getOrder()); + self::assertNotNull($order); + self::assertSame(30, $payment->getBaseAmountPaid()); + self::assertSame(30, $payment->getAmountPaid()); + self::assertSame('0009', $payment->getCcLast4()); + self::assertSame('Discover', $payment->getCcType()); + self::assertSame('04', $payment->getCcExpMonth()); + self::assertSame('2030', $payment->getCcExpYear()); + self::assertArraySubset( + [ + 'transaction_gateway' => 'Test Payment Gateway', + 'transaction_payment_id' => 'ff2e05a2-04c7-4db3-9a3d-c15f5dcca7fe' + ], + $payment->getAdditionalInformation() + ); + self::assertSame('b9f35c91-1c16-4a3e-a985-a6a1af44c0ac', $payment->getLastTransId()); + self::assertTrue($payment->getIsTransactionClosed()); // @phpstan-ignore method.notFound + self::assertSame($order->getQuoteId(), $checkoutSession->getLastQuoteId()); + self::assertSame($order->getQuoteId(), $checkoutSession->getLastSuccessQuoteId()); + self::assertSame($order->getEntityId(), $checkoutSession->getLastOrderId()); + self::assertSame($order->getIncrementId(), $checkoutSession->getLastRealOrderId()); + self::assertSame($order->getStatus(), $checkoutSession->getLastOrderStatus()); } public function testDoesNotAuthorizeAndPlaceSuccessfullyIfQuoteMaskIdIsInvalid(): void @@ -126,6 +158,50 @@ public function testDoesNotAuthorizeAndPlaceSuccessfullyIfQuoteMaskIdIsInvalid() self::assertNull($response->getOrder()); } + public function testDoesNotAuthorizeAndPlaceSuccessfullyIfQuotDoesNotExist(): void + { + $configMock = $this->createMock(ConfigInterface::class); + $maskedQuoteIdToQuoteIdMock = $this->createMock(MaskedQuoteIdToQuoteIdInterface::class); + $loadAndValidateMock = $this->createMock(LoadAndValidate::class); + $boldCheckoutApiClientMock = $this->createMock(ClientInterface::class); + $objectManager = Bootstrap::getObjectManager(); + $placeOrderService = $objectManager->create( + PlaceOrder::class, + [ + 'config' => $configMock, + 'maskedQuoteIdToQuoteId' => $maskedQuoteIdToQuoteIdMock, + 'loadAndValidate' => $loadAndValidateMock, + 'client' => $boldCheckoutApiClientMock, + ] + ); + $publicOrderId = 'fe90e903-e327-4ff4-ad31-c22529e33e50'; + $quoteMaskId = '22b2a1667c47450ea14d7d435fc2b087'; + $expectedErrorData = [ + 'message' => 'Could not find quote with ID "42"', + 'code' => 422, + 'type' => 'server.validation_error' + ]; + + $configMock->method('getShopId') + ->willReturn('74e51be84d1643e8a89df356b80bf2b5'); + + $maskedQuoteIdToQuoteIdMock->method('execute') + ->willReturn(42); + + $loadAndValidateMock->method('load') + ->willReturn($objectManager->create(CartInterface::class)); + + $response = $placeOrderService->authorizeAndPlace($publicOrderId, $quoteMaskId); + $actualErrorData = [ + 'code' => $response->getErrors()[0]->getCode(), + 'message' => $response->getErrors()[0]->getMessage(), + 'type' => $response->getErrors()[0]->getType() + ]; + + self::assertEquals($expectedErrorData, $actualErrorData); + self::assertNull($response->getOrder()); + } + /** * @magentoDataFixture Magento/Checkout/_files/quote_with_shipping_method.php * @dataProvider boldAuthorizedPaymentsApiResultDataProvider @@ -286,7 +362,7 @@ private function getQuote(): CartInterface $quotes = $objectManager->create(CartRepositoryInterface::class) ->getList($searchCriteria) ->getItems(); - $this->quote = reset($quotes); + $this->quote = reset($quotes) ?: $objectManager->create(CartInterface::class); return $this->quote; }