Skip to content

Commit

Permalink
Improve Authorize and Place API Endpoint (#266)
Browse files Browse the repository at this point in the history
* Assert payment details are set on successful order

* Assign order from response to reusable variable

* Move transaction data to order payment additional info

* Assert transaction details are set on successful order

* Fix incorrect comment to ignore "FoundFinal" standard

* Fix incorrect logic for closing transaction

* Refactor logic for building order payload to method

* Fix incorrect method return type annotation

* Fix incorrect transaction error array shape annotations

* Fix incorrect method used to get order identifier

* Suppress static analysis violations

* Suppress missing method comment coding standard error

* Assert checkout session is updated for successful order

* Use contract instead of concrete implementation

* Verify that error is returned if quote doesn't exist

* Ensure quote is always set for tests
  • Loading branch information
JosephLeedy authored Jun 10, 2024
1 parent d3a08d1 commit 83fec9c
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 52 deletions.
133 changes: 85 additions & 48 deletions Model/Order/PlaceOrder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -110,7 +110,7 @@ public function __construct(
CreateOrderFromPayload $createOrderFromPayload,
ProcessOrder $processOrder,
Progress $progress,
MaskedQuoteIdToQuoteId $maskedQuoteIdToQuoteId,
MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId,
LoadAndValidate $loadAndValidate,
StoreManagerInterface $storeManager,
ClientInterface $client,
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -426,7 +404,7 @@ private function getValidationErrorResponse(string $message): ResultInterface
}

/**
* @return null|array{
* @return array{
* data?: array{
* total: int,
* transactions: array{
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -471,7 +449,7 @@ private function getValidationErrorResponse(string $message): ResultInterface
* },
* gateway_response_data: string[]
* }[]
* }
* }[]
* }
* @throws Exception
*/
Expand All @@ -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;
}
}
84 changes: 80 additions & 4 deletions Test/Integration/Model/Order/PlaceOrderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 83fec9c

Please sign in to comment.