<?php
namespace PrestaShop\Module\LCVPrestaConnector;

use PrestaShop\Module\LCVPrestaConnector\Models\Order;
use PrestaShop\Module\LCVPrestaConnector\Models\OrderDetail;
use PrestaShop\Module\LCVPrestaConnector\Models\OrderPayment;
use PrestaShop\Module\LCVPrestaConnector\Models\OrderVoucher;

/**
 * Synchronisation des commandes
 */
class SyncerOrders
{
    /**
     * Initialisé ?
     */
    private bool $inited = false;

    /**
     * Synchroniseur
     * 
     */
    private Syncer $syncer;
    /**
     * Indicateur de progression
     */
    private SyncProgress $progress;

    /**
     * Constructeur
     * 
     * @param Syncer $syncer Synchroniseur
     * @param SyncProgress|null $progress Indicateur de progression
     */
    public function __construct(Syncer $syncer, SyncProgress|null $progress = null)
    {
        $this->syncer = $syncer;
        $this->progress = $progress ?? new SyncProgress($syncer);
        $this->init();
    }

    /**
     * Initialisation
     * 
     */
    private function init()
    {
        if ($this->inited)
            return;

        // On charge l'état d'exportation des commandes ou on le crée s'il n'existe pas !
        $cfg = $this->syncer->module->getCfg();

        $export_state_id = $cfg->get(SyncConfiguration::CFG_ORDER_EXPORT_STATE);

        // On vérifie qu'on a bien un état d'exportation
        // On charge l'état d'exportation des commandes ou on le crée s'il n'existe pas !
        if ($export_state_id)
        {
            $export_state = new \OrderState($export_state_id);
            if (!\Validate::isLoadedObject($export_state))
                $export_state_id = null;
        }

        if (!$export_state_id)
        {
            // Création de l'état d'exportation des commandes
            $export_state = new \OrderState();
            $export_state->name = array();
            foreach (\Language::getLanguages() as $lang)
                $export_state->name[$lang['id_lang']] = 'Exportée vers '.$this->syncer->module->getBridge()->getId();
            $export_state->send_email = false;
            $export_state->color = '#fffcb5';
            $export_state->hidden = true;
            $export_state->delivery = false;
            $export_state->logable = true;
            $export_state->invoice = true;
            $export_state->unremovable = true;
            $export_state->paid = true;
            $export_state->module_name = $this->syncer->module->name;
            $export_state->deleted = false;
            $export_state->add();

            $export_state_id = $export_state->id;
            // Sauvegarde dans la configuration
            $cfg->set(SyncConfiguration::CFG_ORDER_EXPORT_STATE, $export_state_id);
        }

        // On vérifie qu'on a bien un état d'erreur d'exportation
        $error_export_state_id = $cfg->get(SyncConfiguration::CFG_ORDER_ERROR_EXPORT_STATE);
        if ($error_export_state_id)
        {
            $export_state = new \OrderState($error_export_state_id);
            if (!\Validate::isLoadedObject($export_state))
                $error_export_state_id = null;
        }

        if (!$error_export_state_id)
        {
            // Création de l'état d'exportation des commandes
            $export_state = new \OrderState();
            $export_state->name = array();
            foreach (\Language::getLanguages() as $lang)
                $export_state->name[$lang['id_lang']] = 'Erreur d\'export vers '.$this->syncer->module->getBridge()->getId();
            $export_state->send_email = false;
            $export_state->color = '#ff6661';
            $export_state->hidden = true;
            $export_state->delivery = false;
            $export_state->logable = true;
            $export_state->invoice = true;
            $export_state->paid = true;
            $export_state->unremovable = true;
            $export_state->module_name = $this->syncer->module->name;
            $export_state->deleted = false;
            $export_state->add();

            $export_state_id = $export_state->id;
            // Sauvegarde dans la configuration
            $cfg->set(SyncConfiguration::CFG_ORDER_ERROR_EXPORT_STATE, $export_state_id);
        }        

        $this->inited = true;
    }

    /**
     * Synchronisation des stocks
     */
    public function sync()
    {            
        $db = \Db::getInstance();

        $startTime = microtime(true);
        $nbOrders = 0;        

        // Etat "exportée"
        $cfg = $this->syncer->module->getCfg();

        $max_age = $cfg->get(SyncConfiguration::CFG_ORDER_MAX_TIME);
        if (!$max_age)
            $max_age = 90;

        $order_barrier = $cfg->get(SyncConfiguration::CFG_ORDER_BARRIER);
        if ($order_barrier < 0)
        {
            // L'exportation des commandes est désactivée
            // on le journalise dans l'audit et on sort
            $this->syncer->audit('[INF] L\'exportation des commandes est désactivée (barrière d\'exportation -1 dans les options)');
            return;
        }    

        $this->progress->startStep('export_orders', 'Exportation des commandes', '%d commandes exportées');
        try
        {
            if (!$order_barrier)
            {
                // La barrière n'est pas encore positionnée, pour éviter les accidents, on la positionne maintenant sur le plus haut id de commande !
                $sql = 'SELECT MAX(id_order) AS max_order FROM '._DB_PREFIX_.'orders';
                $order_barrier = $db->getValue($sql);
                if (!$order_barrier)
                    $order_barrier = 0;
                $cfg->set(SyncConfiguration::CFG_ORDER_BARRIER, $order_barrier);
            }
            
            // On revient systématiquement deux heures en arrière pour retraiter les éventuels retour en arrière (time adjustement)
            // ou les changements d'horaires.        
            $errorState = $cfg->get(SyncConfiguration::CFG_ORDER_ERROR_EXPORT_STATE);

            $sql = 'SELECT DISTINCT o.reference FROM '._DB_PREFIX_.'orders AS o WHERE
                        o.id_order > '.$order_barrier.' AND
                        o.current_state != '.$errorState.' AND
                        o.date_add > DATE_SUB(NOW(), INTERVAL '.$max_age.' DAY) AND
                        o.date_upd > DATE_SUB(NOW(), INTERVAL 30 DAY) AND
                        o.invoice_number != 0 AND
                        NOT EXISTS (SELECT id_order FROM '.$this->syncer->module->TblPrefix.'orders_updates WHERE id_order = o.id_order AND done = o.date_upd) 
                        ORDER BY o.id_order ASC';
            $orders = $db->executeS($sql);
            if ($orders === FALSE)
                throw new \Exception('Erreur à la récupération des commandes à synchroniser : '.$db->getMsgError());

            if ($orders)
            {            
                foreach ($orders as $order)
                {                    
                    try
                    {
                        if ($this->exportOrder($order['reference']))
                        {
                            // La commande a été synchronisée : on le marque dans le journal !
                            $this->syncer->audit("[INF] Commande ".$order['reference']." exportée !");
                        }
                    }
                    catch (\Exception $e)
                    {
                        $msg = "[ERR] Erreur lors de la synchronisation de la commande ".$order['reference']. " : ".$e->getMessage();
                        $this->syncer->audit($msg);
                        // On met également le message dans le journal de prestashop en mode erreur, en liant à la commande
                        \PrestaShopLogger::addLog($msg, 3, null, $this->syncer->module->name); 
                    }
                    $nbOrders++;
                    $this->progress->progress('export_orders');
                }
            }
            else if ($orders === FALSE)
                throw new \Exception('Erreur à la récupération des commandes à synchroniser : '.$db->getMsgError());        
            
            $stopTime = microtime(true);
            if ($nbOrders > 0)
                $this->syncer->audit(sprintf("[INF] Synchronisation de %d commandes effectuée en %.2f secondes", $nbOrders, $stopTime - $startTime));    
        }
        catch (\Exception $e)
        {
            $this->syncer->audit(sprintf("[ERR] Erreur lors de l'exportation des commandes : %s", $e->getMessage()));
            $this->progress->error('export_orders', $e->getMessage());
        }
        finally
        {
            $this->progress->endStep('export_orders');
        }
    }

    /**
     * Conversion d'une commande Prestashop en commande exportable
     * 
     * @param array<\Order> $psOrders Commandes Prestashop     
     * @param bool $original Indique si on ne doit pas inclure les remboursements s'il y en a dans la commande
     * 
     * @return Order Commande exportable
     */
    private function psOrders2order(array $psOrders, bool $original) : Order
    {
        $cfg = $this->syncer->module->getCfg();
        $db = \Db::getInstance();
        
        // Numéro interne des commandes concernées
        $id_orders = array_map(function($order) { return $order->id; }, $psOrders);
                    
        // On calcule la nouvelle commande à exporter ici !
        $order = new Order();
        $order->reference = $psOrders[0]->reference;
        $order->id_orders = $id_orders;
        $order->id_customer = $psOrders[0]->id_customer;        
        $order->revision = 0;

        // On process les paiements
        $psPayments = \OrderPayment::getByOrderReference($order->reference);
        foreach ($psPayments as $payment)
        {
            $p = new OrderPayment();
            $p->method = $payment->payment_method;
            $p->amount = round($payment->amount / $payment->conversion_rate, 2);
            $p->transaction_id = $payment->transaction_id;                

            if (!$order->date)
                $order->date = (new \DateTime($payment->date_add))->format('Y-m-d H:i:s');

            $order->payments["P".$payment->id] = $p;
        }        

        // Chargement des produits de la commande ici
        // Un premier passage pour récupérer les produits
        foreach ($psOrders as $psOrder)
        {
            /**
             * Gestion des devises ici : $cv_rate
             * Les ventes se font exclusivement dans une seule devise, 
             * on calcul donc un taux de conversion
             */
            $cv_rate = 1.0 / $psOrder->conversion_rate;

            if ($psOrder->valid || $original)
            {
                foreach ($psOrder->getProducts() as &$orderDetail)
                {
                    $sku = $orderDetail["product_supplier_reference"];
                    if (!$sku)
                        $sku = $orderDetail["product_ean13"];

                    if (!$sku)
                        throw new \Exception(sprintf("Commande %s ignorée : produit sans référence fournisseur ni EAN13 : %s", $order->reference, $orderDetail["product_name"]));

                    $detail = new OrderDetail();                     
                    
                    $detail->product_supplier_reference = $orderDetail["product_supplier_reference"];
                    $detail->product_ean13 = $orderDetail["product_ean13"];
                    $detail->product_name = $orderDetail["product_name"];
                    $detail->total_price_tax_incl = round($orderDetail["total_price_tax_incl"] * $cv_rate, 2);
                    $detail->unit_price_tax_excl = round($orderDetail["unit_price_tax_excl"] * $cv_rate, 2);
                    $detail->original_product_price = round($orderDetail["original_product_price"] * $cv_rate, 2);
                    $detail->tax_rate = $orderDetail["tax_rate"];
                    
                    if (empty($detail->tax_rate))
                    {
                        // Le tax_rate est égal à 0, c'est peut-être normal, mais c'est curieux
                        // cela se produit si la base est endommagée sur la table ps_order_detail_tax
                        // On vérifie, dans le cas d'une taxe nulle $detail["unit_price_tax_excl"] et $detail["unit_price_tax_incl"]
                        // sont égaux, sinon, c'est qu'il faut recalculer le taux de taxe (arrondi à 2 décimales)
                        if (round($orderDetail["unit_price_tax_excl"], 3) != round($orderDetail["unit_price_tax_incl"], 3))
                            $detail->tax_rate = round((($orderDetail["unit_price_tax_incl"] / $orderDetail["unit_price_tax_excl"]) - 1) * 100, 3);
                    }

                    // le remboursement est indiquée sur la bonne ligne !
                    $returned = $orderDetail["product_quantity_refunded"] + $orderDetail["product_quantity_return"];
                    if ($returned && !$original)
                    {   
                        $detail->product_quantity = $orderDetail["product_quantity"] - $returned;

                        // le remboursement est indiquée sur la bonne ligne !
                        $detail->total_refund = round($orderDetail["total_refunded_tax_incl"] * $cv_rate, 2);
                        $detail->total_price_tax_incl -= $detail->total_refund;
                    }
                    else
                        $detail->product_quantity = $orderDetail["product_quantity"];
                    
                    if ($detail->product_quantity > 0)
                        $order->details["D".$orderDetail["id_order_detail"]] = $detail;
                }
            }
        }

        // Ici on charge les order_slip pour les frais de livraison et d'emballage
        $results = $db->executeS(sprintf("SELECT SUM(`shipping_cost_amount` / `conversion_rate`) AS shipping FROM `"._DB_PREFIX_."order_slip` WHERE `id_order` IN (%s)", join(', ', $id_orders)));
        if ($results === false)
            throw new \Exception('Erreur à la récupération des frais de port : '.$db->getMsgError());

        $shipping_refund = 0;
        foreach ($results as $row)
            $shipping_refund += round($row['shipping'], 2);

        // Un deuxième passage pour les frais annexes
        foreach ($psOrders as $psOrder)
        {
            /**
             * Gestion des devises ici : $cv_rate
             * Les ventes se font exclusivement dans une seule devise, 
             * on calcul donc un taux de conversion
             */
            $cv_rate = 1.0 / $psOrder->conversion_rate;

            if ($psOrder->valid || $original)
            {
                // Ajout des frais de port
                if ($psOrder->total_shipping_tax_incl > 0 || $psOrder->total_shipping_tax_excl > 0)
                {
                    $ref_shipping = $cfg->get(SyncConfiguration::CFG_SHIPPING_COST_REF);
                    if (!$ref_shipping)
                        throw new \Exception("Référence pour les frais de port non défini : merci de préciser la référence ".$this->syncer->module->getBridge()->getId()." du produit désignant les frais de port !");

                    $detail = new OrderDetail();
                    $detail->product_supplier_reference = $ref_shipping;
                    $detail->product_ean13 = "";
                    $detail->product_quantity = 1;
                    $detail->product_name = "Frais de port";
                    $detail->total_price_tax_incl = round($psOrder->total_shipping_tax_incl * $cv_rate, 2);
                    if (!$original)
                    {
                        $detail->total_price_tax_incl -= $shipping_refund;
                        if ($detail->total_price_tax_incl == 0)
                            $detail->product_quantity = 0;
                        else
                            $detail->product_quantity =  $detail->total_price_tax_incl < 0 ? -1 : 1;
                    }
                    $detail->unit_price_tax_excl = round($psOrder->total_shipping_tax_excl * $cv_rate, 2);
                    $detail->original_product_price = round($psOrder->total_shipping_tax_excl * $cv_rate, 2);
                    $detail->tax_rate = $psOrder->carrier_tax_rate;

                    if ($detail->product_quantity != 0)
                        $order->details["FP" . $detail->total_price_tax_incl] = $detail;
                }

                if ($psOrder->total_wrapping_tax_incl > 0 || $psOrder->total_wrapping_tax_excl > 0)
                {
                    $wrapping_ref = $cfg->get(SyncConfiguration::CFG_WRAPPING_COST_REF);
                    if (!$wrapping_ref)
                        throw new \Exception("Référence pour les emballages non défini : merci de préciser la référence ".$this->syncer->module->getBridge()->getId()." du produit désignant les frais d'emballage !");
                        
                    $detail = new OrderDetail();
                    $detail->product_supplier_reference = $wrapping_ref;
                    $detail->product_ean13 = "";
                    $detail->product_quantity = 1;
                    $detail->product_name = "Frais d'emballage";
                    $detail->total_price_tax_incl = round($psOrder->total_wrapping_tax_incl * $cv_rate, 2);
                    $detail->unit_price_tax_excl = round($psOrder->total_wrapping_tax_excl * $cv_rate, 2);
                    $detail->original_product_price = round($psOrder->total_wrapping_tax_excl * $cv_rate, 2);
                    $detail->tax_rate = $psOrder->carrier_tax_rate;

                    $order->details["FE"] = $detail;
                }
            }
        }

        // Intégration des paiements 
        $total_paid = 0;           
        foreach ($psOrders as $psOrder)
        {
            /**
             * Gestion des devises ici : $cv_rate
             * Les ventes se font exclusivement dans une seule devise, 
             * on calcul donc un taux de conversion
             */
            $cv_rate = 1.0 / $psOrder->conversion_rate;

            $total_paid += round($psOrder->total_paid * $cv_rate, 2);
            $cartRules = $psOrder->getCartRules();                
            foreach ($cartRules as $cartRule)
            {
                $voucher = new OrderVoucher();
                $voucher->amount = round($cartRule['value'] * $cv_rate, 2);

                $rule = new \CartRule($cartRule['id_cart_rule']);

                if ($rule && $rule->id)
                    $voucher->code = $rule->code;
                if (!$voucher->code)
                    $voucher->code = "V".$cartRule['id_cart_rule'];
                
                $order->vouchers["V".$cartRule['id_cart_rule']] = $voucher;
            }
        }

        // Ensuite on ajoute les bons de réduction émis ou les remboursements, et on refait le check
        if (!$original)
        {
            // Ajout des émissions de vouchers ici !!
            // On sélectionne les bons de réduction émis pour cette commande, ils sont dans la table ps_cart_rule
            // avec un lien vers le même id_customer et a son code est strictement égal à "V<id_cart_rule>C<id_customer>O<id_order>"                
            $where = [];
            foreach ($psOrders as $psOrder)
                $where[] = sprintf("`code` = CONCAT('V', `id_cart_rule`, 'C%dO%d')", $psOrder->id_customer, $psOrder->id);

            $results = $db->executeS(sprintf("SELECT `id_cart_rule`, `code`, `reduction_amount` FROM `"._DB_PREFIX_."cart_rule` WHERE
                        `id_customer` = %d AND (%s)", $order->id_customer, join(' OR ', $where)));
            if ($results === FALSE)
                throw new \Exception('Erreur à la récupération des bons de réduction émis : '.$db->getMsgError());

            foreach ($results as $voucher)
            {
                // On traite les bons de réductions émis comme des remises négatives !!!                    
                $v = new OrderVoucher();
                $v->code = $voucher['code'];
                $v->amount = -round($voucher['reduction_amount'] * $cv_rate, 2);

                $order->vouchers["VE".$voucher['id_cart_rule']] = $v;
            }                

            // Ok, on calcule la différence entre ce qui a été réglé et le montant total des produits
            $diffTotal = 0;

            // Cette différence sera interprétée comme un remboursement                
            foreach ($order->details as $d)
                $diffTotal -= $d->total_price_tax_incl;                

            // On retranche tous les paiements
            foreach ($order->payments as $p)
                $diffTotal += $p->amount;

            // On retranche tous les vouchers
            foreach ($order->vouchers as $p)
                $diffTotal += $p->amount;                

            $diffTotal = round($diffTotal, 2);
            if ($diffTotal != 0)
            {
                // Le montant à rembourser
                $rmb = $diffTotal;

                // On regarde le moyen de rmb                
                foreach ($order->payments as $k => $p)
                {
                    $amount = min($p->amount, $rmb);                    

                    $paiement = new OrderPayment();
                    $paiement->method = $p->method;
                    $paiement->amount = -$amount;
                    $order->payments['R-'.$k] = $paiement;

                    $rmb -= $amount;
                    if ($rmb <= 0)
                        break;
                }

                if ($rmb != 0)
                {
                    // Visiblement, on doit compléter !
                    // On va le faire avec un voucher virtuel 
                    $voucher = new OrderVoucher();
                    $voucher->amount = -$rmb;
                    $voucher->code = "RMB";
                    $order->vouchers['RMB'] = $voucher;
                }
            }
        }

        return $order;
    }

    /**
     * Synchronisation de la commande précisée
     * On résonne en référence car on doit exporter une commande entière, même si sur Prestashop, elle est divisée en
     * plus petite part (expédition différente) ; à cause du règlement commun.
     * 
     * @param string $reference Référence de la commande à exporter
     * @param bool $primo Indique si on doit autodétecter la première exportation de la commande. Si faux, alors forcément traiter celle-ci comme une mise à jour.
     * 
     * @return bool Vrai si la commande a été exportée, faux sinon
     */
    public function exportOrder(string $reference, bool $primo = true) : bool
    {
        $cfg = $this->syncer->module->getCfg();
        $db = \Db::getInstance();
        
        try
        {
            if (!$reference)
                throw new \Exception('Référence de commande vide !');
            
            // On charge les commandes concernées
            $psOrders = \Order::getByReference($reference)->getAll()->getResults();
            if (!$psOrders)
                throw new \Exception(sprintf('Commande %s introuvable !', $reference));

            // Numéro interne des commandes concernées
            $id_orders = array_map(function($order) { return $order->id; }, $psOrders);

            // Vérification des paiements
            $total_paid = 0;           
            foreach ($psOrders as $psOrder)
                $total_paid += $psOrder->total_paid;

            // On process les paiements
            $check_total = 0;
            foreach (\OrderPayment::getByOrderReference($reference) as $payment)
                $check_total += $payment->amount;

            if ($check_total != $total_paid)
            {
                $this->syncer->audit(sprintf("[WRN] Commande %s ignorée : le montant payé (%.2f) ne correspond pas au montant de la commande (%.2f) !",
                    $reference, $check_total, $total_paid));
                return false;
            }
            
            // Ancienne version ?
            // On charge la dernière version de la commande qui a été envoyée !
            $revision = 0;
            $lastVersion = null;
            $lastDiff = null;

            // On charge la dernière versionexportée de la commande
            // formattage avec sprintf
            $results = $db->executeS(sprintf("SELECT revision, `snapshot`, `pushed` FROM `".$this->syncer->module->TblPrefix."orders_updates` WHERE `id_order` IN (%s) ORDER BY `id_order` ASC", 
                                                join(', ', $id_orders)));
            if ($results === FALSE)
                throw new \Exception('Erreur à la récupération des commandes exportées : '.$db->getMsgError());

            if ($results)
            {
                foreach ($results as $row)
                {
                    if ($row['snapshot']) $lastVersion = unserialize(base64_decode($row['snapshot']));
                    if ($row['pushed']) $lastDiff = unserialize(base64_decode($row['pushed']));

                    $revision = ((int) $row['revision']) + 1;
                }
            }

            $order = $this->psOrders2order($psOrders, $revision == 0);            

            // On détermine le tag d'export de la commande
            $order->revision = $revision;
            $order->tag = substr($psOrders[0]->date_add, 0, 4) . '/' . $reference . ($order->revision > 0 ? '/'.$order->revision : '');                       
                        
            if ($order->revision == 0)
            {
                // Commande originale
                if (count($order->details) > 0) // Quelque chose à exporter ?
                {
                    $exportWrn = $this->syncer->bridge->pushOrder($order);
                    
                    if (!$exportWrn)
                    {
                        // On met la commande en exportée
                        foreach ($psOrders as $psOrder)
                        {
                            $history = new \OrderHistory();
                            $history->id_order = $psOrder->id;
                            $history->id_order_state = $cfg->get(SyncConfiguration::CFG_ORDER_EXPORT_STATE);
                            $history->id_employee = 0;
                            $history->save();
                        }
                    }
                    else
                    {
                        // On signale dans le journal d'audit que la commande a été ignorée et la raison
                        $this->syncer->audit(sprintf("[INF] Commande %s ignorée : %s", $reference, $exportWrn));
                        return false;
                    }
                }
                else
                    $order = null;

                // On stocke la nouvelle version exportée (commande vide) !
                foreach ($psOrders as $psOrder)
                {
                    // On insère avec un INSERT INTO orders_updates (id_order, revision, snapshot, pushed, done) ... DUPLICATE KEY (revision, snapshot, pushed, done)
                    if ($db->execute(sprintf('INSERT INTO `'.$this->syncer->module->TblPrefix.'orders_updates` (`id_order`, `revision`, `snapshot`, `pushed`, `done`) VALUES (%1$d, %2$d, \'%3$s\', \'%4$s\', \'%5$s\') ON DUPLICATE KEY UPDATE `revision` = %2$d, `snapshot` = \'%3$s\', `pushed` = \'%4$s\', `done` = \'%5$s\'',
                                    $psOrder->id, 
                                    $order->revision, 
                                    base64_encode(serialize($order)), 
                                    '', 
                                    $psOrder->date_upd)) === false)                    
                        throw new \Exception('Erreur à l\'enregistrement de la commande exportée : '.$db->getMsgError());
                }

                // On a fini avec l'exportation.
                // Si c'était une primo-exportation et qu'on a le flag, on se rappel pour être sûr
                // de traiter les éventuels remboursements
                if ($primo && $order->revision == 0)
                    $this->exportOrder($reference, false);
            }
            else 
            {                
                // Différence entre les deux commandes ?
                // comparons ce qui est comparable sur les commandes
                $same = false;
                if ($lastVersion)
                {
                    $same = true;

                    foreach (["details", "payments", "id_customer", "vouchers"] as $prop)
                    {
                        if (json_encode($order->$prop) != json_encode($lastVersion->$prop))
                        {                            
                            $same = false;
                            break;
                        }
                    }
                }

                if (!$same && $cfg->get(SyncConfiguration::CFG_ORDER_EXPORT_REFUND))
                {                    
                    // Si on a une ancienne modification, on la revert !
                    if ($lastDiff)
                    {
                        // On signale dans l'audit comme quoi on avait déjà envoyée la modification de la commande, du coup on annule notre dernière modification
                        $this->syncer->audit(sprintf("[INF] Commande %s : annulation de la modification %s", $reference, $lastDiff->tag));                        
                        $this->syncer->bridge->pushRevertOrder($lastDiff);
                        // On stocke la nouvelle version exportée (commande vide) !
                        foreach ($psOrders as $psOrder)
                        {                        
                            // On modifie orders_updates
                            if ($db->execute(sprintf('UPDATE `'.$this->syncer->module->TblPrefix.'orders_updates` SET `pushed` = \'\' WHERE `id_order` = %1$d', $psOrder->id)) === false)
                                throw new \Exception('Erreur à l\'enregistrement de la commande exportée : '.$db->getMsgError());
                        }
                    }                    

                    // On repart toujours de la commande d'origine
                    $lastVersion = $this->psOrders2order($psOrders, true);

                    $this->syncer->audit(sprintf("[INF] Commande %s : exportation de la modification %s", $reference, $order->tag));
                    
                    // On calcule la commande différentielle                    
                    $diffOrder = new Order();
                    $diffOrder->reference = $reference;
                    $diffOrder->id_customer = $psOrders[0]->id_customer;
                    $diffOrder->tag = $order->tag;
                    $diffOrder->date = $psOrders[0]->date_upd;
                    $diffOrder->revision = $order->revision;                    
                    
                    // Pour chaque élément présent dans l'ancienne commande mais plus dans la nouvelle, on enregistre le retour.
                    // A contrario, on enregistre une addition.
                    foreach ($order->details as $key => $detail)
                    {
                        $oldDetail = isset($lastVersion->details[$key]) ? $lastVersion->details[$key] : null;

                        $diffDetail = new OrderDetail();
                        $diffDetail->product_supplier_reference = $detail->product_supplier_reference;
                        $diffDetail->product_ean13 = $detail->product_ean13;
                        $diffDetail->product_name = $detail->product_name;                            
                        $diffDetail->unit_price_tax_excl = $detail->unit_price_tax_excl;
                        $diffDetail->original_product_price = $detail->original_product_price;
                        $diffDetail->tax_rate = $detail->tax_rate;                            

                        if ($oldDetail)
                        {
                            // Ce détail existait déjà dans l'ancienne commande, on ajoute donc la différence
                            $diffDetail->product_quantity = $detail->product_quantity - $oldDetail->product_quantity;
                            $diffDetail->total_price_tax_incl = round($detail->total_price_tax_incl - $oldDetail->total_price_tax_incl, 2);
                        }
                        else
                        {
                            // Ce nouveau détail n'existe pas dans l'ancien, on ajoute donc toutes les quantités
                            $diffDetail->product_quantity = $detail->product_quantity;                                
                            $diffDetail->total_price_tax_incl = $detail->total_price_tax_incl;
                        }

                        if ($diffDetail->product_quantity != 0)
                        {
                            $diffOrder->details[] = $diffDetail;                                
                        }
                    }

                    // Et il faut retirer également tous les détails qui existaient dans l'ancienne commande et qui n'existent plus dans la nouvelle
                    foreach ($lastVersion->details as $key => $detail)
                    {
                        if (!isset($order->details[$key]))
                        {
                            $diffDetail = new OrderDetail();
                            $diffDetail->product_supplier_reference = $detail->product_supplier_reference;
                            $diffDetail->product_ean13 = $detail->product_ean13;
                            $diffDetail->product_name = $detail->product_name;                            
                            $diffDetail->unit_price_tax_excl = $detail->unit_price_tax_excl;
                            $diffDetail->original_product_price = $detail->original_product_price;
                            $diffDetail->tax_rate = $detail->tax_rate;                            
                            $diffDetail->product_quantity = -$detail->product_quantity;
                            $diffDetail->total_price_tax_incl = -round($detail->total_price_tax_incl, 2);

                            if ($diffDetail->product_quantity != 0)
                                $diffOrder->details[] = $diffDetail;
                        }
                    }                        
                    
                    // On s'occupe des nouveaux paiements 
                    foreach ($order->payments as $key => $payment)
                    {
                        if (!isset($lastVersion->payments[$key]))
                        {
                            $diffPayment = new OrderPayment();
                            $diffPayment->method = $payment->method;
                            $diffPayment->amount = $payment->amount;
                            $diffPayment->transaction_id = $payment->transaction_id;

                            $diffOrder->payments[] = $diffPayment;
                        }
                    }

                    // Les paiements qui ont disparus ?
                    foreach ($lastVersion->payments as $key => $payment)
                    {
                        if (!isset($order->payments[$key]))
                        {
                            $diffPayment = new OrderPayment();
                            $diffPayment->method = $payment->method;
                            $diffPayment->amount = -$payment->amount;
                            $diffPayment->transaction_id = $payment->transaction_id;

                            $diffOrder->payments[] = $diffPayment;
                        }
                    }

                    // Les nouveaux vouchers
                    foreach ($order->vouchers as $key => $voucher)
                    {
                        if (!isset($lastVersion->vouchers[$key]))
                        {
                            $diffVoucher = new OrderVoucher();
                            $diffVoucher->code = $voucher->code;
                            $diffVoucher->amount = $voucher->amount;

                            $diffOrder->vouchers[] = $diffVoucher;
                        }
                    }

                    // Ceux qui ont disparus
                    foreach ($lastVersion->vouchers as $key => $voucher)
                    {
                        if (!isset($order->vouchers[$key]))
                        {
                            $diffVoucher = new OrderVoucher();
                            $diffVoucher->code = $voucher->code;
                            $diffVoucher->amount = -$voucher->amount;

                            $diffOrder->vouchers[] = $diffVoucher;
                        }
                    }                    

                    if (!$diffOrder->details && !$diffOrder->payments && !$diffOrder->vouchers)
                        $diffOrder = null;

                    if ($diffOrder)
                    {
                        // On pousse la commande !
                        $exportWrn = $this->syncer->bridge->pushOrder($diffOrder);
                        if ($exportWrn)
                        {
                            // On signale dans le journal d'audit que la commande a été ignorée et la raison
                            $this->syncer->audit(sprintf("[INF] Commande %s ignorée : %s", $reference, $exportWrn));
                            return false;
                        }
                    }

                    // On met la commande en exportée
                    foreach ($psOrders as $psOrder)
                    {
                        $history = new \OrderHistory();
                        $history->id_order = $psOrder->id;
                        $history->id_order_state = $cfg->get(SyncConfiguration::CFG_ORDER_EXPORT_STATE);
                        $history->id_employee = 0;
                        $history->save();
                    }

                    // On stocke la nouvelle version exportée (commande vide) !
                    foreach ($psOrders as $psOrder)
                    {
                        // On insère avec un INSERT INTO orders_updates (id_order, revision, snapshot, pushed, done) ... DUPLICATE KEY (revision, snapshot, pushed, done)
                        if ($db->execute(sprintf('UPDATE `'.$this->syncer->module->TblPrefix.'orders_updates` SET `revision` = %2$d, `snapshot` = \'%3$s\', `pushed` = \'%4$s\', `done` = \'%5$s\' WHERE `id_order` = %1$d',
                                        $psOrder->id, 
                                        $order->revision, 
                                        base64_encode(serialize($order)), 
                                        base64_encode(serialize($diffOrder)), 
                                        $psOrder->date_upd)) === false)                    
                            throw new \Exception('Erreur à l\'enregistrement de la commande exportée : '.$db->getMsgError());
                    }
                }
                else
                {
                    // Pas de modification :)
                    // On marque les commandes comme ayant été traitée !
                    foreach ($psOrders as $psOrder)
                    {                 
                        if ($db->execute(sprintf('UPDATE `'.$this->syncer->module->TblPrefix.'orders_updates` SET `done` = \'%2$s\' WHERE `id_order` = %1$d',
                            $psOrder->id, 
                            $psOrder->date_upd)) === false)
                            throw new \Exception('Erreur à l\'enregistrement de la commande exportée : '.$db->getMsgError());
                    }                    
                    return false;
                }
            }

            // Et, c'est tout bon !
            return true;
        }
        catch (\Exception $e)
        {            
            // On met les commandes en erreur, sans déclencher de hook, sauf si l'erreur contient "failed to connect"
            // qui sous-entendu un problème de connexion
            if ($primo && $psOrders && stripos($e->getMessage(), "failed to connect") === false)
            {
                // quand $primo est faux, on est déjà appelé dans un catch, la propagation du dessous va régler le soucis !
                foreach ($psOrders as $psOrder)
                {
                    $history = new \OrderHistory();
                    $history->id_order = $psOrder->id;
                    $history->id_order_state = $cfg->get(SyncConfiguration::CFG_ORDER_ERROR_EXPORT_STATE);
                    $history->id_employee = 0;
                    $history->save();
                }
            }

            throw $e;   
        }        
    }

    /**
     * Obtient vrai si la commande est synchronisable
     * 
     * @param string $reference Référence de la commande à exporter
     * 
     * @return bool Vrai si la commande est exportable, faux sinon
     */
    public function isExportableOrder(string $reference) : bool
    {
        $cfg = $this->syncer->module->getCfg();
        $db = \Db::getInstance();
        
        if (!$reference)
            return false;
        
        // On charge les commandes concernées
        $psOrders = \Order::getByReference($reference)->getAll()->getResults();
        if (!$psOrders)
            return false;

        // Numéro interne des commandes concernées
        $id_orders = array_map(function($order) { return $order->id; }, $psOrders);

        // Vérification des paiements
        $total_paid = 0;           
        foreach ($psOrders as $psOrder)
            $total_paid += $psOrder->total_paid;

        // On process les paiements
        $check_total = 0;
        foreach (\OrderPayment::getByOrderReference($reference) as $payment)
            $check_total += $payment->amount;

        if ($check_total != $total_paid)
            return false;
        
        // Ancienne version ?
        // On charge la dernière version de la commande qui a été envoyée !
        $revision = 0;
        $lastVersion = null;

        // On charge la dernière versionexportée de la commande
        // formattage avec sprintf
        $results = $db->executeS(sprintf("SELECT revision, `snapshot`, `pushed` FROM `".$this->syncer->module->TblPrefix."orders_updates` WHERE `id_order` IN (%s) ORDER BY `id_order` ASC", 
                                            join(', ', $id_orders)));
        if ($results === FALSE)
            throw new \Exception('Erreur à la récupération des commandes exportées : '.$db->getMsgError());

        if ($results)
        {
            foreach ($results as $row)
            {
                if ($row['snapshot']) $lastVersion = unserialize(base64_decode($row['snapshot']));
                if ($row['pushed']) $lastDiff = unserialize(base64_decode($row['pushed']));

                $revision = ((int) $row['revision']) + 1;
            }
        }

        $order = $this->psOrders2order($psOrders, $revision == 0);            

        // On détermine le tag d'export de la commande
        $order->revision = $revision;
        $order->tag = substr($psOrders[0]->date_add, 0, 4) . '/' . $reference . ($order->revision > 0 ? '/'.$order->revision : '');                       
                    
        if ($order->revision == 0)
        {
            // Commande originale
            if (count($order->details) > 0) // Quelque chose à exporter ?
                return true;                
        }
        else 
        {                
            // Différence entre les deux commandes ?
            // comparons ce qui est comparable sur les commandes
            $same = false;
            if ($lastVersion)
            {
                $same = true;

                foreach (["details", "payments", "id_customer", "vouchers"] as $prop)
                {
                    if (json_encode($order->$prop) != json_encode($lastVersion->$prop))
                    {                            
                        $same = false;
                        break;
                    }
                }
            }

            if (!$same && $cfg->get(SyncConfiguration::CFG_ORDER_EXPORT_REFUND))
                return true;                                
        }
        
        return false;     
    }
}