Un paiement en double commence presque toujours de la même manière : l’ECR perd la réponse à un paiement qu’il a déjà envoyé, puis le renvoie ou laisse le client payer autrement. Cet article présente les règles que votre ECR doit appliquer et la logique de récupération à exécuter avec l’API locale (Nexo Retailer 3.1, JSON sur HTTP, port 8080).
Pourquoi les doublons se produisent
Le terminal de paiement et l’acquéreur ne connaissent que les demandes qu’ils ont reçues. Ils ne peuvent pas déterminer si un deuxième paiement pour le même panier est une erreur ou un nouvel achat. Seul l’ECR le sait ; la prévention des doublons relève donc de la responsabilité de l’ECR. Causes typiques :
- Le paiement par carte est approuvé, la réponse est perdue (coupure réseau, délai d’expiration ou plantage de l’ECR) et le caissier appuie de nouveau sur Paiement par carte, ou le client paie en espèces. Personne ne sait que la carte a été débitée, donc personne ne l’annule.
- Le caissier double-clique, ou la bibliothèque HTTP réessaie silencieusement un
POSTaprès une erreur de connexion : la même demande parvient deux fois au terminal. - L’ECR redémarre pendant un paiement et commence la vente suivante sans lire le résultat de la vente interrompue.
Remarque
La règle unique : ne jamais renvoyer, remplacer ou abandonner un paiement tant que l’ECR n’en connaît pas le résultat. Toutes les autres règles de cet article servent cette règle.
Ce que le terminal de paiement fait ou ne fait pas pour vous
Deux composants répondent à vos demandes : le serveur HTTP du terminal, qui contient un échange par point de terminaison (un POST l’ouvre, le 200 final sur GET le ferme), et l’application de paiement, qui exécute la transaction. Ils peuvent être dans des états différents ; la logique de récupération vérifie les deux.
| Situation | Comportement du terminal | Ce que cela signifie pour l’ECR |
|---|---|---|
Deuxième POST /payment alors qu’un échange est ouvert sur ce point de terminaison |
403 Forbidden, quelle que soit la valeur de ServiceID
| Les doublons parallèles sont bloqués. Un 403 n’est pas un refus : le premier paiement est toujours ouvert. Exécutez la logique de récupération. |
POST /payment renvoyé avec un ServiceID déjà utilisé, après la fin du premier paiement |
202 Accepted : traité comme un nouveau paiement, avec un nouveau POITransactionID
| Le terminal ne déduplique pas les demandes. La génération de valeurs ServiceID uniques vous incombe. |
GET /payment après la lecture du 200 final, ou lorsqu’aucune opération n’a été démarrée |
423 Locked : aucun échange ouvert sur ce point de terminaison
| Un résultat peut être lu une seule fois avec GET /payment. Ensuite, utilisez POST /status. |
GET /payment lorsque l’application de paiement n’a pas fourni la réponse finale dans le délai d’expiration du point de terminaison du terminal |
408 Request Timeout, envoyé par le terminal (et non par votre client HTTP). Il s’agit de la réponse finale de cet échange : comme un 200, il doit être lu. Jusque-là, le point de terminaison reste dans l’état /blocked et un nouveau POST reçoit une réponse 403. Le 408 n’arrête pas le paiement | Le paiement continue jusqu’à sa propre fin ; sa réponse ne sera pas renvoyée par GET /payment. Exécutez la logique de récupération : /status répond InProgress jusqu’à la fin, puis renvoie son résultat. |
GET /blocked | Répertorie les points de terminaison dont l’échange est ouvert (réponse finale pas encore lue). Une réponse finale attend jusqu’à sa lecture et n’expire pas. /blocked reflète uniquement le serveur HTTP, pas l’application de paiement, et ne déclenche aucune action sur le terminal | Utilisez-le pour trouver un résultat que vous n’avez pas encore lu. Il ne vous indique pas si le terminal est libre : c’est le rôle de /diagnosis. |
POST /diagnosis | Répond même pendant un paiement et sans session. POIStatus.GlobalStatus vaut OK ou Busy
|
Busy avec une liste /blocked vide signifie que l’application de paiement traite encore une opération (un 408 a été lu pendant l’exécution du paiement, ou l’application est bloquée). Aucune demande de l’ECR ne peut la libérer de force. |
POST /status (TransactionStatusRequest) | Toujours HTTP 200. Renvoie le dernier paiement, la dernière acquisition de carte ou la dernière annulation terminé(e) dans RepeatedMessageResponse, même si son échange HTTP n’a jamais été lu. Un MessageReference présent dans la demande est ignoré | Un seul résultat, qui peut provenir d’une autre vente. Effectuez toujours le contrôle de cohérence. |
POST /status pendant qu’un paiement est toujours en cours dans l’application de paiement |
Result: Failure, ErrorCondition: InProgress, sans RepeatedMessageResponse
| Le paiement n’est pas terminé. Continuez à interroger GET /payment si l’échange est ouvert ; sinon, attendez et posez à nouveau la question. |
POST /payment alors que le paiement précédent est toujours en cours dans l’application de paiement (possible après la lecture d’un 408) |
202, puis immédiatement 200 avec Result: Failure, ErrorCondition: Busy et AdditionalResponse TransactionInProgress
| Il ne s’agit ni d’un refus du nouveau paiement ni du résultat de l’ancien. Attendez la fin du paiement précédent, puis récupérez son résultat. |
Aucune session valide : le terminal ou l’application a redémarré, une mise à jour de l’application a redémarré le serveur, ou SaleID et POIID diffèrent de ceux utilisés lors de la connexion |
Result: Failure, ErrorCondition: LoggedOut sur chaque demande jusqu’à un nouveau /login. Une réponse finale produite avant un redémarrage est conservée et reste lisible avec GET /payment ; un paiement interrompu avant la production d’une réponse ne laisse rien (423), et /status répond alors NotFound ou renvoie une transaction plus ancienne | Envoyez immédiatement /login, puis effectuez la récupération. Après un redémarrage, NotFound signifie inconnu, et non non payé. |
POST /abort pendant un paiement |
202, aucun GET /abort (405). Le paiement se termine en quelques secondes avec Result: Failure, ErrorCondition: Aborted sur GET /payment, sous le ServiceID du paiement. Une annulation envoyée alors qu’aucune opération n’est en cours reçoit également 202 et n’a aucun effet | Continuez à interroger le point de terminaison du paiement après une annulation. |
Remarque
Nexo Retailer exige également que le récepteur refuse une demande dont le ServiceID a déjà été utilisé (message répété). Le terminal de paiement n’effectue pas ce contrôle actuellement : une demande renvoyée est une nouvelle transaction. Un ECR conçu selon les règles ci-dessous fonctionne dans les deux cas.
Règles à appliquer sur l’ECR
1. Écrire un journal des transactions avant l’envoi
Avant chaque POST d’un paiement, d’une acquisition de carte ou d’une annulation, écrivez une entrée dans un stockage persistant : ServiceID, SaleTransactionID (TransactionID et TimeStamp), point de terminaison, montant, devise et état IN_FLIGHT. Lors du 200 final, enregistrez POIData.POITransactionID et le résultat, puis clôturez l’entrée. Après un redémarrage, une entrée toujours à l’état IN_FLIGHT correspond à un paiement dont le résultat est inconnu : résolvez-le avant toute autre opération sur cette voie.
2. Utiliser un nouveau ServiceID pour chaque demande
- Chaque demande reçoit un nouveau
ServiceID, y compris/status,/diagnosis,/abort,/loginet une nouvelle tentative de paiement dont il est confirmé qu’elle n’a pas été traitée. Conservez le compteur ; n’utilisez jamais une constante telle que"0". - Le
SaleTransactionIDidentifie la vente : il est unique par panier, reste inchangé pour les tentatives concernant le même panier et est toujours envoyé avec sonTimeStamp.
3. Verrouiller la vente tant que son résultat est inconnu
Désactivez le bouton Paiement par carte dès qu’il est pressé. Tant qu’une entrée est à l’état IN_FLIGHT pour un panier : aucune nouvelle demande de paiement, aucun autre moyen de paiement (espèces, bon d’achat, deuxième carte), aucune nouvelle vente sur cette voie. Le verrou prend fin uniquement avec un résultat final, une annulation contrôlée ou la procédure non résolue ci-dessous.
4. Ne jamais laisser la couche HTTP réessayer un POST
Désactivez les tentatives automatiques dans votre client HTTP et dans tout proxy situé entre l’ECR et le terminal. Un POST qui échoue au niveau du transport est traité par la logique de récupération, et non renvoyé.
5. Interroger jusqu’à 200, avec des temporisations adaptées à un vrai paiement
POST /payment répond 202. Appelez ensuite GET /payment (sans corps) jusqu’à obtenir une réponse 200.
Réponses de GET /payment
| Signification | Action de l’ECR |
|---|---|---|
200 |
PaymentResponse final
| Enregistrer le résultat, clôturer l’entrée du journal et déverrouiller la voie. |
201 | Message intermédiaire (par exemple une DisplayRequest) | Afficher le texte au caissier, continuer les interrogations et réinitialiser le délai du paiement. |
204 | Paiement en cours, rien à lire pour le moment | Continuer les interrogations. |
206 | Un message est renvoyé et d’autres sont en attente | Le traiter, réinitialiser le délai du paiement et appeler immédiatement GET /payment à nouveau. |
408 | L’application de paiement n’a pas répondu dans le délai d’expiration du point de terminaison du terminal. La lecture du 408 ferme l’échange ; le paiement peut toujours être en cours | Passer à la logique de récupération, étape 2. |
423 | Aucun échange ouvert sur ce point de terminaison | Passer à la logique de récupération. |
| Temporisation | Valeur |
|---|---|
| Fenêtre globale du paiement (PIN, pourboire, choix de la devise, autorisation en ligne) | 120 s, jamais moins de 90 s |
Pause entre deux interrogations GET
| 0,5 à 1 s |
Délai d’expiration d’une demande HTTP unique (interrogation GET, /blocked, /diagnosis, /status) | 10 s |
Attente du 200 final après une annulation | 30 s, puis procédure non résolue |
| Tentatives de récupération avant de déclarer le terminal inaccessible | 3, puis procédure non résolue |
Le terminal applique son propre délai d’expiration par point de terminaison, défini dans la configuration de son serveur HTTP : 180 s sur /payment, identique sur tous les terminaux. Il s’agit du temps pendant lequel le terminal attend la réponse finale de l’application de paiement ; au-delà, GET répond 408 au lieu de renvoyer la réponse. Maintenez votre fenêtre de paiement en dessous de cette valeur. Lorsque la fenêtre de paiement expire, ne libérez pas l’écran et ne renvoyez pas la demande : passez à la logique de récupération.
6. Annuler avec le POITransactionID si deux moyens de paiement ont été utilisés
Si un paiement par carte s’avère approuvé après que le client a déjà payé autrement, envoyez une ReversalRequest citant OriginalPOITransaction.POITransactionID provenant du journal ou de /status. N’annulez jamais avec le SaleTransactionID.
La logique de récupération
Exécutez-la lorsque l’ECR démarre avec une entrée IN_FLIGHT, lorsqu’une interrogation échoue ou que la fenêtre de paiement expire, lorsqu’un POST répond 403 ou échoue au niveau du transport, et lorsque GET /payment répond 408 ou 423 alors qu’une entrée est toujours à l’état IN_FLIGHT. Utilisez un nouveau ServiceID pour chaque appel. Les trois étapes répondent à trois questions différentes : un échange est-il toujours ouvert, l’application de paiement est-elle toujours occupée, quel était le dernier résultat.
Étape 1 : un échange HTTP est-il toujours ouvert ?
GET /blocked répertorie chaque point de terminaison dont l’échange est ouvert, au format JSON brut :
GET http://<terminal-ip>:8080/blocked
200 {"blocked":["/payment"]} un résultat est en attente ou encore en cours de production
200 {"blocked":[]} aucun échange ouvert-
/paymentest répertorié : reprenezGET /paymentjusqu’à obtenir200. Cette réponse constitue le résultat, quelle que soit l’ancienneté de la fin du paiement. N’appelez pas/statusmaintenant : il renverrait la vente précédente. Si leGETde récupération répond408ou423, passez à l’étape 2. -
La liste est vide : la réponse finale a déjà été lue (un
200ou un408), ou le paiement n’a jamais démarré. L’étape 2 vous indique si l’application de paiement traite encore quelque chose.
Étape 2 : l’application de paiement est-elle toujours occupée ?
POST http://<terminal-ip>:8080/diagnosis
{ "MessageHeader": { ..., "MessageCategory":"Diagnosis", "ServiceID":"1042" },
"DiagnosisRequest": {"POIID":"POI_01"} }
200
{ "DiagnosisResponse": { "Response": {"Result":"Success"},
"POIStatus": {"GlobalStatus":"OK"}, ... } }GlobalStatus | Signification | Action de l’ECR |
|---|---|---|
OK | L’application de paiement est inactive | Passer à l’étape 3. |
Busy | Une transaction est toujours en cours dans l’application de paiement, sans échange restant permettant de la lire (/status répond InProgress dans cette situation) | Revérifier toutes les 5 s pendant la fenêtre de paiement. Si l’état reste Busy au-delà de la fenêtre : envoyer une seule requête /abort faisant référence au ServiceID en cours, puis revérifier. Si l’état reste Busy : marquer l’entrée UNRESOLVED et demander au caissier de redémarrer le terminal, puis exécuter à nouveau la logique de récupération. |
Aucune réponse (erreur de transport ou 408) | Le terminal ou l’application de paiement ne répond pas | Compter une tentative ; après 3 tentatives, marquer l’entrée UNRESOLVED. |
Étape 3 : lire la dernière transaction terminée
POST http://<terminal-ip>:8080/status
{ "MessageHeader": { ..., "MessageCategory":"TransactionStatus", "ServiceID":"1043" },
"TransactionStatusRequest": {} }
200
{ "TransactionStatusResponse": {
"Response": {"Result":"Success"},
"RepeatedMessageResponse": {
"MessageHeader": {"MessageCategory":"Payment","ServiceID":"1040", ...},
"PaymentResponse": {
"Response": {"Result":"Success"},
"SaleData": {"SaleTransactionID":{"TransactionID":"SALE-1040","TimeStamp":"..."}},
"POIData": {"POITransactionID":{"TransactionID":"9991","TimeStamp":"..."}},
"PaymentResult": {"AmountsResp":{"AuthorizedAmount":"11.99","Currency":"EUR"}}, ... } } } }Avertissement
Le Response.Result = Success externe signifie seulement que le terminal a trouvé une transaction à renvoyer. Le résultat du paiement est le PaymentResponse.Response interne, et uniquement après le contrôle de cohérence.
La transaction renvoyée correspond à votre entrée du journal uniquement si RepeatedMessageResponse.MessageHeader.ServiceID, SaleTransactionID.TransactionID et SaleTransactionID.TimeStamp sont identiques, si MessageCategory vaut Payment et si le montant est celui demandé (ou inférieur en cas d’approbation partielle).
Ce que renvoie /status
| Conclusion | Action de l’ECR |
|---|---|---|
Correspondance, Result: Success interne
| Paiement approuvé | Enregistrer le POITransactionID, imprimer le PaymentReceipt et clôturer la vente comme payée. |
Correspondance, Result: Failure interne (Aborted, Refusal, Cancel...) | Paiement non effectué | Clôturer la tentative comme non payée. Une nouvelle tentative utilise un nouveau ServiceID. |
Aucune correspondance ou NotFound, aucun LoggedOut observé depuis l’envoi de la demande | Le terminal n’a jamais démarré votre paiement | Clôturer la tentative comme non payée. Une nouvelle tentative est sûre avec un nouveau ServiceID. |
Failure avec InProgress
| Le paiement est toujours en cours | Attendre 5 s et demander à nouveau, pendant la fenêtre de paiement ; au-delà, suivre la ligne Busy de l’étape 2. |
Failure avec LoggedOut
| Aucune session (redémarrage, mise à jour de l’application ou SaleID/POIID différent de celui de la connexion) | Envoyer immédiatement /login, puis répéter une fois les étapes 1 à 3. |
| Toujours aucune correspondance après le redémarrage d’un terminal | Inconnu : une autorisation peut avoir quitté le terminal avant le redémarrage | Marquer l’entrée UNRESOLVED. Ne pas la considérer comme non payée. |
| Aucune réponse après 3 tentatives | Terminal inaccessible | Marquer l’entrée UNRESOLVED. |
Procédure pour les cas non résolus
- Conservez l’entrée dans le stockage persistant avec l’état
UNRESOLVED, ainsi que la voie, le montant et l’heure, et alertez le responsable du magasin ou le back-office. Un autre moyen de paiement n’est autorisé que par cette procédure explicite, jamais silencieusement. - Lorsque le terminal est de nouveau accessible et inactif, exécutez automatiquement les étapes 1 à 3 pour chaque entrée
UNRESOLVED. Si le paiement par carte s’avère approuvé et qu’un autre moyen de paiement a été utilisé, annulez-le (règle 6). - Si le résultat ne peut plus être lu sur le terminal, effectuez le rapprochement avec le rapport des transactions du commerçant avant de rembourser le client.
Annuler lorsque le client abandonne
Si le paiement est toujours en cours au-delà de la fenêtre de paiement (par exemple, le client est parti pendant la demande de PIN), le caissier peut l’annuler : envoyez une AbortRequest dont le MessageReference contient le ServiceID du paiement, puis continuez à interroger GET /payment. Le 200 final avec ErrorCondition: Aborted constitue le résultat. Après une acquisition de carte (paiement en deux étapes), utilisez la variante Admin (AdminRequest avec DualTransactionAborted) décrite dans la spécification de l’API locale.
Avertissement
Une annulation est une demande, pas une garantie : si l’autorisation est déjà en cours, le paiement peut tout de même être approuvé et, sur certains écrans de terminal, l’annulation n’est pas traitée. Lisez toujours le 200 final ; si aucun n’arrive dans les 30 s, marquez l’entrée UNRESOLVED. Si l’échange du paiement s’est déjà terminé par un 408, envoyez tout de même l’annulation et lisez le résultat avec /status.
Ce que voient le caissier ou le client
| État de l’ECR | Voie avec assistance | Caisse libre-service |
|---|---|---|
| Récupération en cours (généralement quelques secondes) | « Vérification du paiement, veuillez patienter ». Le bouton Paiement par carte reste désactivé. | « Confirmation de votre paiement » avec un indicateur de progression. Le client ne peut pas revenir en arrière. |
| Paiement toujours en cours au-delà de la fenêtre | Deux choix seulement : Continuer à patienter ou Annuler le paiement (envoie l’annulation). | Annulation automatique, puis « Paiement annulé, veuillez réessayer ». |
UNRESOLVED | « Le paiement n’a pas pu être confirmé ». Un autre moyen de paiement est autorisé uniquement après confirmation du responsable ; tout paiement en double confirmé sera annulé. | Appelez l’employé. Ne proposez pas automatiquement un autre moyen de paiement. |