Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PAYSHIP-3046] Order capture retry #1275

Merged
merged 20 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<module>
<name>ps_checkout</name>
<displayName><![CDATA[PrestaShop Checkout]]></displayName>
<version><![CDATA[8.4.2.1]]></version>
<version><![CDATA[8.4.2.2]]></version>
<description><![CDATA[Provide the most commonly used payment methods to your customers in this all-in-one module, and manage all your sales in a centralized interface.]]></description>
<author><![CDATA[PrestaShop]]></author>
<tab><![CDATA[payments_gateways]]></tab>
Expand Down
2 changes: 1 addition & 1 deletion config/cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ services:
- '@=service("PrestaShop\\ModuleLibCacheDirectoryProvider\\Cache\\CacheDirectoryProvider").getPath()'

ps_checkout.cache.paypal.order:
class: 'Symfony\Component\Cache\Simple\ChainCache'
class: 'PrestaShop\Module\PrestashopCheckout\PayPal\Order\Cache\PayPalOrderCache'
public: true
arguments:
- [
Expand Down
88 changes: 61 additions & 27 deletions controllers/front/payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
use PrestaShop\Module\PrestashopCheckout\PayPal\Order\Command\CapturePayPalOrderCommand;
use PrestaShop\Module\PrestashopCheckout\PayPal\Order\Entity\PayPalOrder;
use PrestaShop\Module\PrestashopCheckout\PayPal\Order\Exception\PayPalOrderException;
use PrestaShop\Module\PrestashopCheckout\PayPal\Order\Query\GetPayPalOrderForCheckoutCompletedQuery;
use PrestaShop\Module\PrestashopCheckout\PayPal\Order\Query\GetPayPalOrderForCheckoutCompletedQueryResult;
use PrestaShop\Module\PrestashopCheckout\PayPal\Order\ValueObject\PayPalOrderId;
use PrestaShop\Module\PrestashopCheckout\PayPal\PayPalOrderProvider;
use PrestaShop\Module\PrestashopCheckout\Repository\PaymentTokenRepository;
use PrestaShop\Module\PrestashopCheckout\Repository\PayPalOrderRepository;

Expand All @@ -48,9 +49,14 @@ class Ps_CheckoutPaymentModuleFrontController extends AbstractFrontController
*/
private $paypalOrderId;

/**
* @var CommandBusInterface
*/
private $commandBus;

public function checkAccess()
{
return $this->context->customer && $this->context->customer->isLogged() && $this->context->cart;
return $this->context->customer && $this->context->cart;
seiwan marked this conversation as resolved.
Show resolved Hide resolved
}

public function initContent()
Expand Down Expand Up @@ -79,33 +85,41 @@ public function postProcess()
try {
$this->paypalOrderId = new PayPalOrderId($orderId);

$this->commandBus = $this->module->getService('ps_checkout.bus.command');

/** @var PayPalOrderRepository $payPalOrderRepository */
$payPalOrderRepository = $this->module->getService(PayPalOrderRepository::class);
/** @var PayPalOrderProvider $payPalOrderProvider */
$payPalOrderProvider = $this->module->getService(PayPalOrderProvider::class);
/** @var CommandBusInterface $commandBus */
$commandBus = $this->module->getService('ps_checkout.bus.command');
/** @var Psr\SimpleCache\CacheInterface $payPalOrderCache */
$payPalOrderCache = $this->module->getService('ps_checkout.cache.paypal.order');

$payPalOrder = $payPalOrderRepository->getPayPalOrderById($this->paypalOrderId);

$orders = new PrestaShopCollection(Order::class);
$orders->where('id_cart', '=', $payPalOrder->getIdCart());

if ($orders->count()) {
if ($this->context->customer->isLogged()) {
$this->redirectToOrderHistoryPage();
} else {
$payPalOrderQueryResult = $this->getPayPalOrder($orderId);
$payPalOrderFromCache = $payPalOrderQueryResult->getPayPalOrder();

$this->redirectToOrderConfirmationPage($payPalOrder->getIdCart(), $payPalOrderFromCache['purchase_units'][0]['payments']['captures'][0]['id'], $payPalOrderFromCache['status']);
}
}

if ($payPalOrder->getIdCart() !== $this->context->cart->id) {
throw new Exception('PayPal order does not belong to this customer');
$this->redirectToOrderPage();
}

$payPalOrderFromCache = $payPalOrderProvider->getById($payPalOrder->getId()->getValue());
$payPalOrderQueryResult = $this->getPayPalOrder($orderId);
$payPalOrderFromCache = $payPalOrderQueryResult->getPayPalOrder();

if ($payPalOrderFromCache['status'] === 'COMPLETED') {
$this->createOrder($payPalOrderFromCache, $payPalOrder);
}

if ($payPalOrderFromCache['status'] === 'PAYER_ACTION_REQUIRED') {
// Delete from cache so when user is redirected from 3DS authentication page the order is fetched from PayPal
if ($payPalOrderCache->has($this->paypalOrderId->getValue())) {
$payPalOrderCache->delete($this->paypalOrderId->getValue());
}

$this->redirectTo3DSVerification($payPalOrderFromCache);
}

Expand All @@ -117,15 +131,21 @@ public function postProcess()
$this->redirectTo3DSVerification($payPalOrderFromCache);
break;
case Card3DSecure::PROCEED:
$commandBus->handle(new CapturePayPalOrderCommand($this->paypalOrderId->getValue(), array_keys($payPalOrderFromCache['payment_source'])[0]));
$payPalOrderFromCache = $payPalOrderCache->get($this->paypalOrderId->getValue());
$this->commandBus->handle(new CapturePayPalOrderCommand($orderId, array_keys($payPalOrderFromCache['payment_source'])[0]));
$payPalOrderFromCache = $payPalOrderCache->get($orderId);
$this->createOrder($payPalOrderFromCache, $payPalOrder);
break;
case Card3DSecure::NO_DECISION:
default:
break;
}
}

if ($payPalOrderFromCache['status'] === 'APPROVED') {
$this->commandBus->handle(new CapturePayPalOrderCommand($orderId, array_keys($payPalOrderFromCache['payment_source'])[0]));
$payPalOrderFromCache = $payPalOrderCache->get($orderId);
$this->createOrder($payPalOrderFromCache, $payPalOrder);
}
} catch (Exception $exception) {
$this->context->smarty->assign('error', $exception->getMessage());
}
Expand All @@ -143,19 +163,14 @@ public function postProcess()
*/
private function createOrder($payPalOrderFromCache, $payPalOrder)
{
/** @var CommandBusInterface $commandBus */
$commandBus = $this->module->getService('ps_checkout.bus.command');

$capture = $payPalOrderFromCache['purchase_units'][0]['payments']['captures'][0];
if ($capture['status'] === 'COMPLETED') {
$commandBus->handle(new CreateOrderCommand($payPalOrder->getId()->getValue(), $capture));
if ($payPalOrder->getPaymentTokenId() && $payPalOrder->checkCustomerIntent(PayPalOrder::CUSTOMER_INTENT_FAVORITE)) {
/** @var PaymentTokenRepository $paymentTokenRepository */
$paymentTokenRepository = $this->module->getService(PaymentTokenRepository::class);
$paymentTokenRepository->setTokenFavorite($payPalOrder->getPaymentTokenId());
}
$this->redirectToOrderConfirmationPage($payPalOrder->getIdCart(), $capture['id'], $payPalOrderFromCache['status']);
$capture = isset($payPalOrderFromCache['purchase_units'][0]['payments']['captures'][0]) ? $payPalOrderFromCache['purchase_units'][0]['payments']['captures'][0] : null;
$this->commandBus->handle(new CreateOrderCommand($payPalOrder->getId()->getValue(), $capture));
if ($payPalOrder->getPaymentTokenId() && $payPalOrder->checkCustomerIntent(PayPalOrder::CUSTOMER_INTENT_FAVORITE)) {
/** @var PaymentTokenRepository $paymentTokenRepository */
$paymentTokenRepository = $this->module->getService(PaymentTokenRepository::class);
$paymentTokenRepository->setTokenFavorite($payPalOrder->getPaymentTokenId());
}
$this->redirectToOrderConfirmationPage($payPalOrder->getIdCart(), $capture ? $capture['id'] : null, $payPalOrderFromCache['status']);
}

/**
Expand Down Expand Up @@ -218,4 +233,23 @@ private function redirectToOrderConfirmationPage($cartId, $captureId, $payPalOrd
));
}
}

/**
* @param string $orderId
*
* @return GetPayPalOrderForCheckoutCompletedQueryResult
*
* @throws PayPalOrderException
*/
private function getPayPalOrder($orderId)
{
$payPalOrderQuery = new GetPayPalOrderForCheckoutCompletedQuery($orderId);

return $this->commandBus->handle($payPalOrderQuery);
}

private function redirectToOrderHistoryPage()
{
Tools::redirect($this->context->link->getPageLink('history'));
}
}
3 changes: 3 additions & 0 deletions controllers/front/validate.php
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ private function handleException(Exception $exception)
'body' => [
'error' => [
'message' => $exceptionMessageForCustomer,
'code' => (int) $exception->getCode() < 400 && $exception->getPrevious() !== null
? (int) $exception->getPrevious()->getCode()
: (int) $exception->getCode(),
],
],
'exceptionCode' => $exception->getCode(),
Expand Down
5 changes: 3 additions & 2 deletions ps_checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class Ps_checkout extends PaymentModule

// Needed in order to retrieve the module version easier (in api call headers) than instanciate
// the module each time to get the version
const VERSION = '8.4.2.1';
const VERSION = '8.4.2.2';

const INTEGRATION_DATE = '2024-04-01';

Expand All @@ -137,7 +137,7 @@ public function __construct()

// We cannot use the const VERSION because the const is not computed by addons marketplace
// when the zip is uploaded
$this->version = '8.4.2.1';
$this->version = '8.4.2.2';
$this->author = 'PrestaShop';
$this->currencies = true;
$this->currencies_mode = 'checkbox';
Expand Down Expand Up @@ -1110,6 +1110,7 @@ public function hookActionFrontControllerSetMedia()
'checkout.form.error.label' => $this->l('There was an error during the payment. Please try again or contact the support.'),
'loader-component.label.header' => $this->l('Thanks for your purchase!'),
'loader-component.label.body' => $this->l('Please wait, we are processing your payment'),
'loader-component.label.body.longer' => $this->l('This is taking longer than expected. Please wait...'),
'error.paypal-sdk.contingency.cancel' => $this->l('Card holder authentication canceled, please choose another payment method or try again.'),
'error.paypal-sdk.contingency.error' => $this->l('An error occurred on card holder authentication, please choose another payment method or try again.'),
'error.paypal-sdk.contingency.failure' => $this->l('Card holder authentication failed, please choose another payment method or try again.'),
Expand Down
2 changes: 1 addition & 1 deletion src/Checkout/CheckoutChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function __construct(LoggerInterface $logger)
public function continueWithAuthorization($cartId, $orderPayPal)
{
if ($orderPayPal['status'] === 'COMPLETED') {
throw new PsCheckoutException(sprintf('PayPal Order %s is already captured', $orderPayPal['id']));
throw new PsCheckoutException(sprintf('PayPal Order %s is already captured', $orderPayPal['id']), PsCheckoutException::PAYPAL_ORDER_ALREADY_CAPTURED);
}

$paymentSource = isset($orderPayPal['payment_source']) ? key($orderPayPal['payment_source']) : '';
Expand Down
52 changes: 35 additions & 17 deletions src/Checkout/EventSubscriber/CheckoutEventSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,36 +118,46 @@ public function updatePaymentMethodSelected(CheckoutCompletedEvent $event)
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
* @throws PsCheckoutException
* @throws HttpTimeoutException
*/
public function proceedToPayment(CheckoutCompletedEvent $event)
{
$payPalOrderId = $event->getPayPalOrderId()->getValue();

/** @var GetPayPalOrderForCheckoutCompletedQueryResult $getPayPalOrderForCheckoutCompletedQueryResult */
$getPayPalOrderForCheckoutCompletedQueryResult = $this->commandBus->handle(new GetPayPalOrderForCheckoutCompletedQuery(
$payPalOrderId
));

$payPalOrder = $getPayPalOrderForCheckoutCompletedQueryResult->getPayPalOrder();

try {
/** @var GetPayPalOrderForCheckoutCompletedQueryResult $getPayPalOrderForCheckoutCompletedQueryResult */
$getPayPalOrderForCheckoutCompletedQueryResult = $this->commandBus->handle(new GetPayPalOrderForCheckoutCompletedQuery(
$event->getPayPalOrderId()->getValue()
));
} catch (HttpTimeoutException $exception) {
$this->commandBus->handle(new CreateOrderCommand($event->getPayPalOrderId()->getValue()));

return;
}
$this->checkoutChecker->continueWithAuthorization($event->getCartId()->getValue(), $payPalOrder);
} catch (PsCheckoutException $exception) {
if ($exception->getCode() === PsCheckoutException::PAYPAL_ORDER_ALREADY_CAPTURED) {
$capture = isset($payPalOrder['purchase_units'][0]['payments']['captures'][0]) ? $payPalOrder['purchase_units'][0]['payments']['captures'][0] : null;
$this->commandBus->handle(new CreateOrderCommand($payPalOrderId, $capture));

$this->checkoutChecker->continueWithAuthorization($event->getCartId()->getValue(), $getPayPalOrderForCheckoutCompletedQueryResult->getPayPalOrder());
return;
} else {
throw $exception;
}
}

try {
$this->commandBus->handle(
new CapturePayPalOrderCommand(
$event->getPayPalOrderId()->getValue(),
$payPalOrderId,
$event->getFundingSource()
)
);
} catch (PayPalException $exception) {
if ($exception->getCode() === PayPalException::ORDER_NOT_APPROVED) {
$this->commandBus->handle(new CreateOrderCommand($event->getPayPalOrderId()->getValue()));
$this->commandBus->handle(new CreateOrderCommand($payPalOrderId));

return;
} elseif ($exception->getCode() === PayPalException::RESOURCE_NOT_FOUND) {
$psCheckoutCart = $this->psCheckoutCartRepository->findOneByPayPalOrderId($event->getPayPalOrderId()->getValue());
$psCheckoutCart = $this->psCheckoutCartRepository->findOneByPayPalOrderId($payPalOrderId);

if (Validate::isLoadedObject($psCheckoutCart)) {
$psCheckoutCart->paypal_status = PsCheckoutCart::STATUS_CANCELED;
Expand All @@ -156,14 +166,22 @@ public function proceedToPayment(CheckoutCompletedEvent $event)

throw $exception;
} elseif ($exception->getCode() === PayPalException::ORDER_ALREADY_CAPTURED) {
if (isset($payPalOrder['purchase_units'][0]['payments']['captures'][0])) {
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
$capture = $payPalOrder['purchase_units'][0]['payments']['captures'][0];
} else {
$payPalOrderQuery = new GetPayPalOrderForCheckoutCompletedQuery($payPalOrderId);

/** @var GetPayPalOrderForCheckoutCompletedQueryResult $getPayPalOrderForCheckoutCompletedQueryResult */
$getPayPalOrderForCheckoutCompletedQueryResult = $this->commandBus->handle($payPalOrderQuery);
$payPalOrder = $getPayPalOrderForCheckoutCompletedQueryResult->getPayPalOrder();
$capture = isset($payPalOrder['purchase_units'][0]['payments']['captures'][0]) ? $payPalOrder['purchase_units'][0]['payments']['captures'][0] : null;
}
$this->commandBus->handle(new CreateOrderCommand($payPalOrderId, $capture));

return;
} else {
throw $exception;
}
} catch (HttpTimeoutException $exception) {
$this->commandBus->handle(new CreateOrderCommand($event->getPayPalOrderId()->getValue()));

return;
}
}
}
2 changes: 2 additions & 0 deletions src/Exception/PsCheckoutException.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,6 @@ class PsCheckoutException extends \Exception
const CART_ADDRESS_DELIVERY_INVALID = 57;
const CART_DELIVERY_OPTION_INVALID = 58;
const PSCHECKOUT_HTTP_UNAUTHORIZED = 59;

const PAYPAL_ORDER_ALREADY_CAPTURED = 60;
}
52 changes: 52 additions & 0 deletions src/PayPal/Order/Cache/PayPalOrderCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License version 3.0
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so we can send you a copy immediately.
*
* @author PrestaShop SA and Contributors <[email protected]>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
*/

namespace PrestaShop\Module\PrestashopCheckout\PayPal\Order\Cache;

use PsCheckoutCart;
use Symfony\Component\Cache\Simple\ChainCache;

class PayPalOrderCache extends ChainCache
{
const CACHE_TTL = [
PsCheckoutCart::STATUS_CREATED => 600,
PsCheckoutCart::STATUS_PAYER_ACTION_REQUIRED => 600,
PsCheckoutCart::STATUS_APPROVED => 600,
PsCheckoutCart::STATUS_VOIDED => 3600,
PsCheckoutCart::STATUS_SAVED => 3600,
PsCheckoutCart::STATUS_CANCELED => 3600,
PsCheckoutCart::STATUS_COMPLETED => 3600,
];

/**
* {@inheritdoc}
*
* @return bool
*/
public function set($key, $value, $ttl = null)
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
{
if (!$ttl && isset($value['status']) && isset(self::CACHE_TTL[$value['status']])) {
$ttl = self::CACHE_TTL[$value['status']];
}

return parent::set($key, $value, $ttl);
}
}
28 changes: 28 additions & 0 deletions src/PayPal/Order/Cache/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License version 3.0
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to [email protected] so we can send you a copy immediately.
*
* @author PrestaShop SA and Contributors <[email protected]>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');

header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');

header('Location: ../');
exit;
Loading
Loading