Prevenire i pagamenti duplicati dall’ECR

Un pagamento duplicato inizia quasi sempre nello stesso modo: l’ECR perde la risposta a un pagamento già inviato, quindi lo invia nuovamente oppure consente al cliente di pagare in un altro modo. Questo articolo descrive le regole che l’ECR deve implementare e la logica di recupero da eseguire con la Local API (Nexo Retailer 3.1, JSON su HTTP, porta 8080).

Perché si verificano i duplicati

Il terminale di pagamento e l’acquirer conoscono solo le richieste che hanno ricevuto. Non possono sapere se un secondo pagamento per lo stesso carrello è un errore o un nuovo acquisto. Solo l’ECR lo sa, quindi la prevenzione dei duplicati è responsabilità dell’ECR. Cause tipiche:

  • Il pagamento con carta viene approvato, la risposta viene persa (interruzione della rete, timeout o arresto anomalo dell’ECR) e l’operatore preme nuovamente Pagamento con carta, oppure il cliente paga in contanti. Nessuno sa che la carta è stata addebitata, quindi nessuno annulla il pagamento.
  • L’operatore fa doppio clic oppure la libreria HTTP riprova automaticamente un POST dopo un errore di connessione: la stessa richiesta raggiunge due volte il terminale.
  • L’ECR si riavvia durante un pagamento e avvia la vendita successiva senza leggere l’esito di quella interrotta.

Nota

L’unica regola: non inviare nuovamente, sostituire o abbandonare mai un pagamento finché l’ECR non ne conosce l’esito. Tutte le altre regole di questo articolo servono a rispettare questa regola.

Cosa fa e cosa non fa per voi il terminale di pagamento

Due componenti rispondono alle vostre richieste: il server HTTP del terminale, che mantiene uno scambio per endpoint (un POST lo apre, il 200 finale su GET lo chiude), e l’applicazione di pagamento, che esegue la transazione. Possono trovarsi in stati diversi e la logica di recupero controlla entrambi.

SituazioneComportamento del terminaleCosa significa per l’ECR
Secondo POST /payment mentre è aperto uno scambio su quell’endpoint 403 Forbidden, indipendentemente dal ServiceID I duplicati paralleli vengono bloccati. Un 403 non è un rifiuto: il primo pagamento è ancora aperto. Eseguite la logica di recupero.
Nuovo invio di POST /payment con un ServiceID già utilizzato, dopo il completamento del primo pagamento 202 Accepted: elaborato come un nuovo pagamento, con un nuovo POITransactionID Il terminale non elimina i duplicati. I valori univoci di ServiceID sono responsabilità vostra.
GET /payment dopo la lettura del 200 finale, oppure quando non è stato avviato nulla 423 Locked: nessuno scambio aperto su questo endpoint Un risultato può essere letto una sola volta con GET /payment. Dopodiché usate POST /status.
GET /payment quando l’applicazione di pagamento non ha fornito la risposta finale entro il timeout dell’endpoint del terminale 408 Request Timeout, inviato dal terminale (non dal vostro client HTTP). È la risposta finale di quello scambio: come un 200, deve essere letto. Fino a quel momento l’endpoint rimane in /blocked e un nuovo POST su di esso restituisce 403. Il 408 non interrompe il pagamentoIl pagamento continua a essere eseguito fino alla propria conclusione; la relativa risposta non arriverà tramite GET /payment. Eseguite la logica di recupero: /status restituisce InProgress fino alla conclusione, quindi restituisce il risultato.
GET /blockedElenca gli endpoint con uno scambio aperto (risposta finale non ancora letta). Una risposta finale rimane in attesa finché non viene letta e non scade. /blocked riflette solo il server HTTP, non l’applicazione di pagamento, e non attiva alcuna operazione sul terminaleUsatelo per trovare un risultato non ancora letto. Non indica se il terminale è libero: per questo serve /diagnosis.
POST /diagnosisRisponde anche durante un pagamento e senza sessione. POIStatus.GlobalStatus è OK oppure Busy Busy con un elenco /blocked vuoto significa che l’applicazione di pagamento sta ancora elaborando (è stato letto un 408 mentre il pagamento era in corso oppure l’applicazione è bloccata). Nessuna richiesta dell’ECR può renderla libera.
POST /status (TransactionStatusRequest)Sempre HTTP 200. Restituisce l’ultimo pagamento, acquisizione della carta o annullamento completato in RepeatedMessageResponse, anche se il relativo scambio HTTP non è mai stato letto. Un MessageReference nella richiesta viene ignoratoUn solo risultato, che potrebbe appartenere a un’altra vendita. Eseguite sempre il controllo di coerenza.
POST /status mentre un pagamento è ancora in corso nell’applicazione di pagamento Result: Failure, ErrorCondition: InProgress, nessun RepeatedMessageResponse Il pagamento non è terminato. Continuate a eseguire il polling di GET /payment se lo scambio è aperto; altrimenti attendete e chiedete nuovamente.
POST /payment mentre il pagamento precedente è ancora in corso nell’applicazione di pagamento (possibile dopo la lettura di un 408) 202, quindi un 200 immediato con Result: Failure, ErrorCondition: Busy, AdditionalResponse TransactionInProgress Non è un rifiuto del nuovo pagamento né il risultato di quello precedente. Attendete la conclusione del pagamento precedente, quindi recuperatene il risultato.
Nessuna sessione valida: il terminale o l’applicazione si sono riavviati, un aggiornamento dell’applicazione ha riavviato il server oppure SaleID e POIID sono diversi da quelli utilizzati al Login Result: Failure, ErrorCondition: LoggedOut su ogni richiesta fino a un nuovo /login. Una risposta finale prodotta prima di un riavvio viene conservata ed è ancora leggibile con GET /payment; un pagamento interrotto prima di produrne una non lascia nulla (423) e /status restituisce quindi NotFound oppure una transazione precedenteInviate immediatamente /login, quindi eseguite il recupero. Dopo un riavvio, NotFound significa sconosciuto, non non pagato.
POST /abort durante un pagamento 202, nessun GET /abort (405). Il pagamento termina entro pochi secondi con Result: Failure, ErrorCondition: Aborted su GET /payment, usando il ServiceID del pagamento. Un annullamento inviato quando non è in corso nulla riceve anch’esso 202 e non ha alcun effettoDopo un annullamento, continuate a eseguire il polling dell’endpoint del pagamento.

Nota

Nexo Retailer richiede inoltre al ricevitore di rifiutare una richiesta il cui ServiceID sia già stato utilizzato (messaggio ripetuto). Il terminale di pagamento oggi non applica questo controllo: una richiesta reinviata è una nuova transazione. Un ECR realizzato secondo le regole riportate di seguito funziona in entrambi i casi.

Regole da implementare sull’ECR

1. Scrivere un registro della transazione prima dell’invio

Prima di ogni POST di un pagamento, di un’acquisizione della carta o di un annullamento, scrivete una voce in uno storage persistente: ServiceID, SaleTransactionID (TransactionID e TimeStamp), endpoint, importo, valuta e stato IN_FLIGHT. Sul 200 finale, memorizzate POIData.POITransactionID e il risultato, quindi chiudete la voce. Dopo un riavvio, una voce ancora nello stato IN_FLIGHT rappresenta un pagamento dall’esito sconosciuto: risolvetelo prima di qualsiasi altra operazione su quella postazione.

2. Usare un nuovo ServiceID per ogni richiesta

  • Ogni richiesta riceve un nuovo ServiceID, inclusi /status, /diagnosis, /abort, /login e un nuovo tentativo di pagamento confermato come non elaborato. Salvate il contatore; non usate mai una costante come "0".
  • Il SaleTransactionID identifica la vendita: è univoco per ogni carrello, non cambia tra i tentativi relativi allo stesso carrello ed è sempre inviato insieme al relativo TimeStamp.

3. Bloccare la vendita quando l’esito è sconosciuto

Disabilitate il pulsante Pagamento con carta non appena viene premuto. Quando una voce è nello stato IN_FLIGHT per un carrello: nessuna nuova richiesta di pagamento, nessun altro metodo di pagamento (contanti, buono, seconda carta), nessuna nuova vendita su quella postazione. Il blocco termina sempre con un risultato finale, un annullamento controllato o la procedura per i casi irrisolti descritta di seguito.

4. Non consentire al livello HTTP di riprovare un POST

Disattivate i tentativi automatici nel client HTTP e in qualsiasi proxy tra l’ECR e il terminale. Un POST che fallisce a livello di trasporto deve passare alla logica di recupero, non a un nuovo tentativo.

5. Eseguire il polling fino a 200, con timer adeguati a un pagamento reale

POST /payment restituisce 202. Quindi chiamate GET /payment (senza corpo) finché non restituisce 200.

Le risposte di GET /payment SignificatoAzione dell’ECR
200 PaymentResponse finale Memorizzate il risultato, chiudete la voce del registro e sbloccate la postazione.
201Messaggio intermedio (ad esempio un DisplayRequest)Mostrate il testo all’operatore, continuate il polling e riavviate il timer del pagamento.
204Pagamento in corso, nulla da leggere per il momentoContinuate il polling.
206Viene restituito un messaggio e ce ne sono altri in attesaElaboratelo, riavviate il timer del pagamento e chiamate nuovamente GET /payment immediatamente.
408L’applicazione di pagamento non ha risposto entro il timeout dell’endpoint del terminale. La lettura del 408 chiude lo scambio; il pagamento potrebbe essere ancora in corsoPassate alla logica di recupero, punto 2.
423Nessuno scambio aperto su questo endpointPassate alla logica di recupero.
TimerValore
Finestra complessiva del pagamento (PIN, mancia, scelta della valuta, autorizzazione online)120 s, mai inferiore a 90 s
Pausa tra due polling con GET Da 0,5 a 1 s
Timeout di una singola richiesta HTTP (polling GET, /blocked, /diagnosis, /status)10 s
Attesa del 200 finale dopo un annullamento30 s, quindi procedura per i casi irrisolti
Tentativi di recupero prima di dichiarare il terminale non raggiungibile3, quindi procedura per i casi irrisolti

Il terminale applica un proprio timeout per endpoint, impostato nella configurazione del server HTTP: 180 s su /payment, uguale su ogni terminale. È il tempo durante il quale il terminale attende la risposta finale dell’applicazione di pagamento; superato tale tempo, GET restituisce 408 invece della risposta. Mantenete la finestra del pagamento al di sotto di questo valore. Quando la finestra del pagamento scade, non rilasciate la schermata e non inviate nuovamente la richiesta: passate alla logica di recupero.

6. Eseguire l’annullamento con il POITransactionID se sono stati utilizzati due metodi di pagamento

Se un pagamento con carta risulta approvato dopo che il cliente ha già pagato in un altro modo, inviate una ReversalRequest indicando OriginalPOITransaction.POITransactionID dal registro o da /status. Non eseguite mai l’annullamento utilizzando il SaleTransactionID.

La logica di recupero

Eseguitela quando l’ECR si avvia con una voce IN_FLIGHT, quando un polling non riesce o scade la finestra del pagamento, quando un POST restituisce 403 o fallisce a livello di trasporto e quando GET /payment restituisce 408 o 423 con una voce ancora nello stato IN_FLIGHT. Usate un nuovo ServiceID per ogni chiamata. I tre passaggi rispondono a tre domande diverse: è ancora aperto uno scambio, l’applicazione di pagamento è ancora occupata, qual è stato l’ultimo risultato.

Passaggio 1: è ancora aperto uno scambio HTTP?

GET /blocked elenca ogni endpoint con uno scambio aperto, come JSON semplice:

GET http://<terminal-ip>:8080/blocked

200 {"blocked":["/payment"]}     a result is waiting, or still being produced
200 {"blocked":[]}               no open exchange
  • /payment è elencato: riprendete GET /payment fino a 200. Quella risposta è l’esito, indipendentemente da quanto tempo sia trascorso dalla fine del pagamento. Non chiamate ora /status: restituirebbe la vendita precedente. Se il GET di svuotamento restituisce 408 o 423, passate al punto 2.
  • L’elenco è vuoto: la risposta finale è già stata letta (un 200 o un 408) oppure il pagamento non è mai iniziato. Il punto 2 indica se l’applicazione di pagamento sta ancora elaborando qualcosa.

Passaggio 2: l’applicazione di pagamento è ancora occupata?

POST http://<terminal-ip>:8080/diagnosis
{ "MessageHeader": { ..., "MessageCategory":"Diagnosis", "ServiceID":"1042" },
  "DiagnosisRequest": {"POIID":"POI_01"} }

200
{ "DiagnosisResponse": { "Response": {"Result":"Success"},
                         "POIStatus": {"GlobalStatus":"OK"}, ... } }
GlobalStatusSignificatoAzione dell’ECR
OKL’applicazione di pagamento è inattivaPassate al punto 3.
BusyUna transazione è ancora in corso nell’applicazione di pagamento, senza alcuno scambio residuo da cui leggerla (/status restituisce InProgress nella stessa situazione)Ricontrollate ogni 5 s entro la finestra del pagamento. Se è ancora Busy oltre la finestra: inviate un solo /abort facendo riferimento al ServiceID in corso e ricontrollate. Se è ancora Busy: contrassegnate la voce come UNRESOLVED e chiedete all’operatore di riavviare il terminale, quindi eseguite nuovamente la logica di recupero.
Nessuna risposta (errore di trasporto o 408)Il terminale o l’applicazione di pagamento non rispondonoContate un tentativo; dopo 3 tentativi, contrassegnate la voce come UNRESOLVED.

Passaggio 3: leggere l’ultima transazione completata

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"}}, ... } } } }

Avviso

L’elemento esterno Response.Result = Success indica solo che il terminale ha trovato una transazione da restituire. L’esito del pagamento è quello di PaymentResponse.Response, l’elemento interno, e solo dopo il controllo di coerenza.

La transazione restituita corrisponde alla voce del registro solo se RepeatedMessageResponse.MessageHeader.ServiceID, SaleTransactionID.TransactionID e SaleTransactionID.TimeStamp sono uguali, MessageCategory è Payment e l’importo è quello richiesto (o inferiore, in caso di approvazione parziale).

Cosa restituisce /status ConclusioneAzione dell’ECR
Corrispondenza, Result: Success interno Pagamento approvatoMemorizzate POITransactionID, stampate la PaymentReceipt e chiudete la vendita come pagata.
Corrispondenza, Result: Failure interno (Aborted, Refusal, Cancel...)Pagamento non eseguitoChiudete il tentativo come non pagato. Un nuovo tentativo usa un nuovo ServiceID.
Nessuna corrispondenza o NotFound, senza LoggedOut dal momento dell’invio della richiestaIl terminale non ha mai avviato il vostro pagamentoChiudete il tentativo come non pagato. Un nuovo tentativo è sicuro con un nuovo ServiceID.
Failure con InProgress Il pagamento è ancora in corsoAttendete 5 s e chiedete nuovamente, entro la finestra del pagamento; oltre tale finestra, seguite la riga Busy del punto 2.
Failure con LoggedOut Nessuna sessione (riavvio, aggiornamento dell’applicazione oppure SaleID/POIID diversi da quelli del Login)Inviate immediatamente /login, quindi ripetete una volta i passaggi da 1 a 3.
Ancora nessuna corrispondenza dopo il riavvio del terminaleSconosciuto: un’autorizzazione potrebbe essere rimasta nel terminale prima del riavvioContrassegnate la voce come UNRESOLVED. Non trattatela come non pagata.
Nessuna risposta dopo 3 tentativiTerminale non raggiungibileContrassegnate la voce come UNRESOLVED.

Procedura per i casi irrisolti

  • Mantenete la voce nello storage persistente con stato UNRESOLVED, oltre a postazione, importo e ora, e avvisate il responsabile del negozio o il back office. Un altro metodo di pagamento è consentito solo tramite questo percorso esplicito, mai in modo silenzioso.
  • Quando il terminale è nuovamente raggiungibile e inattivo, eseguite automaticamente i passaggi da 1 a 3 per ogni voce UNRESOLVED. Se il pagamento con carta risulta approvato e nel frattempo è stato utilizzato un altro metodo di pagamento, annullatelo (regola 6).
  • Se il risultato non può più essere letto dal terminale, eseguite la riconciliazione con il report delle transazioni dell’esercente prima di rimborsare il cliente.

Annullare quando il cliente rinuncia

Se il pagamento è ancora in corso oltre la finestra del pagamento (ad esempio, il cliente si è allontanato durante la richiesta del PIN), l’operatore può annullarlo: inviate un AbortRequest il cui MessageReference contiene il ServiceID del pagamento, quindi continuate a eseguire il polling di GET /payment. Il 200 finale con ErrorCondition: Aborted costituisce l’esito. Dopo un’acquisizione della carta (pagamento in due passaggi), usate la variante Admin (AdminRequest con DualTransactionAborted) descritta nelle specifiche della Local API.

Avviso

Un annullamento è una richiesta, non una garanzia: se l’autorizzazione è già in corso, il pagamento può comunque terminare con un’approvazione e, su alcune schermate del terminale, l’annullamento potrebbe non essere elaborato. Leggete sempre il 200 finale; se non arriva entro 30 s, contrassegnate la voce come UNRESOLVED. Se lo scambio del pagamento è già terminato con un 408, inviate comunque l’annullamento e leggete l’esito tramite /status.

Cosa vede l’operatore o il cliente

Stato dell’ECRPostazione assistitaCassa automatica
Recupero in corso (generalmente pochi secondi)"Controllo del pagamento, attendere". Il pulsante Pagamento con carta rimane disabilitato."Conferma del pagamento in corso" con un indicatore di avanzamento. Il cliente non può tornare indietro.
Pagamento ancora in corso oltre la finestraSolo due opzioni: Continua ad attendere oppure Annulla pagamento (invia l’annullamento).Annullamento automatico, quindi "Pagamento annullato, riprovare".
UNRESOLVED"Impossibile confermare il pagamento". Un altro metodo di pagamento è consentito solo dopo la conferma del responsabile; qualsiasi pagamento duplicato confermato verrà annullato.Chiamare l’operatore. Non offrire automaticamente un altro metodo di pagamento.

 

Questo articolo ti è stato utile?

Utenti che ritengono sia utile: 0 su 0