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

Capture retry + negative testing #1277

Open
wants to merge 38 commits into
base: prestashop/8.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
af9f9a9
Added negative testing headers
L3RAZ Sep 13, 2024
e4ad9a9
Added redirect to payment page after all capture retries fail
L3RAZ Oct 9, 2024
70232f4
added order creation
L3RAZ Oct 14, 2024
bbef44f
Added exception code for errors
L3RAZ Oct 14, 2024
bc9da5a
Added retry in maasland http client
L3RAZ Oct 15, 2024
ac77090
Removed wrong imports
L3RAZ Oct 16, 2024
84eec51
Removed retries from MaaslandHttpClient and added custom ttl for PayP…
L3RAZ Oct 23, 2024
fbe47b8
CS fixes
L3RAZ Oct 23, 2024
5e911aa
Removed redundant test
L3RAZ Oct 23, 2024
17d1d0d
CS fix
L3RAZ Oct 23, 2024
cc3d1ad
Updated exception code handling for ajax requests
L3RAZ Oct 25, 2024
65b0995
Added translation for changed loader
L3RAZ Oct 29, 2024
5a11dca
Merge branch 'feat/PAYSHIP-3020' into spike/capture-retry
L3RAZ Oct 29, 2024
d66b322
Merge branch 'prestashop/8.x' into spike/capture-retry
L3RAZ Oct 29, 2024
059a4d3
Updated PayPal order fetching to check date_upd
L3RAZ Oct 30, 2024
08c775f
Added redirect to payment page after all capture retries fail
L3RAZ Oct 9, 2024
ebb380e
added order creation
L3RAZ Oct 14, 2024
0e3a43c
Added exception code for errors
L3RAZ Oct 14, 2024
ce44782
Added retry in maasland http client
L3RAZ Oct 15, 2024
4724aea
Removed wrong imports
L3RAZ Oct 16, 2024
2782adf
Removed retries from MaaslandHttpClient and added custom ttl for PayP…
L3RAZ Oct 23, 2024
50d2bd2
CS fixes
L3RAZ Oct 23, 2024
a036267
Removed redundant test
L3RAZ Oct 23, 2024
d76bfdc
Updated exception code handling for ajax requests
L3RAZ Oct 25, 2024
3999f9e
Added translation for changed loader
L3RAZ Oct 29, 2024
5e0341f
Added order status check before fetch
L3RAZ Oct 30, 2024
a7b2026
Added redirects from payment controller
L3RAZ Oct 30, 2024
87f1481
Fixed payment controller access and redirection
L3RAZ Oct 31, 2024
f3d39da
Merge branch 'spike/PAYSHIP-3046' into spike/capture-retry
L3RAZ Oct 31, 2024
065bee5
Changed the wrong Query to correct one
L3RAZ Nov 4, 2024
66d463f
Fixed incorrect order status issues
L3RAZ Nov 4, 2024
c473798
Removed capture status check in payment controller
L3RAZ Nov 5, 2024
6948784
Merge branch 'spike/PAYSHIP-3046' into spike/capture-retry
L3RAZ Nov 6, 2024
84d9f10
Fixed review notes
L3RAZ Nov 8, 2024
acc1a46
CS fixes
L3RAZ Nov 12, 2024
ae385d4
Merge branch 'spike/PAYSHIP-3046' into spike/capture-retry
L3RAZ Nov 12, 2024
4dfa5cd
Fixed redirect to guest customer
L3RAZ Nov 12, 2024
ee501e2
Fix for strange behavior when PayPal order creation fails
L3RAZ Nov 12, 2024
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
1 change: 1 addition & 0 deletions config/http-clients.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ services:
public: true
arguments:
- "@ps_checkout.http.client"
- '@PrestaShop\Module\PrestashopCheckout\Configuration\PrestaShopConfiguration'

PrestaShop\Module\PrestashopCheckout\Http\CheckoutHttpClient:
class: 'PrestaShop\Module\PrestashopCheckout\Http\CheckoutHttpClient'
Expand Down
1 change: 1 addition & 0 deletions config/query-handlers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ services:
public: true
arguments:
- "@ps_checkout.cache.paypal.order"
- '@PrestaShop\Module\PrestashopCheckout\Repository\PsCheckoutCartRepository'

PrestaShop\Module\PrestashopCheckout\PayPal\Order\QueryHandler\GetPayPalOrderForOrderConfirmationQueryHandler:
class: 'PrestaShop\Module\PrestashopCheckout\PayPal\Order\QueryHandler\GetPayPalOrderForOrderConfirmationQueryHandler'
Expand Down
87 changes: 60 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;
}

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,22 @@ 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])) {
$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;
}
Loading