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
POSTdopo 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.
| Situazione | Comportamento del terminale | Cosa 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 pagamento | Il 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 /blocked | Elenca 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 terminale | Usatelo per trovare un risultato non ancora letto. Non indica se il terminale è libero: per questo serve /diagnosis. |
POST /diagnosis | Risponde 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 ignorato | Un 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 precedente | Inviate 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 effetto | Dopo 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,/logine un nuovo tentativo di pagamento confermato come non elaborato. Salvate il contatore; non usate mai una costante come"0". - Il
SaleTransactionIDidentifica la vendita: è univoco per ogni carrello, non cambia tra i tentativi relativi allo stesso carrello ed è sempre inviato insieme al relativoTimeStamp.
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
| Significato | Azione dell’ECR |
|---|---|---|
200 |
PaymentResponse finale
| Memorizzate il risultato, chiudete la voce del registro e sbloccate la postazione. |
201 | Messaggio intermedio (ad esempio un DisplayRequest) | Mostrate il testo all’operatore, continuate il polling e riavviate il timer del pagamento. |
204 | Pagamento in corso, nulla da leggere per il momento | Continuate il polling. |
206 | Viene restituito un messaggio e ce ne sono altri in attesa | Elaboratelo, riavviate il timer del pagamento e chiamate nuovamente GET /payment immediatamente. |
408 | L’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 corso | Passate alla logica di recupero, punto 2. |
423 | Nessuno scambio aperto su questo endpoint | Passate alla logica di recupero. |
| Timer | Valore |
|---|---|
| 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 annullamento | 30 s, quindi procedura per i casi irrisolti |
| Tentativi di recupero prima di dichiarare il terminale non raggiungibile | 3, 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: riprendeteGET /paymentfino a200. Quella risposta è l’esito, indipendentemente da quanto tempo sia trascorso dalla fine del pagamento. Non chiamate ora/status: restituirebbe la vendita precedente. Se ilGETdi svuotamento restituisce408o423, passate al punto 2. -
L’elenco è vuoto: la risposta finale è già stata letta (un
200o un408) 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"}, ... } }GlobalStatus | Significato | Azione dell’ECR |
|---|---|---|
OK | L’applicazione di pagamento è inattiva | Passate al punto 3. |
Busy | Una 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 rispondono | Contate 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
| Conclusione | Azione dell’ECR |
|---|---|---|
Corrispondenza, Result: Success interno
| Pagamento approvato | Memorizzate POITransactionID, stampate la PaymentReceipt e chiudete la vendita come pagata. |
Corrispondenza, Result: Failure interno (Aborted, Refusal, Cancel...) | Pagamento non eseguito | Chiudete il tentativo come non pagato. Un nuovo tentativo usa un nuovo ServiceID. |
Nessuna corrispondenza o NotFound, senza LoggedOut dal momento dell’invio della richiesta | Il terminale non ha mai avviato il vostro pagamento | Chiudete il tentativo come non pagato. Un nuovo tentativo è sicuro con un nuovo ServiceID. |
Failure con InProgress
| Il pagamento è ancora in corso | Attendete 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 terminale | Sconosciuto: un’autorizzazione potrebbe essere rimasta nel terminale prima del riavvio | Contrassegnate la voce come UNRESOLVED. Non trattatela come non pagata. |
| Nessuna risposta dopo 3 tentativi | Terminale non raggiungibile | Contrassegnate 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’ECR | Postazione assistita | Cassa 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 finestra | Solo 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. |