hipay / hipay-fullservice-sdk-magento2

Official repository of the HiPay Fullservice extension for Magento v2.
https://developer.hipay.com/doc/hipay-fullservice-sdk-magento2/
Apache License 2.0
10 stars 16 forks source link

Impossible de réutiliser un code promo après une commande "canceled" suite à une transaction en erreur #156

Closed Synoffs closed 11 months ago

Synoffs commented 1 year ago

Description

Après le passage d'une commande utilisant une code promo dont la transaction est en échec, avec pour résultat une commande en statut "canceled". Il est impossible de réutiliser ce même code promo pour repasser une commande.

Pré-conditions

Magento 2.4.4-p2 (pas testé sous d'autres versions) PHP 8.1 Module HiPay installé (hipay/hipay-fullservice-sdk-magento2) 1.16.0

Étapes pour reproduire

Activer le mode de paiement HiPay Enterprise Hosted Page (Stores > Configuration > Sales > Order > Payment methods > HiPay Enterprise Hosted Page) Configurer HiPay Enterprise Credit Card Hosted field :

Créer une règle panier (salesrule) :

Ajouter un produit panier Accéder à la page panier Ajouter le coupon précédemment créée Valider le panier pour aller dans le checkout Utiliser la carte de test 4010 0586 9321 4759 pour avoir un code retour d'erreur La page d’échec s'affiche. Faire une nouvelle commande. Ajouter le même code promo.

Résultat actuel

Lors de l'ajout du code même promo, après avoir passé une commande qui a échoué, le message d'erreur "le code de réduction "XXXXXXXX" n'est pas valide" s'affiche et le code promo ne s'applique pas

Résultat attendu

Lors de l'ajout du code même promo, après avoir passé une commande qui a échoué, le code promo devrait s'appliqué

Informations complémentaires

Avec plugin aroundSubmit sur la "quote" , magento ajoute un message en queue. Ce message va incrémenter l'usage des coupons. Avec plugin afterCacncel" , magento va décrémenter l'usage des coupons.

Le soucis vient du fait qu'à part lors de votre cron qui "cancel" les commandes, votre module ne gère pas l'usage des coupons. Les methodes comme \HiPay\FullserviceMagento\Model\Notify::_doTransactionDenied ou \HiPay\FullserviceMagento\Model\Notify::_doTransactionFailure ne font que changer le statut de la commande. Il faut aussi gérer les coupons dans cette partie là.

JBaptisteLvt commented 1 year ago

Bonjour ! Merci pour cette issue, nous allons intégrer le fix dans notre ROADMAP dès que possible !

JBaptisteLvt commented 1 year ago

Fixed in 1.19.0 - 11/05/2023

Synoffs commented 1 year ago

Bonjour,

Je viens de tester la version 1.19.1 et malheureusement le problème n'est pas résolu. Dans cette version, à cet endroit hipay-fullservice-sdk-magento2/Model/Notify.php:1004, vous faites $this->updateCouponUsages->execute($this->_order, false); et ceci engendre cet erreur :

[2023-05-26T15:35:52.483538+00:00] report.CRITICAL: PDOException: SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`main`.`salesrule_customer`, CONSTRAINT `SALESRULE_CUSTOMER_RULE_ID_SEQUENCE_SALESRULE_SEQUENCE_VALUE` FOREIGN KEY (`rule_id`) REFERENCES `sequence_salesrule` (`sequence_value`) ON DELETE CASC) in /app/vendor/magento/framework/DB/Statement/Pdo/Mysql.php:90
Stack trace:
#0 /app/vendor/magento/framework/DB/Statement/Pdo/Mysql.php(90): PDOStatement->execute()
#1 /app/vendor/magento/framework/DB/Statement/Pdo/Mysql.php(106): Magento\Framework\DB\Statement\Pdo\Mysql->Magento\Framework\DB\Statement\Pdo\{closure}()
#2 /app/vendor/magento/framework/DB/Statement/Pdo/Mysql.php(91): Magento\Framework\DB\Statement\Pdo\Mysql->tryExecute(Object(Closure))
#3 /app/vendor/magento/zendframework1/library/Zend/Db/Statement.php(313): Magento\Framework\DB\Statement\Pdo\Mysql->_execute(Array)
#4 /app/vendor/magento/zendframework1/library/Zend/Db/Adapter/Abstract.php(480): Zend_Db_Statement->execute(Array)
#5 /app/vendor/magento/zendframework1/library/Zend/Db/Adapter/Pdo/Abstract.php(238): Zend_Db_Adapter_Abstract->query('INSERT INTO `sa...', Array)
#6 /app/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php(564): Zend_Db_Adapter_Pdo_Abstract->query('INSERT INTO `sa...', Array)
#7 /app/vendor/magento/framework/DB/Adapter/Pdo/Mysql.php(631): Magento\Framework\DB\Adapter\Pdo\Mysql->_query('INSERT INTO `sa...', Array)
#8 /app/vendor/magento/zendframework1/library/Zend/Db/Adapter/Abstract.php(576): Magento\Framework\DB\Adapter\Pdo\Mysql->query('INSERT INTO `sa...', Array)
#9 /app/vendor/magento/framework/Model/ResourceModel/Db/AbstractDb.php(759): Zend_Db_Adapter_Abstract->insert('salesrule_custo...', Array)
#10 /app/vendor/magento/framework/Model/ResourceModel/Db/AbstractDb.php(399): Magento\Framework\Model\ResourceModel\Db\AbstractDb->saveNewObject(Object(Magento\SalesRule\Model\Rule\Customer))
#11 /app/vendor/magento/framework/Model/AbstractModel.php(658): Magento\Framework\Model\ResourceModel\Db\AbstractDb->save(Object(Magento\SalesRule\Model\Rule\Customer))
#12 /app/vendor/magento/module-sales-rule/Model/Coupon/Usage/Processor.php(147): Magento\Framework\Model\AbstractModel->save()
#13 /app/vendor/magento/module-sales-rule/Model/Coupon/Usage/Processor.php(81): Magento\SalesRule\Model\Coupon\Usage\Processor->updateCustomerRuleUsages(false, 144, 10000042)
#14 /app/vendor/magento/module-sales-rule/Model/Coupon/UpdateCouponUsages.php(62): Magento\SalesRule\Model\Coupon\Usage\Processor->process(Object(Magento\SalesRule\Model\Coupon\Usage\UpdateInfo))
#15 /app/vendor/hipay/hipay-fullservice-sdk-magento2/Model/Notify.php(1004): Magento\SalesRule\Model\Coupon\UpdateCouponUsages->execute(Object(Magento\Sales\Model\Order\Interceptor), false)
#16 /app/vendor/hipay/hipay-fullservice-sdk-magento2/Model/Notify.php(414): HiPay\FullserviceMagento\Model\Notify->_doTransactionFailure()
#17 /app/vendor/hipay/hipay-fullservice-sdk-magento2/Controller/Notify/Index.php(121): HiPay\FullserviceMagento\Model\Notify->processTransaction()
#18 /app/vendor/magento/framework/Interception/Interceptor.php(58): HiPay\FullserviceMagento\Controller\Notify\Index->execute()
#19 /app/vendor/magento/framework/Interception/Interceptor.php(138): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->___callParent('execute', Array)
#20 /app/vendor/magento/framework/Interception/Interceptor.php(153): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->Magento\Framework\Interception\{closure}()
#21 /app/generated/code/HiPay/FullserviceMagento/Controller/Notify/Index/Interceptor.php(23): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->___callPlugins('execute', Array, Array)
#22 /app/vendor/magento/framework/App/Action/Action.php(111): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->execute()
#23 /app/vendor/magento/framework/Interception/Interceptor.php(58): Magento\Framework\App\Action\Action->dispatch(Object(Magento\Framework\App\Request\Http))
#24 /app/vendor/magento/framework/Interception/Interceptor.php(138): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->___callParent('dispatch', Array)
#25 /app/vendor/magento/framework/Interception/Interceptor.php(153): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http))
#26 /app/generated/code/HiPay/FullserviceMagento/Controller/Notify/Index/Interceptor.php(32): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->___callPlugins('dispatch', Array, Array)
#27 /app/vendor/magento/framework/App/FrontController.php(245): HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor->dispatch(Object(Magento\Framework\App\Request\Http))
#28 /app/vendor/magento/framework/App/FrontController.php(212): Magento\Framework\App\FrontController->getActionResponse(Object(HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor), Object(Magento\Framework\App\Request\Http))
#29 /app/vendor/magento/framework/App/FrontController.php(147): Magento\Framework\App\FrontController->processRequest(Object(Magento\Framework\App\Request\Http), Object(HiPay\FullserviceMagento\Controller\Notify\Index\Interceptor))
#30 /app/vendor/magento/framework/Interception/Interceptor.php(58): Magento\Framework\App\FrontController->dispatch(Object(Magento\Framework\App\Request\Http))
#31 /app/vendor/magento/framework/Interception/Interceptor.php(138): Magento\Framework\App\FrontController\Interceptor->___callParent('dispatch', Array)
#32 /app/vendor/magento/module-store/App/FrontController/Plugin/RequestPreprocessor.php(99): Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http))
#33 /app/vendor/magento/framework/Interception/Interceptor.php(135): Magento\Store\App\FrontController\Plugin\RequestPreprocessor->aroundDispatch(Object(Magento\Framework\App\FrontController\Interceptor), Object(Closure), Object(Magento\Framework\App\Request\Http))
#34 /app/vendor/fastly/magento2/Model/FrontControllerPlugin.php(128): Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http))
#35 /app/vendor/magento/framework/Interception/Interceptor.php(135): Fastly\Cdn\Model\FrontControllerPlugin->aroundDispatch(Object(Magento\Framework\App\FrontController\Interceptor), Object(Closure), Object(Magento\Framework\App\Request\Http))
#36 /app/vendor/magento/module-page-cache/Model/App/FrontController/BuiltinPlugin.php(75): Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http))
#37 /app/vendor/magento/framework/Interception/Interceptor.php(135): Magento\PageCache\Model\App\FrontController\BuiltinPlugin->aroundDispatch(Object(Magento\Framework\App\FrontController\Interceptor), Object(Closure), Object(Magento\Framework\App\Request\Http))
#38 /app/vendor/magento/framework/Interception/Interceptor.php(153): Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}(Object(Magento\Framework\App\Request\Http))
#39 /app/generated/code/Magento/Framework/App/FrontController/Interceptor.php(23): Magento\Framework\App\FrontController\Interceptor->___callPlugins('dispatch', Array, NULL)
#40 /app/vendor/magento/framework/App/Http.php(116): Magento\Framework\App\FrontController\Interceptor->dispatch(Object(Magento\Framework\App\Request\Http))
#41 /app/vendor/magento/framework/App/Bootstrap.php(264): Magento\Framework\App\Http->launch()
#42 /app/pub/index.php(30): Magento\Framework\App\Bootstrap->run(Object(Magento\Framework\App\Http\Interceptor))
#43 {main}

Le problème, c'est que vous faites un décrément alors que les tables de salesrule_customer et salesrule_coupon_usage ne sont toujours pas remplies . L'utilisation des coupons est faite en asynchrone en utilisant les queue db magento.

image

Tant que les "consumers" ne sont pas passés pour prendre en compte les coupons, vous ne pouvez pas décrémenter les utilisations des coupons. Il vous faut donc gérer avec les queue db magento cette décrémentation des utilisations.

JBaptisteLvt commented 1 year ago

Bonjour ! Merci pour ce retour, on va regarder ça dès que possible :)

ffssnyolia commented 1 year ago

Bonjour ! Un update sur ce ticket ?

JBaptisteLvt commented 1 year ago

Bonjour, Malheureusement nous sommes une petite équipe, nous traitons ce sujet dès que possible.

walkwizus commented 12 months ago

Bonjour,

Après des tests supplémentaires, il semble que Magento ne prenne pas en compte nativement le décrément d'un coupon de réduction lors d'une commande annulée, remboursée etc...

Aussi, je pense que cela ne devrait pas être prévu directement dans HiPay (ou autre moyen de paiement) car cela pourrait venir en doublon dans le cas où cette fonction serait développée dans un module tiers ou deviendrait native dans Magento.

De mon côté, j'ai donc patché le module HiPay afin de supprimer le décrément du coupon (PR que j'avais fait ici: https://github.com/hipay/hipay-fullservice-sdk-magento2/pull/157) afin de revenir sur la version précédente.

Actuellement le module HiPay ne fait que modifier le statut d'une commande lors de l'IPN:

J'ai rajouté l'annulation de la commande via l'interface: Magento\Sales\Api\OrderManagementInterface comme dans le cron PendingOrders afin de trigger les évènements Magento:

Index: Model/Notify.php
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/Model/Notify.php b/Model/Notify.php
--- a/Model/Notify.php
+++ b/Model/Notify.php  (date 1696846988599)
@@ -31,7 +31,7 @@
 use Magento\Sales\Api\Data\OrderInterface;
 use Magento\Sales\Model\Order\Invoice;
 use Magento\Sales\Api\Data\OrderPaymentInterface;
-use Magento\SalesRule\Model\Coupon\UpdateCouponUsages;
+use Magento\Sales\Api\OrderManagementInterface;

 /**
  * Notify Class Model
@@ -146,10 +146,7 @@
      */
     protected $creditmemoSender;

-    /**
-     * @var UpdateCouponUsages
-     */
-    protected $updateCouponUsages;
+    protected $orderManagement;

     public function __construct(
         TransactionRepository $transactionRepository,
@@ -165,7 +162,7 @@
         \Magento\Framework\DB\Transaction $_transactionDB,
         \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency,
         \Magento\Sales\Model\Order\Email\Sender\CreditmemoSender $creditmemoSender,
-        UpdateCouponUsages $updateCouponUsages,
+        OrderManagementInterface $orderManagement,
         $params = []
     ) {
         $this->_orderFactory = $orderFactory;
@@ -184,7 +181,7 @@

         $this->creditmemoSender = $creditmemoSender;

-        $this->updateCouponUsages = $updateCouponUsages;
+        $this->orderManagement = $orderManagement;

         if (isset($params['response']) && is_array($params['response'])) {
             $incrementId = $params['response']['order']['id'];
@@ -969,10 +966,9 @@
             ->deny(false);

         $orderStatus = $this->_order->getPayment()->getMethodInstance()->getConfigData('order_status_payment_refused');
+        $this->orderManagement->cancel($this->_order->getId());
         $this->_order->setStatus($orderStatus);
-
         $this->_order->save();
-        $this->updateCouponUsages->execute($this->_order, false);
     }

     /**
@@ -994,9 +990,9 @@
                 'order_status_payment_canceled'
             );
         }
+        $this->orderManagement->cancel($this->_order->getId());
         $this->_order->setStatus($orderStatus);
         $this->_order->save();
-        $this->updateCouponUsages->execute($this->_order, false);
     }

     /**

J'ai ensuite fait un petit module additionnel qui permet de décrémenter un coupon lors de l'annulation d'une commande. Un simple observer sur l'event sales_order_payment_cancel qui est trigger lors de l'annulation d'une commande (d'ou la nécessité d'annuler la commande dans la classe Notify).

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="sales_order_payment_cancel">
        <observer name="observer" instance="Vendor\SalesRule\Observer\UpdateCouponUsages"/>
    </event>
</config>

Voici l'implémentation pour le décrément du coupon (de manière asynchrone):

<?php

declare(strict_types=1);

namespace Vendor\SalesRule\Observer;

use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\SalesRule\Model\Coupon\Quote\UpdateCouponUsages as MagentoUpdateCouponUsages;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Sales\Api\Data\OrderPaymentInterface;
use Magento\Sales\Api\Data\OrderInterface;

class UpdateCouponUsages implements ObserverInterface
{
    /**
     * @var CartRepositoryInterface
     */
    protected CartRepositoryInterface $cartRepository;

    /**
     * @var MagentoUpdateCouponUsages
     */
    protected MagentoUpdateCouponUsages $updateCouponUsages;

    /**
     * @param CartRepositoryInterface $cartRepository
     * @param MagentoUpdateCouponUsages $updateCouponUsages
     */
    public function __construct(
        CartRepositoryInterface $cartRepository,
        MagentoUpdateCouponUsages $updateCouponUsages
    ) {
        $this->cartRepository = $cartRepository;
        $this->updateCouponUsages = $updateCouponUsages;
    }

    /**
     * @param Observer $observer
     * @return void
     */
    public function execute(Observer $observer): void
    {
        /** @var OrderPaymentInterface $payment */
        $payment = $observer->getData('payment');

        /** @var OrderInterface $order */
        $order = $payment->getOrder();

        $quoteId = $order->getQuoteId();
        if ($quoteId && $order->getCouponCode()) {
            try {
                $cart = $this->cartRepository->get($quoteId);
                $this->updateCouponUsages->execute($cart, false);
            } catch (\Exception $e) {

            }
        }
    }
}

Pour finir, je pense que dans le champs de configuration "Order status when payment refused" il ne faudrait lister que les status de commande liés à l'état Canceled. J'ai également patché la classe: https://github.com/hipay/hipay-fullservice-sdk-magento2/blob/master/Model/System/Config/Source/Status/Refused.php afin de supprimer les états Holded (commande bloquée manuellement) et Closed (commande remboursée).

JBaptisteLvt commented 11 months ago

Bonjour !

Suite au commentaire de @walkwizus, nous avons fait une étude du fonctionnement des coupons Magento2 de notre côté, et il est apparu que leur gestion est très personnalisable, trop pour que cela ait du sens de le gérer dans le module HiPay. Nous avons supprimé la gestion que nous avions faite du module, en s'inspirant du patch proposé, et vous invitons à gérer de votre côté les coupons et leur régénération lors de l'annulation / remboursement des commandes.

walkwizus commented 11 months ago

Bonjour,

Ce n'est pas complètement résolu, il faut faire la même chose CleanPendingOrders.

JBaptisteLvt commented 11 months ago

Bonjour,

Je ne comprends pas ce qui ne va pas. Sur la Cron CleanPendingOrders, c'est déjà le OrderManagementInterface qui cancel l'order.

walkwizus commented 11 months ago

Oui, mais cela n'arrive jamais car la commande est annulée quelques lignes avant, donc le canCancel renvoie false: https://github.com/hipay/hipay-fullservice-sdk-magento2/blob/master/Cron/CleanPendingOrders.php#L204