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 16 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
52 changes: 32 additions & 20 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 @@ -50,7 +51,7 @@ class Ps_CheckoutPaymentModuleFrontController extends AbstractFrontController

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 @@ -81,31 +82,33 @@ public function postProcess()

/** @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()) {
$this->redirectToOrderHistoryPage();
}

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());
/** @var GetPayPalOrderForCheckoutCompletedQueryResult $payPalOrderQueryResult */
$payPalOrderQueryResult = $commandBus->handle(new GetPayPalOrderForCheckoutCompletedQuery($this->paypalOrderId->getValue()));
$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 @@ -126,6 +129,12 @@ public function postProcess()
break;
}
}

if ($payPalOrderFromCache['status'] === 'APPROVED') {
$commandBus->handle(new CapturePayPalOrderCommand($this->paypalOrderId->getValue(), array_keys($payPalOrderFromCache['payment_source'])[0]));
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
$payPalOrderFromCache = $payPalOrderCache->get($this->paypalOrderId->getValue());
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
$this->createOrder($payPalOrderFromCache, $payPalOrder);
}
} catch (Exception $exception) {
$this->context->smarty->assign('error', $exception->getMessage());
}
Expand All @@ -146,16 +155,14 @@ 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 = $payPalOrderFromCache['purchase_units'][0]['payments']['captures'][0] ?? null;
$commandBus->handle(new CreateOrderCommand($payPalOrder->getId()->getValue(), $capture));
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
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']);
}

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

private function redirectToOrderHistoryPage()
{
Tools::redirect($this->context->link->getPageLink('history'));
}
}
5 changes: 4 additions & 1 deletion controllers/front/validate.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ private function generateResponse()
}

$response = [
'status' => $psCheckoutCart->paypal_status,
'status' => $psCheckoutCart->paypal_status, // Need to change this to get the status from PayPal Order instead?
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
'paypalOrderId' => $psCheckoutCart->paypal_order,
'transactionIdentifier' => $paypalOrder && isset($paypalOrder->getOrderPayPal()['purchase_units'][0]['payments']['captures'][0]) ? $paypalOrder->getOrderPayPal()['purchase_units'][0]['payments']['captures'][0]['id'] : null,
];
Expand Down 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
44 changes: 30 additions & 14 deletions src/Checkout/EventSubscriber/CheckoutEventSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,21 +118,29 @@ public function updatePaymentMethodSelected(CheckoutCompletedEvent $event)
* @throws PrestaShopDatabaseException
* @throws PrestaShopException
* @throws PsCheckoutException
* @throws HttpTimeoutException
*/
public function proceedToPayment(CheckoutCompletedEvent $event)
{
/** @var GetPayPalOrderForCheckoutCompletedQueryResult $getPayPalOrderForCheckoutCompletedQueryResult */
$getPayPalOrderForCheckoutCompletedQueryResult = $this->commandBus->handle(new GetPayPalOrderForCheckoutCompletedQuery(
$event->getPayPalOrderId()->getValue()
));

$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($event->getPayPalOrderId()->getValue(), $capture));

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

try {
$this->commandBus->handle(
Expand All @@ -156,14 +164,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 {
/** @var GetPayPalOrderForCheckoutCompletedQueryResult $getPayPalOrderForCheckoutCompletedQueryResult */
$getPayPalOrderForCheckoutCompletedQueryResult = $this->commandBus->handle(new GetPayPalOrderForCheckoutCompletedQuery(
$event->getPayPalOrderId()->getValue()
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
));
$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($event->getPayPalOrderId()->getValue(), $capture));
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved

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;
}
47 changes: 47 additions & 0 deletions src/PayPal/Order/Cache/PayPalOrderCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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,
];

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;
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
namespace PrestaShop\Module\PrestashopCheckout\PayPal\Order\CommandHandler;

use Configuration;
use Context;
use PrestaShop\Module\PrestashopCheckout\Context\PrestaShopContext;
use PrestaShop\Module\PrestashopCheckout\Customer\ValueObject\CustomerId;
use PrestaShop\Module\PrestashopCheckout\Event\EventDispatcherInterface;
Expand Down Expand Up @@ -90,8 +89,7 @@ public function __construct(

public function handle(CapturePayPalOrderCommand $capturePayPalOrderCommand)
{
$context = Context::getContext();
$merchantId = Configuration::get('PS_CHECKOUT_PAYPAL_ID_MERCHANT', null, null, $context->shop->id);
$merchantId = Configuration::get('PS_CHECKOUT_PAYPAL_ID_MERCHANT', null, null, $this->prestaShopContext->getShopId());

$payload = [
'mode' => $capturePayPalOrderCommand->getFundingSource(),
Expand Down
2 changes: 1 addition & 1 deletion src/PayPal/Order/PaypalOrderDataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function isTokenSaved()

public function getPaymentTokenIdentifier()
{
if ($this->payPalOrder) {
if ($this->payPalOrder && isset($this->payPalOrder->getPaymentSource()[$this->payPalOrder->getFundingSource()])) {
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
$paymentSource = $this->payPalOrder->getPaymentSource()[$this->payPalOrder->getFundingSource()];
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved

if ($this->payPalOrder->getFundingSource() === 'card') {
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ public function handle(GetPayPalOrderForCheckoutCompletedQuery $getPayPalOrderQu
/** @var array{id: string, status: string} $order */
$order = $this->orderPayPalCache->get($getPayPalOrderQuery->getOrderPayPalId()->getValue());

if (!empty($order) && $order['status'] === 'APPROVED') {
if (!empty($order) && in_array($order['status'], ['COMPLETED', 'CANCELED'])) {
return new GetPayPalOrderForCheckoutCompletedQueryResult($order);
}

try {
$orderPayPal = new PaypalOrder($getPayPalOrderQuery->getOrderPayPalId()->getValue());
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
$this->orderPayPalCache->set($getPayPalOrderQuery->getOrderPayPalId()->getValue(), $orderPayPal->getOrder());
$orderToStoreInCache = !empty($order) ? array_replace_recursive($order, $orderPayPal->getOrder()) : $orderPayPal->getOrder();
$this->orderPayPalCache->set($getPayPalOrderQuery->getOrderPayPalId()->getValue(), $orderToStoreInCache);
L3RAZ marked this conversation as resolved.
Show resolved Hide resolved
} catch (HttpTimeoutException $exception) {
throw $exception;
} catch (Exception $exception) {
Expand Down
Loading
Loading