Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.env
.env.*
.idea
.gitmodules
prestashop
Expand All @@ -15,3 +16,5 @@ vendor
docker-compose.local.yml
composer2.2.phar
.claude.local.md
views
.history
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ make phpstan-baseline # Regenerate PHPStan baseline for current $MODULE_VER
make phpstan-baseline-all # Regenerate baselines for all versions

make unit-test # All unit tests (api, utility, core, presentation)
make integration-test # All integration tests (module, core, infrastructure)
make create-test-db # Drop + recreate test_prestashop from prestashop (required before integration tests)
make integration-test # create-test-db + all integration tests (module, core, infrastructure)
make test # Full test suite

# Run a single test suite directly
Expand All @@ -55,6 +56,7 @@ Module logs (inside project root, not container): `prestashop/<PS_VERSION_TAG>/v

All `make` test commands run inside the Docker container using `$MODULE_VERSION` and `$PS_VERSION_TAG` from `.env`.
`make phpstan` runs locally without Docker. All `make *-test` commands require a running Docker container (`make up`).
Integration tests additionally require the `test_prestashop` database; `make integration-test` creates it automatically via `make create-test-db`.

Do not run `phpunit` directly from the host — cross-package autoloading (e.g., `api/` classes in `core/` tests) only resolves inside the Docker container via the module's `vendor/autoload.php`. Always use `make` commands for tests.

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ php-unit-presentation:

unit-test: php-unit-api php-unit-utility php-unit-core php-unit-presentation

php-integration-core:
create-test-db:
docker exec -i $${MODULE_VERSION}-ps-mysql-$${PS_VERSION_TAG} bash -c "mysql -uroot -pprestashop -e \"DROP DATABASE IF EXISTS test_prestashop; CREATE DATABASE test_prestashop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\""
docker exec -i $${MODULE_VERSION}-ps-mysql-$${PS_VERSION_TAG} bash -c "mysqldump -uroot -pprestashop prestashop | mysql -uroot -pprestashop test_prestashop"

php-integration-core: create-test-db
docker exec -i $${MODULE_VERSION}-ps-prestashop-$${PS_VERSION_TAG} bash -c "php modules/ps_checkout/vendor/bin/phpunit --configuration=modules/ps_checkout/vendor/invertus/core/tests/phpunit-integration.xml --bootstrap=modules/ps_checkout/vendor/invertus/core/tests/bootstrap-integration.php"

integration-test: php-integration-core
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,16 @@ Run `make unit-test` in terminal.

#### Integration tests

Run `make integration-test` in terminal.
Integration tests require a running Docker environment and a dedicated test database.

```bash
make up # Start containers (required)
make integration-test # Creates test_prestashop DB, then runs tests
```

The `make integration-test` target automatically runs `make create-test-db` first, which drops and recreates a `test_prestashop` database from the current `prestashop` database. This ensures a consistent state before each run.

> **Note:** If you only need to recreate the test database without running tests, use `make create-test-db` directly.

## Contributing

Expand Down
15 changes: 15 additions & 0 deletions api/src/Http/OrderHttpClient.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
Expand All @@ -24,6 +25,7 @@
use Http\Client\Exception\HttpException;
use PsCheckout\Api\Http\Configuration\HttpClientConfigurationBuilderInterface;
use PsCheckout\Api\Http\Exception\PayPalError;
use PsCheckout\Core\Exception\PsCheckoutException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

Expand All @@ -44,6 +46,19 @@ public function sendRequest(RequestInterface $request): ResponseInterface
} catch (HttpException $exception) {
$response = $exception->getResponse();
$decodedBody = json_decode((string) $response->getBody(), true);

if ($response->getStatusCode() === 422 && is_array($decodedBody)) {
Comment thread
seiwan marked this conversation as resolved.
Outdated
$errorName = isset($decodedBody['name']) && is_string($decodedBody['name']) ? $decodedBody['name'] : '';

if ($errorName === 'SHOP_NOT_REGISTERED_IN_MDU') {
throw new PsCheckoutException(
'Shop is not registered in the PrestaShop Checkout services.',
PsCheckoutException::SHOP_NOT_REGISTERED_IN_MDU,
$exception
);
}
}

$message = $this->extractMessage(is_array($decodedBody) ? $decodedBody : []);

if ($message) {
Expand Down
25 changes: 25 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [Unreleased]

### Added

- **XO-3014** — Handle 422 MDU registration error on order creation
- Detect `SHOP_NOT_REGISTERED_IN_MDU` error code returned by `order-api` with HTTP 422
- Display a polite, generic message to the buyer: *"This payment method is temporarily unavailable, please choose another one."*
- Log an explicit error message on the merchant side when the shop is not registered in the PrestaShop Checkout services
- Persist a `PS_CHECKOUT_SHOP_NOT_REGISTERED_IN_MDU` configuration flag when the error occurs
- Display a persistent danger alert on the module configuration page in the back-office when the flag is set
- Translations added for the admin alert in: English, French, German, Spanish, Italian, Dutch, Polish, Portuguese
- New `PsCheckoutException::SHOP_NOT_REGISTERED_IN_MDU` exception code (`82`)

### Fixed

- Integration tests: create a fresh `test_prestashop` database automatically before each run (`make create-test-db`)
- Integration tests: add `stderr="true"` to `phpunit-integration.xml` to prevent "headers already sent" errors caused by PHPUnit output conflicting with PHP session start
2 changes: 2 additions & 0 deletions core/src/Exception/PsCheckoutException.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,6 @@ class PsCheckoutException extends \Exception
const CART_CUSTOMER_EMAIL_INVALID = 80;

const CART_CUSTOMER_PHONE_INVALID = 81;

const SHOP_NOT_REGISTERED_IN_MDU = 82;
}
26 changes: 23 additions & 3 deletions core/src/Order/Exception/Handler/OrderCreationExceptionHandler.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
Expand All @@ -23,7 +24,9 @@
use Exception;
use PsCheckout\Api\Http\Exception\PayPalException;
use PsCheckout\Core\Exception\PsCheckoutException;
use PsCheckout\Core\Settings\Configuration\PayPalConfiguration;
use PsCheckout\Infrastructure\Action\CustomerNotifyActionInterface;
use PsCheckout\Infrastructure\Adapter\ConfigurationInterface;
use PsCheckout\Presentation\TranslatorInterface;
use Psr\Log\LoggerInterface;

Expand All @@ -44,14 +47,21 @@ class OrderCreationExceptionHandler implements OrderCreationExceptionHandlerInte
*/
private $customerNotifyAction;

/**
* @var ConfigurationInterface
*/
private $configuration;

public function __construct(
TranslatorInterface $translator,
LoggerInterface $logger,
CustomerNotifyActionInterface $customerNotifyAction
CustomerNotifyActionInterface $customerNotifyAction,
ConfigurationInterface $configuration
) {
$this->translator = $translator;
$this->logger = $logger;
$this->customerNotifyAction = $customerNotifyAction;
$this->configuration = $configuration;
}

/**
Expand Down Expand Up @@ -341,7 +351,6 @@ public function handle(Exception $exception, string $paypalOrderId)
$exceptionMessageForCustomer = $this->translator->trans('The currency you selected is not supported. Please try another payment method or contact support for assistance.');

break;

}
}

Expand Down Expand Up @@ -441,11 +450,22 @@ public function handleOrderCreateException(Exception $exception, ?string $fundin
$isClientError = true;
$exceptionMessageForCustomer = $this->translator->trans('Your date of birth is invalid or missing. Please check and try again.');

break;
case PsCheckoutException::SHOP_NOT_REGISTERED_IN_MDU:
$exceptionMessageForCustomer = $this->translator->trans('This payment method is temporarily unavailable, please choose another one.');

$this->configuration->set(PayPalConfiguration::PS_CHECKOUT_SHOP_NOT_REGISTERED_IN_MDU, '1');

break;
}
}

if ($isClientError) {
if ($exception instanceof PsCheckoutException && $exception->getCode() === PsCheckoutException::SHOP_NOT_REGISTERED_IN_MDU) {
$this->logger->error(
'CreateOrder - Shop is not registered in the PrestaShop Checkout services: PayPal payments are blocked. Please re-onboard via the PrestaShop back office.',
['exception' => $exception]
);
} elseif ($isClientError) {
$this->logger->notice('CreateOrder - Exception ' . $exception->getCode(), ['exception' => $exception]);
} else {
$this->logger->error('CreateOrder - Exception ' . $exception->getCode(), ['exception' => $exception]);
Expand Down
2 changes: 2 additions & 0 deletions core/src/Settings/Configuration/PayPalConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class PayPalConfiguration

const PS_CHECKOUT_PAYPAL_COUNTRY_MERCHANT = 'PS_CHECKOUT_PAYPAL_COUNTRY_MERCHANT';

const PS_CHECKOUT_SHOP_NOT_REGISTERED_IN_MDU = 'PS_CHECKOUT_SHOP_NOT_REGISTERED_IN_MDU';

// NOT CONFIGURATION
const PS_CHECKOUT_CUSTOMER_INTENT_VAULT = 'VAULT';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
use PsCheckout\Api\Http\Exception\PayPalException;
use PsCheckout\Core\Exception\PsCheckoutException;
use PsCheckout\Core\Order\Exception\Handler\OrderCreationExceptionHandler;
use PsCheckout\Core\Settings\Configuration\PayPalConfiguration;
use PsCheckout\Infrastructure\Action\CustomerNotifyActionInterface;
use PsCheckout\Infrastructure\Adapter\ConfigurationInterface;
use PsCheckout\Presentation\TranslatorInterface;
use Psr\Log\LoggerInterface;

Expand All @@ -42,6 +44,9 @@ class OrderCreationExceptionHandlerTest extends TestCase
/** @var CustomerNotifyActionInterface&\PHPUnit\Framework\MockObject\MockObject */
private $customerNotifyAction;

/** @var ConfigurationInterface&\PHPUnit\Framework\MockObject\MockObject */
private $configuration;

protected function setUp(): void
{
parent::setUp();
Expand All @@ -53,11 +58,13 @@ protected function setUp(): void

$this->logger = $this->createMock(LoggerInterface::class);
$this->customerNotifyAction = $this->createMock(CustomerNotifyActionInterface::class);
$this->configuration = $this->createMock(ConfigurationInterface::class);

$this->handler = new OrderCreationExceptionHandler(
$this->translator,
$this->logger,
$this->customerNotifyAction
$this->customerNotifyAction,
$this->configuration
);
}

Expand Down Expand Up @@ -338,4 +345,60 @@ public function testHandleOrderCreateExceptionReturns500ForUnknownException(): v
$this->assertFalse($result['status']);
$this->assertSame('Something went wrong', $result['body']['error']['message']);
}

public function testShopNotRegisteredInMduReturnsPoliteMessageAndLogsExplicitError(): void
{
$exception = new PsCheckoutException(
'Shop is not registered in the PrestaShop Checkout services.',
PsCheckoutException::SHOP_NOT_REGISTERED_IN_MDU
);

$this->logger->expects($this->never())->method('notice');
$this->logger->expects($this->once())->method('error')->with(
$this->stringContains('not registered in the PrestaShop Checkout services')
);
$this->configuration->expects($this->once())->method('set')->with(
PayPalConfiguration::PS_CHECKOUT_SHOP_NOT_REGISTERED_IN_MDU,
'1'
);

/** @var array{httpCode: int, status: bool, body: array{error: array{message: string}}} $result */
$result = $this->handler->handleOrderCreateException($exception, null);

$this->assertSame(500, $result['httpCode']);
$this->assertFalse($result['status']);
$this->assertSame(
'This payment method is temporarily unavailable, please choose another one.',
$result['body']['error']['message']
);
}

public function testShopNotRegisteredInMduDoesNotNotifyCustomerService(): void
{
$exception = new PsCheckoutException(
'Shop is not registered in the PrestaShop Checkout services.',
PsCheckoutException::SHOP_NOT_REGISTERED_IN_MDU
);

$this->customerNotifyAction->expects($this->never())->method('execute');
$this->configuration->method('set')->willReturn(true);

$this->handler->handleOrderCreateException($exception, null);
}

public function testShopNotRegisteredInMduPersistsFlagRegardlessOfFundingSource(): void
{
$exception = new PsCheckoutException(
'Shop is not registered in the PrestaShop Checkout services.',
PsCheckoutException::SHOP_NOT_REGISTERED_IN_MDU
);

$this->configuration->expects($this->once())->method('set')->with(
PayPalConfiguration::PS_CHECKOUT_SHOP_NOT_REGISTERED_IN_MDU,
'1'
)->willReturn(true);
$this->logger->method('error');

$this->handler->handleOrderCreateException($exception, 'paypal');
}
}
1 change: 1 addition & 0 deletions core/tests/phpunit-integration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="bootstrap-integration.php"
colors="true"
stderr="true"
executionOrder="random"
beStrictAboutChangesToGlobalState="true"
beStrictAboutResourceUsageDuringSmallTests="true"
Expand Down
1 change: 1 addition & 0 deletions ps17/config/front/handler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ services:
- '@PsCheckout\Module\Presentation\Translator'
- '@Psr\Log\LoggerInterface'
- '@PsCheckout\Infrastructure\Action\CustomerNotifyAction'
- '@PsCheckout\Infrastructure\Adapter\Configuration'

PsCheckout\Core\PayPal\Refund\Exception\Handler\RefundExceptionHandler:
class: PsCheckout\Core\PayPal\Refund\Exception\Handler\RefundExceptionHandler
Expand Down
5 changes: 5 additions & 0 deletions ps17/ps_checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,14 @@ public function getContent()
}
}

/** @var Configuration $configuration */
$configuration = $this->getService(Configuration::class);
$shopNotRegisteredInMdu = (bool) $configuration->get(PayPalConfiguration::PS_CHECKOUT_SHOP_NOT_REGISTERED_IN_MDU);

$this->context->smarty->assign([
'requiredDependencies' => $requiredDependencies,
'hasRequiredDependencies' => $hasRequiredDependencies,
'shopNotRegisteredInMdu' => $shopNotRegisteredInMdu,
]);

return $this->display(__FILE__, 'views/templates/admin/configuration.tpl');
Expand Down
3 changes: 3 additions & 0 deletions ps17/translations/de.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,6 @@
$_MODULE['<{ps_checkout}prestashop>translator_d74dc4730c60108b5245dd5852ed20b8'] = 'Diese Zahlungsmethode ist für internationale Transaktionen nicht verfügbar. Bitte versuchen Sie eine andere Zahlungsmethode.';
$_MODULE['<{ps_checkout}prestashop>translator_6000b48fba6aa3541fef1b45dee3275f'] = 'Das Transaktionslimit wurde überschritten. Bitte versuchen Sie einen kleineren Betrag oder eine andere Zahlungsmethode.';
$_MODULE['<{ps_checkout}prestashop>translator_f04233142f9f1d876c8d2539ad65dade'] = 'Die Zahlung kann derzeit nicht verarbeitet werden. Bitte kontaktieren Sie unseren Kundenservice.';
$_MODULE['<{ps_checkout}prestashop>configuration_a56b2a00cf5a5d064ba5cda91b756da5'] = 'PayPal-Zahlungen sind derzeit gesperrt.';
$_MODULE['<{ps_checkout}prestashop>configuration_b4d2f434d0f642613efa63e50f079ffd'] = 'Dieser Shop ist nicht bei den PrestaShop Checkout-Diensten registriert. PayPal-Zahlungen können erst verarbeitet werden, wenn das Problem behoben ist. Bitte überprüfen Sie Ihre Modulkonfiguration und registrieren Sie sich gegebenenfalls neu.';
$_MODULE['<{ps_checkout}prestashop>configuration_d3d2e617335f08df83599665eef8a418'] = 'Schließen';
3 changes: 3 additions & 0 deletions ps17/translations/en.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,6 @@
$_MODULE['<{ps_checkout}prestashop>translator_d74dc4730c60108b5245dd5852ed20b8'] = 'This payment method is not available for international transactions. Please try another payment method.';
$_MODULE['<{ps_checkout}prestashop>translator_6000b48fba6aa3541fef1b45dee3275f'] = 'The transaction limit has been exceeded. Please try a smaller amount or another payment method.';
$_MODULE['<{ps_checkout}prestashop>translator_f04233142f9f1d876c8d2539ad65dade'] = 'Payment cannot be processed at the moment. Please contact our customer service.';
$_MODULE['<{ps_checkout}prestashop>configuration_a56b2a00cf5a5d064ba5cda91b756da5'] = 'PayPal payments are currently blocked.';
$_MODULE['<{ps_checkout}prestashop>configuration_b4d2f434d0f642613efa63e50f079ffd'] = 'This shop is not registered in the PrestaShop Checkout services. PayPal payments cannot be processed until the issue is resolved. Please check your module configuration and re-onboard if necessary.';
$_MODULE['<{ps_checkout}prestashop>configuration_d3d2e617335f08df83599665eef8a418'] = 'Close';
3 changes: 3 additions & 0 deletions ps17/translations/es.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,6 @@
$_MODULE['<{ps_checkout}prestashop>translator_d74dc4730c60108b5245dd5852ed20b8'] = 'Este método de pago no está disponible para transacciones internacionales. Por favor, pruebe otro método de pago.';
$_MODULE['<{ps_checkout}prestashop>translator_6000b48fba6aa3541fef1b45dee3275f'] = 'Se ha superado el límite de transacción. Por favor, pruebe con un importe menor o con otro método de pago.';
$_MODULE['<{ps_checkout}prestashop>translator_f04233142f9f1d876c8d2539ad65dade'] = 'El pago no puede procesarse en este momento. Por favor, póngase en contacto con nuestro servicio de atención al cliente.';
$_MODULE['<{ps_checkout}prestashop>configuration_a56b2a00cf5a5d064ba5cda91b756da5'] = 'Los pagos de PayPal están actualmente bloqueados.';
$_MODULE['<{ps_checkout}prestashop>configuration_b4d2f434d0f642613efa63e50f079ffd'] = 'Esta tienda no está registrada en los servicios de PrestaShop Checkout. Los pagos de PayPal no se pueden procesar hasta que se resuelva el problema. Por favor, compruebe la configuración de su módulo y vuelva a incorporarse si es necesario.';
$_MODULE['<{ps_checkout}prestashop>configuration_d3d2e617335f08df83599665eef8a418'] = 'Cerrar';
3 changes: 3 additions & 0 deletions ps17/translations/fr.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,6 @@
$_MODULE['<{ps_checkout}prestashop>translator_d74dc4730c60108b5245dd5852ed20b8'] = 'Ce moyen de paiement n\'est pas disponible pour les transactions internationales. Veuillez essayer un autre moyen de paiement.';
$_MODULE['<{ps_checkout}prestashop>translator_6000b48fba6aa3541fef1b45dee3275f'] = 'La limite de transaction a été dépassée. Veuillez essayer un montant inférieur ou un autre moyen de paiement.';
$_MODULE['<{ps_checkout}prestashop>translator_f04233142f9f1d876c8d2539ad65dade'] = 'Le paiement ne peut pas être traité pour le moment. Veuillez contacter notre service client.';
$_MODULE['<{ps_checkout}prestashop>configuration_a56b2a00cf5a5d064ba5cda91b756da5'] = 'Les paiements PayPal sont actuellement bloqués.';
$_MODULE['<{ps_checkout}prestashop>configuration_b4d2f434d0f642613efa63e50f079ffd'] = 'Cette boutique n\'est pas enregistrée dans les services PrestaShop Checkout. Les paiements PayPal ne peuvent pas être traités jusqu\'à ce que le problème soit résolu. Veuillez vérifier la configuration de votre module et vous réintégrer si nécessaire.';
$_MODULE['<{ps_checkout}prestashop>configuration_d3d2e617335f08df83599665eef8a418'] = 'Fermer';
Loading