Problème de doublons sur les numéros de facture sous OS-Commerce

Ce problème intervient lors de l’utilisation de la contribution VADS.

Sur votre boutique qui présente ce problème, faites le test suivant : avec deux navigateurs (Internet Explorer et Firefox par exemple), vous lancer deux commandes avec deux comptes clients de test distincts, jusqu’à l’étape de de confirmation de la commande en choisissant un paiement par carte bancaire. Là, vous éditer le code source de la page à la recherche du bouton de confirmation et particulièrement de la ligne suivante (recherchez la chaine « order_id »):

<input type="hidden" name="order_id" value="3322" />

Vous constaterez que l’order_id envoyé à la banque est le même. Et pour cause, il est généré par le module de paiement « vads.php » grâce à la fonction suivante :

function getIdOrder(){
        $sql="SELECT MAX(orders_id) FROM " . TABLE_ORDERS;
        $rech=tep_db_query($sql);
        if(tep_db_num_rows($rech) == 0)
            return 0;
        return mysql_result($rech,0,0)+1;
    }

Cette dernière recherche le plus grand ID utilisé dans la table ORDER_ID et l’incrémente de 1, partant du principe que les clients achètent sur votre boutique chacun leur tour et que ce numéro de commande tirer sera forcement celui qui sera attribuer ensuite à la facture après sa validation ( nous parlons de la contribution originale récupérée depuis le site Cyberplus .

Si l’on poursuit le test, après le paiement, la validation ajoute la commande à la base de données, le premier avec le bon ID (il n’est pas précisé dans la requête d’insertion et est généré en auto-incrément par MySQL). La commande est bien validée au client avec le bon ID car PHP récupère le mysql_insert_id() et donc l’ID réellement utilisé.

Pour le deuxième client, l’insertion renvoi un ID incrémenté d’un et donc différent de celui de la banque.

La solution est donc de fixé l’ID de commande au moment de la génération du bouton de validation en réservant l’entrée par des jetons sous forme de fichiers. Attention ! Les ID de commandes ne seront alors plus forcement continus, ce
qui peut poser un problème comptable, mais pas plus qu’une commande par chèque pour laquelle vous n’avez jamais de retour.

Modifications

Ajout d’un répertoire /order_ids en racine du site
Ajout d’un fichier htaccess dans ce dossier avec la directive « deny all » pour bloquer l’accès HTTP à ce répertoire.

Ajout d’un répertoire /session_id2order_id en racine du site
Ajout d’un fichier htaccess dans ce dossier avec la directive « deny all » pour bloquer l’accès HTTP à ce répertoire.
Dans le fichier : /includes/modules/payment/vads.php

Remplacement de la méthode :

function getIdOrder(){
 $sql="SELECT MAX(orders_id) FROM " . TABLE_ORDERS;
 $rech=tep_db_query($sql);
 if(tep_db_num_rows($rech) == 0)
 return 0;
 return mysql_result($rech,0,0)+1;
 }

Par :

function getIdOrder(){
 $sql="SELECT MAX(orders_id) FROM " . TABLE_ORDERS;
 $rech=tep_db_query($sql);
 if(tep_db_num_rows($rech) == 0)
 return 0;
 $id_genere = mysql_result($rech,0,0)+1;

// BUGFIX ICODIA
 while(file_exists(DIR_FS_CATALOG . "/order_ids/".$id_genere)){
 $id_genere++;
 }
 file_put_contents(DIR_FS_CATALOG . "/order_ids/".$id_genere, time());
 file_put_contents(DIR_FS_CATALOG . "/session_id2order_id/".session_id(), $id_genere);
 // FIN BUGFIX ICODIA

return $id_genere;
 }

Dans le fichier /checkout_process.php :

Placer avant :

tep_db_perform(TABLE_ORDERS, $sql_data_array);

(ligne 105 environ)

// BUGFIX ICODIA
 if($_POST["order_id"]>0){
 $order_id = $_POST["order_id"];
 $sql_data_array['orders_id'] = $order_id;
 }
 else if($_GET["order_id"]>0){
 $order_id = $_GET["order_id"];
 $sql_data_array['orders_id'] = $order_id;
 }
 else if(file_exists(DIR_FS_CATALOG . "/session_id2order_id/".session_id())){
 $order_id = file_get_contents(DIR_FS_CATALOG . "/session_id2order_id/".session_id());
 $sql_data_array['orders_id'] = $order_id;
 }
 // FIN BUGFIX ICODIA

Explications en français :

Système de jetons pour éviter les doublons :

Après tirage d’un ID de commande par le module de paiements VADS à partir du plus grand ID de commande de la base ORDERS incrémenté une fois (script d’origine), on vérifie dans le dossier « order_ids » si ce numéro a déjà été tiré et on incrémente à nouveau le n° de commande si c’est le cas, jusqu’à obtenir un numéro unique.
Ensuite, on place un jeton dans ce même répertoire pour indiqué que le numéro est réservé, pour qu’un autre client ne le tire pas avant la procédure de finalisation de la commande.
Un fichier contenant le numéro de commande et nommé avec le numéro de session PHP du visiteur est également placé dans le répertoire « session_id2order_id » par sécurité (voir plus bas).

Récupération du numéro de commande utilisé :

Par défaut, OS-Commerce enregistre la commande après le paiement en laissant MySQL attribuer un numéro de commande automatiquement car le champ « orders_id » de la table ORDERS à un attribut « auto-incremente ». Le numéro de commande n’est donc pas préciser à l’enregistrement, mais récupéré après l’enregistrement au moyen de la fonction PHP « mysql_insert_id() ».
La portion de script ajouté dans le fichier « checkout_process.php » tente de récupérer le numéro de commande renvoyé à la page par le module de paiement (T.P.E. par exemple) par la méthode POST (normalement utilisée), sinon par la méthode GET (pour un autre module de paiement, et enfin en utilisant le fichier placé précédemment dans le répertoire « session_id2order_id ». Si aucune de ces méthodes ne donne de résultat, alors le processus normal d’attribution prend le relais (attribution automatique par MySQL).

Si vous utilisez également le système de paiement PayPal IPN, il faut ajouter dans le fichier includes/modules/payment/paypal_ipn.php ligne 181 environ, avant :

tep_db_perform(TABLE_ORDERS, $sql_data_array);

le code suivant :

// BUGFIX ICODIA
 $sql="SELECT MAX(orders_id) FROM " . TABLE_ORDERS;
 $rech=tep_db_query($sql);
 if(tep_db_num_rows($rech) == 0) $id_genere=0;
 else $id_genere = mysql_result($rech,0,0)+1;
 while(file_exists(DIR_FS_CATALOG . "/order_ids/".$id_genere)){ $id_genere++; }
 file_put_contents(DIR_FS_CATALOG . "/order_ids/".$id_genere, time()."PAYPAL");
 file_put_contents(DIR_FS_CATALOG . "/session_id2order_id/".session_id(), $id_genere);
 $sql_data_array['orders_id'] = $id_genere;
 // FIN BUGFIX ICODIA

Si vous proposez aussi le paiement par chèque à vos client, il faut modifier le fichier includes/modules/payment/moneyorder.php (ligne 75) :

remplacer :

function process_button() {
 return false;
 }

par :

function process_button() {
 echo "<input type=\"hidden\" name=\"order_id\" value=\"". $this->getIdOrder()."\" />";
 return false;
 }
function getIdOrder(){
 $sql="SELECT MAX(orders_id) FROM " . TABLE_ORDERS;

$rech=tep_db_query($sql);

if(tep_db_num_rows($rech) == 0)
 return 0;

 $id_genere = mysql_result($rech,0,0)+1;
 // BUGFIX ICODIA
 while(file_exists(DIR_FS_CATALOG . "/order_ids/".$id_genere)){
 $id_genere++;
 }
 file_put_contents(DIR_FS_CATALOG . "/order_ids/".$id_genere, time());
 file_put_contents(DIR_FS_CATALOG . "/session_id2order_id/".session_id(), $id_genere);
 // FIN BUGFIX ICODIA
 return $id_genere;
}