From 47ccb8230bfbeb204e889e87a7a3a7a1f4a26d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 18 Oct 2026 15:00:00 +0200 Subject: [PATCH 1/5] fix: send the buyer back to the confirmation and failure pages of the shop --- PayPlugModule.php | 16 +++++++++------- Service/PaymentService.php | 12 +++++++++++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/PayPlugModule.php b/PayPlugModule.php index fb28c18..590e53c 100755 --- a/PayPlugModule.php +++ b/PayPlugModule.php @@ -26,7 +26,6 @@ use Thelia\Log\Tlog; use Thelia\Model\Order; use Thelia\Module\AbstractPaymentModule; -use Thelia\Tools\URL; class PayPlugModule extends AbstractPaymentModule { @@ -109,13 +108,15 @@ public function pay(Order $order): ?Response PayPlugModule::getConfigValue(PayPlugConfigValue::DIFFERED_PAYMENT_ENABLED, false), PayPlugModule::getConfigValue(PayPlugConfigValue::ONE_CLICK_PAYMENT_ENABLED, false), $slice, - $orderTotalAmount + $orderTotalAmount, + $this->getPaymentSuccessPageUrl($order->getId()), + $this->getPaymentFailurePageUrl($order->getId(), null) ); $forceRedirect = false; if (true === $payment['isPaid']) { $forceRedirect = true; - $payment['url'] = URL::getInstance()->absoluteUrl('/order/placed/'.$order->getId()); + $payment['url'] = $this->getPaymentSuccessPageUrl($order->getId()); } if ($this->getRequest()->isXmlHttpRequest()) { @@ -127,13 +128,14 @@ public function pay(Order $order): ?Response ); } } catch (\Exception $exception) { - if ($this->getRequest()->isXmlHttpRequest()) { - return new JsonResponse(['error' => $exception->getMessage()], 400); - } Tlog::getInstance()->addError( 'Error PayPlugModule::pay() : ' . $exception->getMessage() ); - return new RedirectResponse(URL::getInstance()->absoluteUrl('error')); + + // The failure page of the shop, for a plain request and for the lightbox + // script alike: the script follows the redirection. The provider's message + // stays in the log. + return new RedirectResponse($this->getPaymentFailurePageUrl($order->getId(), null)); } return new RedirectResponse($payment['url']); diff --git a/Service/PaymentService.php b/Service/PaymentService.php index eccbacc..48b13a4 100755 --- a/Service/PaymentService.php +++ b/Service/PaymentService.php @@ -45,13 +45,23 @@ public function sendOrderPayment( bool $capture, bool $allowSaveCard, int $paymentSlice, - float $totalOrder + float $totalOrder, + ?string $returnUrl = null, + ?string $cancelUrl = null ) { $paymentEvent = (new PayPlugPaymentEvent()) ->buildFromOrder($order,$totalOrder) ->setCapture($capture) ->setAllowSaveCard($allowSaveCard); + if (null !== $returnUrl) { + $paymentEvent->setHostedPaymentReturnUrl($returnUrl); + } + + if (null !== $cancelUrl) { + $paymentEvent->setHostedPaymentCancelUrl($cancelUrl); + } + if (null !== $card = PayPlugCardQuery::create()->findOneByCustomerId($order->getCustomerId())) { $todayDate = date("Y-m"); $expireDate = date("Y-m", strtotime($card->getExpireYear().'-'.$card->getExpireMonth())); From e4a3b623bd742c89d0ee488225f05e354882e3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 18 Oct 2026 15:00:34 +0200 Subject: [PATCH 2/5] feat: offer the payment in instalments as an option of the checkout --- .../FormExtend/OrderFormListener.php | 81 ------------- EventListener/PaymentOptionListener.php | 114 ++++++++++++++++++ I18n/frontOffice/default/fr_FR.php | 1 + PayPlugModule.php | 5 +- 4 files changed, 117 insertions(+), 84 deletions(-) delete mode 100755 EventListener/FormExtend/OrderFormListener.php create mode 100644 EventListener/PaymentOptionListener.php diff --git a/EventListener/FormExtend/OrderFormListener.php b/EventListener/FormExtend/OrderFormListener.php deleted file mode 100755 index 68a9ada..0000000 --- a/EventListener/FormExtend/OrderFormListener.php +++ /dev/null @@ -1,81 +0,0 @@ -request = $requestStack->getCurrentRequest(); - } - - public function addMultiPaymentField(TheliaFormEvent $event) - { - if (!PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_ENABLED)) { - return; - } - - $event->getForm()->getFormBuilder() - ->add( - self::PAY_PLUG_MULTI_PAYMENT_FIELD_NAME, - CheckboxType::class - ); - - } - - public function checkMultiPaymentSelected(OrderEvent $event) - { - $request = $this->request; - if (null === $request || !$request->hasSession()) { - return; - } - - $request->getSession()->set(self::PAY_PLUG_MULTI_PAYMENT_FIELD_NAME, 0); - $formData = $request->attributes->get( - self::THELIA_CUSTOMER_ORDER_PAYMENT_FROM_NAME, - $request->query->get( - self::THELIA_CUSTOMER_ORDER_PAYMENT_FROM_NAME, - $request->request->get(self::THELIA_CUSTOMER_ORDER_PAYMENT_FROM_NAME) - ) - ); - - if (!isset($formData[self::PAY_PLUG_MULTI_PAYMENT_FIELD_NAME]) || 0 == $formData[self::PAY_PLUG_MULTI_PAYMENT_FIELD_NAME]) { - return; - } - - $request->getSession()->set(self::PAY_PLUG_MULTI_PAYMENT_FIELD_NAME, 1); - } - - /** - * Returns an array of event names this subscriber wants to listen to. - * - * @return array The event names to listen to - * - * @api - */ - public static function getSubscribedEvents(): array - { - return array( - TheliaEvents::FORM_AFTER_BUILD.'.'.self::THELIA_CUSTOMER_ORDER_PAYMENT_FROM_NAME => array('addMultiPaymentField', 64), - TheliaEvents::ORDER_SET_PAYMENT_MODULE => array('checkMultiPaymentSelected', 64) - ); - } -} \ No newline at end of file diff --git a/EventListener/PaymentOptionListener.php b/EventListener/PaymentOptionListener.php new file mode 100644 index 0000000..be15c5f --- /dev/null +++ b/EventListener/PaymentOptionListener.php @@ -0,0 +1,114 @@ + ['addMultiPaymentOption', 128], + ]; + } + + public function addMultiPaymentOption(PaymentModuleOptionEvent $event): void + { + if ($event->getModule()->getId() !== PayPlugModule::getModuleId()) { + return; + } + + if (!PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_ENABLED)) { + return; + } + + $cart = $event->getCart(); + + if (null === $cart) { + return; + } + + $country = $event->getCountry() ?? $this->taxEngine->getDeliveryCountry(); + $cartAmount = $cart->getTaxedAmount($country); + + if ($cartAmount <= (float) PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_MINIMUM) + || $cartAmount >= (float) PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_MAXIMUM)) { + return; + } + + $translator = Translator::getInstance(); + $domain = PayPlugModule::DOMAIN_NAME.'.fo.default'; + + $option = (new PaymentModuleOption()) + ->setCode(self::MULTI_PAYMENT_OPTION_CODE) + ->setTitle($translator->trans( + 'Pay in %n times without fees.', + ['%n' => PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_TIMES)], + $domain, + )) + ->setDescription($translator->trans('You will be asked to save your payment card data.', [], $domain)); + + $group = (new PaymentModuleOptionGroup()) + ->setCode(self::GROUP_CODE) + ->setTitle($translator->trans('Payment in instalments', [], $domain)) + ->setMinimumSelectedOptions(0) + ->setMaximumSelectedOptions(1) + ->appendPaymentModuleOption($option); + + $event->appendPaymentModuleOptionGroups($group); + } + + /** + * Whether the buyer ticked the instalment option, while the shop still offers it. + */ + public static function isMultiPaymentChosen(SessionInterface $session): bool + { + if (!PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_ENABLED)) { + return false; + } + + $choices = $session->get(self::SESSION_KEY, []); + + if (!\is_array($choices)) { + return false; + } + + foreach ($choices as $choice) { + if (\is_array($choice) + && self::GROUP_CODE === ($choice['group'] ?? null) + && \is_array($choice['values'] ?? null) + && \in_array(self::MULTI_PAYMENT_OPTION_CODE, $choice['values'], true)) { + return true; + } + } + + return false; + } +} diff --git a/I18n/frontOffice/default/fr_FR.php b/I18n/frontOffice/default/fr_FR.php index 6303a0d..d156074 100755 --- a/I18n/frontOffice/default/fr_FR.php +++ b/I18n/frontOffice/default/fr_FR.php @@ -4,6 +4,7 @@ 'A payment card ending by %last4 has been previously saved at your request. Payment for your order will be made immediately after clicking the "Next Step" button.' => 'Une carte de paiement finissant par %last4 as été enregitrée précédemment a votre demande. Le paiement de cette commande seras fait automatiquement après un clic sur le bouton "Etape suivante".', 'One click payment is available !' => 'Paiement en un clic disponible !', 'Pay in %n times without fees.' => 'Paiement en %n fois sans frais.', + 'Payment in instalments' => 'Paiement en plusieurs fois', 'To clear your payment card details, please click here' => 'Pour supprimer ces informations de paiement, cliquez ici', 'You will be asked to save your payment card data.' => 'Ils vous sera demandé d\'enregistrer votre carte bancaire.', ); diff --git a/PayPlugModule.php b/PayPlugModule.php index 590e53c..5b49435 100755 --- a/PayPlugModule.php +++ b/PayPlugModule.php @@ -12,7 +12,7 @@ namespace PayPlugModule; -use PayPlugModule\EventListener\FormExtend\OrderFormListener; +use PayPlugModule\EventListener\PaymentOptionListener; use PayPlugModule\Model\PayPlugConfigValue; use PayPlugModule\Service\PaymentService; use Propel\Runtime\Connection\ConnectionInterface; @@ -90,8 +90,7 @@ public function pay(Order $order): ?Response $request = $this->getRequest(); $isMultiPayment = $request->hasSession() - ? $request->getSession()->get(OrderFormListener::PAY_PLUG_MULTI_PAYMENT_FIELD_NAME, 0) - : 0; + && PaymentOptionListener::isMultiPaymentChosen($request->getSession()); $orderTotalAmount = $this->getOrderPayTotalAmount($order); if ($isMultiPayment) { From ba5d2d892056efb66be1cec5db0873ef501160dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 18 Oct 2026 15:01:38 +0200 Subject: [PATCH 3/5] feat: show the saved card and open the lightbox on the checkout payment step --- Hook/Theme/PayPlugPaymentThemeHook.php | 155 ++++++++++++++++++ I18n/frontOffice/default/fr_FR.php | 6 +- .../PayPlugModule/lightbox-script.html.twig | 64 ++++++++ .../PayPlugModule/payment-extra.html.twig | 13 ++ 4 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 Hook/Theme/PayPlugPaymentThemeHook.php create mode 100644 templates/frontOffice/default/PayPlugModule/lightbox-script.html.twig create mode 100644 templates/frontOffice/default/PayPlugModule/payment-extra.html.twig diff --git a/Hook/Theme/PayPlugPaymentThemeHook.php b/Hook/Theme/PayPlugPaymentThemeHook.php new file mode 100644 index 0000000..c75c5ca --- /dev/null +++ b/Hook/Theme/PayPlugPaymentThemeHook.php @@ -0,0 +1,155 @@ +isLightbox() + ? $this->twig->render(self::TEMPLATE_DIR.'lightbox-script.html.twig') + : ''; + } + + if ((int) ($parameters['module'] ?? 0) !== PayPlugModule::getModuleId()) { + return ''; + } + + $last4 = $this->savedCardLast4(); + $payUrl = $this->isLightbox() ? $this->payUrl() : null; + + if (null === $last4 && null === $payUrl) { + return ''; + } + + // Translated here: the module's front strings are read by the Thelia translator, + // the Twig one of the theme does not load them. + $translator = Translator::getInstance(); + $domain = PayPlugModule::DOMAIN_NAME.'.fo.default'; + + return $this->twig->render(self::TEMPLATE_DIR.'payment-extra.html.twig', [ + 'payUrl' => $payUrl, + 'savedCard' => null === $last4 ? null : [ + 'title' => $translator->trans('One click payment is available !', [], $domain), + 'notice' => $translator->trans( + 'A payment card ending in %last4% is saved on your account: it will be charged as soon as you place the order.', + ['%last4%' => $last4], + $domain, + ), + 'deleteLabel' => $translator->trans('Delete this card', [], $domain), + 'deleteUrl' => $this->urlGenerator->generate('payplugmodule_delete_card'), + ], + ]); + } + + private function isLightbox(): bool + { + return 'lightbox' === PayPlugModule::getConfigValue(PayPlugConfigValue::PAYMENT_PAGE_TYPE); + } + + /** + * The address the order button of the theme leads to, which the lightbox script + * takes over. A theme without that route keeps the hosted payment page. + */ + private function payUrl(): ?string + { + try { + return $this->urlGenerator->generate(self::PAY_ROUTE); + } catch (RouteNotFoundException) { + return null; + } + } + + /** + * The last digits of the card PaymentService will charge, under the same conditions: + * one click payment on, a card saved by the customer, not expired. + */ + private function savedCardLast4(): ?string + { + if (!PayPlugModule::getConfigValue(PayPlugConfigValue::ONE_CLICK_PAYMENT_ENABLED)) { + return null; + } + + $request = $this->requestStack->getCurrentRequest(); + + if (null === $request || !$request->hasSession()) { + return null; + } + + $session = $request->getSession(); + + if (!$session instanceof Session) { + return null; + } + + $customer = $session->getCustomerUser(); + + if (null === $customer) { + return null; + } + + $card = PayPlugCardQuery::create()->findOneByCustomerId($customer->getId()); + + if (null === $card) { + return null; + } + + $expireDate = date('Y-m', strtotime($card->getExpireYear().'-'.$card->getExpireMonth())); + + if ($expireDate <= date('Y-m')) { + return null; + } + + return (string) $card->getLast4(); + } +} diff --git a/I18n/frontOffice/default/fr_FR.php b/I18n/frontOffice/default/fr_FR.php index d156074..f3e94cc 100755 --- a/I18n/frontOffice/default/fr_FR.php +++ b/I18n/frontOffice/default/fr_FR.php @@ -1,10 +1,10 @@ 'Une carte de paiement finissant par %last4 as été enregitrée précédemment a votre demande. Le paiement de cette commande seras fait automatiquement après un clic sur le bouton "Etape suivante".', + 'A payment card ending in %last4% is saved on your account: it will be charged as soon as you place the order.' => 'Une carte de paiement finissant par %last4% est enregistrée sur votre compte : elle sera débitée dès que vous passerez la commande.', + 'Delete this card' => 'Supprimer cette carte', 'One click payment is available !' => 'Paiement en un clic disponible !', 'Pay in %n times without fees.' => 'Paiement en %n fois sans frais.', 'Payment in instalments' => 'Paiement en plusieurs fois', - 'To clear your payment card details, please click here' => 'Pour supprimer ces informations de paiement, cliquez ici', - 'You will be asked to save your payment card data.' => 'Ils vous sera demandé d\'enregistrer votre carte bancaire.', + 'You will be asked to save your payment card data.' => 'Il vous sera demandé d\'enregistrer votre carte bancaire.', ); diff --git a/templates/frontOffice/default/PayPlugModule/lightbox-script.html.twig b/templates/frontOffice/default/PayPlugModule/lightbox-script.html.twig new file mode 100644 index 0000000..34f8e5b --- /dev/null +++ b/templates/frontOffice/default/PayPlugModule/lightbox-script.html.twig @@ -0,0 +1,64 @@ +{# In the head of the payment pages, in lightbox mode. A click on a link to the address + the PayPlug method of the page names (data-pay-url, see payment-extra.html.twig) is + sent in the background: the module answers with the PayPlug payment address, opened in + the PayPlug lightbox, or with the address to go to (a card already saved, an error). + Without the marker — another method chosen — or without the library, the link is + followed as usual and the hosted payment page takes over. #} + + diff --git a/templates/frontOffice/default/PayPlugModule/payment-extra.html.twig b/templates/frontOffice/default/PayPlugModule/payment-extra.html.twig new file mode 100644 index 0000000..8a7c72a --- /dev/null +++ b/templates/frontOffice/default/PayPlugModule/payment-extra.html.twig @@ -0,0 +1,13 @@ +{# Under the PayPlug method of the payment step, once chosen. In lightbox mode the root + carries the address the order button leads to: lightbox-script.html.twig opens the + PayPlug form instead of following it. The texts come translated from + PayPlugPaymentThemeHook; the classes are those of the Flexy theme. #} +
+ {% if savedCard %} +

{{ savedCard.title }}

+

+ {{ savedCard.notice }} + {{ savedCard.deleteLabel }} +

+ {% endif %} +
From 5b6c8b9f6cf5a224e13716c1a1a38df5dc9f0722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 18 Oct 2026 15:01:38 +0200 Subject: [PATCH 4/5] refactor: remove the Smarty front templates and their unused hook --- Hook/FrontHookManager.php | 105 ------------------ .../default/PayPlugModule/multi-payment.html | 8 -- .../PayPlugModule/one-click-payment.html | 6 - .../order-invoice-after-js-include.html | 34 ------ 4 files changed, 153 deletions(-) delete mode 100755 Hook/FrontHookManager.php delete mode 100755 templates/frontOffice/default/PayPlugModule/multi-payment.html delete mode 100755 templates/frontOffice/default/PayPlugModule/one-click-payment.html delete mode 100755 templates/frontOffice/default/PayPlugModule/order-invoice-after-js-include.html diff --git a/Hook/FrontHookManager.php b/Hook/FrontHookManager.php deleted file mode 100755 index 3d4dda3..0000000 --- a/Hook/FrontHookManager.php +++ /dev/null @@ -1,105 +0,0 @@ -taxEngine = $taxEngine; - } - - public function onOrderInvoiceAfterJsInclude(HookRenderEvent $event) - { - $payPlugModuleId = PayPlugModule::getModuleId(); - if (PayPlugModule::getConfigValue(PayPlugConfigValue::PAYMENT_PAGE_TYPE) === "lightbox") { - $event->add($this->render( - 'PayPlugModule/order-invoice-after-js-include.html', - compact('payPlugModuleId') - )); - } - } - - public function onOrderInvoicePaymentExtra(HookRenderEvent $event) - { - if ((int)$event->getArgument('module') !== PayPlugModule::getModuleId()) { - return; - } - - $this->displayOneClickPayment($event); - $this->displayMultiPayment($event); - } - - protected function displayMultiPayment(HookRenderEvent $event) - { - if (!PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_ENABLED)) { - return; - } - - $nTimes = PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_TIMES); - $minimumAmount = PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_MINIMUM); - $maximumAmount = PayPlugModule::getConfigValue(PayPlugConfigValue::MULTI_PAYMENT_MAXIMUM); - - /** @var Country $country */ - $country = $this->taxEngine->getDeliveryCountry(); - - $request = $this->getRequest(); - if (null === $request || !$request->hasSession()) { - return; - } - - $cart = $request->getSession()->getSessionCart(); - $cartAmount = $cart->getTaxedAmount($country); - if ($cartAmount <= $minimumAmount || $cartAmount >= $maximumAmount) { - return; - } - - $event->add( - $this->render( - 'PayPlugModule/multi-payment.html', - compact("nTimes") - ) - ); - } - - protected function displayOneClickPayment(HookRenderEvent $event) - { - if (!PayPlugModule::getConfigValue(PayPlugConfigValue::ONE_CLICK_PAYMENT_ENABLED)) { - return; - } - - $request = $this->getRequest(); - if (null === $request || !$request->hasSession()) { - return; - } - - $customerId = $request->getSession()->getCustomerUser()->getId(); - - $payPlugCard = PayPlugCardQuery::create() - ->findOneByCustomerId($customerId); - - if (null === $payPlugCard) { - return; - } - - $event->add( - $this->render( - 'PayPlugModule/one-click-payment.html', - [ - 'last4' => $payPlugCard->getLast4() - ] - ) - ); - } -} \ No newline at end of file diff --git a/templates/frontOffice/default/PayPlugModule/multi-payment.html b/templates/frontOffice/default/PayPlugModule/multi-payment.html deleted file mode 100755 index 4229e30..0000000 --- a/templates/frontOffice/default/PayPlugModule/multi-payment.html +++ /dev/null @@ -1,8 +0,0 @@ -{form_field form=$form field='pay_plug_multi_payment'} -

- - -
- {intl l='You will be asked to save your payment card data.' n={url path='/payplug/card/delete'} d='payplugmodule.fo.default'} -

-{/form_field} \ No newline at end of file diff --git a/templates/frontOffice/default/PayPlugModule/one-click-payment.html b/templates/frontOffice/default/PayPlugModule/one-click-payment.html deleted file mode 100755 index 60e3146..0000000 --- a/templates/frontOffice/default/PayPlugModule/one-click-payment.html +++ /dev/null @@ -1,6 +0,0 @@ -

{intl l='One click payment is available !' d='payplugmodule.fo.default'}

-

- {intl l='A payment card ending by %last4 has been previously saved at your request. Payment for your order will be made immediately after clicking the "Next Step" button.' last4=$last4 d='payplugmodule.fo.default'} -
- {intl l='To clear your payment card details, please click here' url={url path='/payplug/card/delete'} d='payplugmodule.fo.default'}. -

diff --git a/templates/frontOffice/default/PayPlugModule/order-invoice-after-js-include.html b/templates/frontOffice/default/PayPlugModule/order-invoice-after-js-include.html deleted file mode 100755 index 512fa42..0000000 --- a/templates/frontOffice/default/PayPlugModule/order-invoice-after-js-include.html +++ /dev/null @@ -1,34 +0,0 @@ - - - \ No newline at end of file From 66a9d5644f10a31be07bbacd82f5d1bf8c79971c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20Nozi=C3=A8re?= Date: Sun, 18 Oct 2026 15:26:15 +0200 Subject: [PATCH 5/5] chore: bump module version to 3.2.0 --- Config/module.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config/module.xml b/Config/module.xml index 7ed0c5d..aee6a55 100755 --- a/Config/module.xml +++ b/Config/module.xml @@ -17,7 +17,7 @@ en_US fr_FR - 3.1.0 + 3.2.0 Vincent Lopes-Vicente