Een dubbele betaling begint bijna altijd op dezelfde manier: de ECR verliest het antwoord op een betaling die hij al heeft verzonden en verzendt die vervolgens opnieuw of laat de klant op een andere manier betalen. Dit artikel bevat de regels die uw ECR moet implementeren en de herstelprocedure die moet worden uitgevoerd met de Local API (Nexo Retailer 3.1, JSON over HTTP, poort 8080).
Waarom dubbele betalingen gebeuren
De betaalterminal en de acquirer weten alleen welke aanvragen ze hebben ontvangen. Ze kunnen niet bepalen of een tweede betaling voor hetzelfde winkelmandje een vergissing of een nieuwe aankoop is. Alleen de ECR weet dat, dus het voorkomen van dubbele betalingen is de verantwoordelijkheid van de ECR. Typische oorzaken:
- De kaartbetaling wordt goedgekeurd, het antwoord gaat verloren (netwerkonderbreking, time-out of crash van de ECR) en de kassier drukt opnieuw op Kaartbetaling, of de klant betaalt contant. Niemand weet dat de kaart werd belast, dus niemand draait de betaling terug.
- De kassier klikt tweemaal, of de HTTP-bibliotheek probeert na een verbindingsfout stilzwijgend opnieuw een
POSTuit te voeren: dezelfde aanvraag bereikt de terminal tweemaal. - De ECR wordt tijdens een betaling opnieuw opgestart en begint de volgende verkoop zonder de uitkomst van de onderbroken betaling te lezen.
Opmerking
De ene regel: verzend een betaling nooit opnieuw, vervang ze nooit en laat ze nooit vallen voordat de ECR de uitkomst kent. Elke andere regel in dit artikel dient die ene regel.
Wat de betaalterminal wel en niet voor u doet
Twee componenten beantwoorden uw aanvragen: de HTTP-server van de terminal, die één uitwisseling per endpoint beheert (een POST opent deze, de laatste 200 op GET sluit deze), en de betaaltoepassing, die de transactie uitvoert. Ze kunnen zich in verschillende toestanden bevinden en de herstelprocedure controleert beide.
| Situatie | Gedrag van de terminal | Wat dit betekent voor de ECR |
|---|---|---|
Tweede POST /payment terwijl een uitwisseling op dat endpoint open is |
403 Forbidden, ongeacht de ServiceID
| Parallelle dubbele aanvragen worden geblokkeerd. Een 403 is geen weigering: de eerste betaling staat nog open. Voer de herstelprocedure uit. |
POST /payment opnieuw verzonden met een al gebruikte ServiceID, nadat de eerste betaling is voltooid |
202 Accepted: verwerkt als een nieuwe betaling, met een nieuwe POITransactionID
| De terminal dedupliceert niet. Unieke ServiceID-waarden zijn uw verantwoordelijkheid. |
GET /payment nadat de laatste 200 is gelezen, of wanneer niets is gestart |
423 Locked: geen open uitwisseling op dit endpoint | Een resultaat kan slechts één keer worden gelezen met GET /payment. Gebruik daarna POST /status. |
GET /payment wanneer de betaaltoepassing het laatste antwoord niet binnen de time-out van het terminalendpoint heeft geleverd |
408 Request Timeout, verzonden door de terminal (niet door uw HTTP-client). Dit is het laatste antwoord van die uitwisseling: net als een 200 moet het worden gelezen. Tot dan blijft het endpoint in /blocked en geeft een nieuwe POST daarop 403. De 408 stopt de betaling niet | De betaling blijft lopen tot haar eigen einde; het antwoord komt niet via GET /payment. Voer de herstelprocedure uit: /status antwoordt met InProgress totdat de betaling is beëindigd en retourneert daarna het resultaat. |
GET /blocked | Geeft de endpoints weer met een open uitwisseling (het laatste antwoord is nog niet gelezen). Een laatste antwoord blijft wachten totdat het wordt gelezen en vervalt niet. /blocked geeft alleen de HTTP-server weer, niet de betaaltoepassing, en activeert niets op de terminal | Gebruik dit om een resultaat te vinden dat u nog niet hebt gelezen. Het geeft niet aan of de terminal vrij is: daarvoor dient /diagnosis. |
POST /diagnosis | Antwoordt zelfs tijdens een betaling en zonder sessie. POIStatus.GlobalStatus is OK of Busy
|
Busy met een lege lijst van /blocked betekent dat de betaaltoepassing nog bezig is (een 408 werd gelezen terwijl de betaling liep, of de toepassing zit vast). Geen enkele ECR-aanvraag kan de terminal vrijmaken. |
POST /status (TransactionStatusRequest) | Altijd HTTP 200. Retourneert de laatst voltooide betaling, kaartverwerving of terugdraaiing in RepeatedMessageResponse, zelfs als de bijbehorende HTTP-uitwisseling nooit werd gelezen. Een MessageReference in de aanvraag wordt genegeerd | Slechts één resultaat, mogelijk van een andere verkoop. Voer altijd de consistentiecontrole uit. |
POST /status terwijl een betaling nog loopt in de betaaltoepassing |
Result: Failure, ErrorCondition: InProgress, geen RepeatedMessageResponse
| De betaling is niet voltooid. Blijf GET /payment pollen als de uitwisseling open is; wacht anders en vraag opnieuw. |
POST /payment terwijl de vorige betaling nog loopt in de betaaltoepassing (mogelijk nadat een 408 is gelezen) |
202, gevolgd door onmiddellijk 200 met Result: Failure, ErrorCondition: Busy, AdditionalResponse TransactionInProgress
| Dit is geen weigering van de nieuwe betaling en ook niet het resultaat van de oude. Wacht tot de vorige betaling is beëindigd en herstel daarna het resultaat. |
Geen geldige sessie: de terminal of toepassing is opnieuw opgestart, een update van de toepassing heeft de server opnieuw gestart, of SaleID en POIID verschillen van de waarden bij Login |
Result: Failure, ErrorCondition: LoggedOut bij elke aanvraag totdat een nieuwe /login is uitgevoerd. Een laatste antwoord dat vóór een herstart is geproduceerd, wordt bewaard en blijft leesbaar via GET /payment; een betaling die werd onderbroken voordat ze een antwoord produceerde, laat niets achter (423), en /status antwoordt vervolgens met NotFound of retourneert een oudere transactie | Verstuur onmiddellijk /login en voer daarna herstel uit. Na een herstart betekent NotFound onbekend, niet niet betaald. |
POST /abort tijdens een betaling |
202, geen GET /abort (405). De betaling eindigt binnen enkele seconden met Result: Failure, ErrorCondition: Aborted op GET /payment, onder de ServiceID van de betaling. Een annulering wanneer niets actief is, krijgt ook 202 en heeft geen effect | Blijf het betalingendpoint pollen na een annulering. |
Opmerking
Nexo Retailer vereist ook dat de ontvanger een aanvraag weigert waarvan de ServiceID al werd gebruikt (herhaald bericht). De betaalterminal voert deze controle momenteel niet uit: een opnieuw verzonden aanvraag is een nieuwe transactie. Een ECR die volgens de onderstaande regels is gebouwd, werkt in beide gevallen.
Regels die op de ECR moeten worden geïmplementeerd
1. Schrijf een transactiedagboek voordat u verzendt
Schrijf vóór elke POST van een betaling, kaartverwerving of terugdraaiing één item naar persistente opslag: ServiceID, SaleTransactionID (TransactionID en TimeStamp), endpoint, bedrag, valuta en status IN_FLIGHT. Sla bij de laatste 200 de POIData.POITransactionID en het resultaat op en sluit het item. Na een herstart is een item dat nog de status IN_FLIGHT heeft een betaling met onbekende uitkomst: los dit op voordat u iets anders op die kassa doet.
2. Gebruik voor elke aanvraag een nieuwe ServiceID
- Elke aanvraag krijgt een nieuwe
ServiceID, inclusief/status,/diagnosis,/abort,/loginen een nieuwe poging voor een betaling waarvan is bevestigd dat ze niet verwerkt is. Bewaar de teller; gebruik nooit een vaste waarde zoals"0". - De
SaleTransactionIDidentificeert de verkoop: uniek per winkelmandje, ongewijzigd bij pogingen voor hetzelfde winkelmandje en altijd samen met de bijbehorendeTimeStampverzonden.
3. Vergrendel de verkoop zolang de uitkomst onbekend is
Schakel de knop Kaartbetaling uit zodra erop wordt gedrukt. Zolang een item voor een winkelmandje de status IN_FLIGHT heeft: geen nieuwe betalingsaanvraag, geen andere betaalmethode (contant geld, voucher, tweede kaart) en geen nieuwe verkoop op die kassa. De vergrendeling eindigt altijd met een definitief resultaat, een gecontroleerde annulering of de onderstaande procedure voor onopgeloste gevallen.
4. Laat de HTTP-laag een POST nooit opnieuw proberen
Schakel automatische nieuwe pogingen uit in uw HTTP-client en in elke proxy tussen de ECR en de terminal. Een POST die op transportniveau mislukt, gaat naar de herstelprocedure en wordt niet opnieuw geprobeerd.
5. Blijf pollen tot 200, met timers die passen bij een echte betaling
POST /payment antwoordt met 202. Roep daarna GET /payment (zonder body) aan totdat dit met 200 antwoordt.
Antwoorden van GET /payment
| Betekenis | Actie van de ECR |
|---|---|---|
200 | Laatste PaymentResponse
| Sla het resultaat op, sluit het dagboekitem en ontgrendel de kassa. |
201 | Tussentijds bericht (bijvoorbeeld een DisplayRequest) | Toon de tekst aan de kassier, blijf pollen en start de betalingstimer opnieuw. |
204 | Betaling loopt, er is nog niets te lezen | Blijf pollen. |
206 | Er wordt een bericht geretourneerd en er wachten nog meer berichten | Verwerk het, start de betalingstimer opnieuw en roep onmiddellijk opnieuw GET /payment aan. |
408 | De betaaltoepassing antwoordde niet binnen de time-out van het terminalendpoint. Door het lezen van de 408 wordt de uitwisseling gesloten; de betaling kan nog lopen | Ga naar de herstelprocedure, stap 2. |
423 | Geen open uitwisseling op dit endpoint | Ga naar de herstelprocedure. |
| Timer | Waarde |
|---|---|
| Algemene betalingsperiode (pincode, fooi, valutakeuze, online autorisatie) | 120 s, nooit minder dan 90 s |
Pauze tussen twee GET-polls | 0,5 tot 1 s |
Time-out voor één HTTP-aanvraag (GET-poll, /blocked, /diagnosis, /status) | 10 s |
Wachten op de laatste 200 na een annulering | 30 s, daarna procedure voor onopgeloste gevallen |
| Herstelpogingen voordat de terminal onbereikbaar wordt verklaard | 3, daarna procedure voor onopgeloste gevallen |
De terminal gebruikt zijn eigen time-out per endpoint, ingesteld in de configuratie van de HTTP-server: 180 s op /payment, hetzelfde op elke terminal. Dit is de tijd die de terminal wacht op het laatste antwoord van de betaaltoepassing; daarna antwoordt GET met 408 in plaats van het antwoord. Houd uw betalingsperiode daaronder. Wanneer de betalingsperiode verstrijkt, mag u het scherm niet vrijgeven en mag u de betaling niet opnieuw verzenden: ga naar de herstelprocedure.
6. Draai terug met de POITransactionID als er twee betaalmethoden zijn gebruikt
Als een kaartbetaling achteraf goedgekeurd blijkt nadat de klant al op een andere manier heeft betaald, verzendt u een ReversalRequest met de OriginalPOITransaction.POITransactionID uit het dagboek of uit /status. Draai nooit terug met de SaleTransactionID.
De herstelprocedure
Voer deze uit wanneer de ECR start met een item met status IN_FLIGHT, wanneer een poll mislukt of de betalingsperiode verstrijkt, wanneer een POST antwoordt met 403 of op transportniveau mislukt, en wanneer GET /payment antwoordt met 408 of 423 terwijl er nog een item met status IN_FLIGHT bestaat. Gebruik voor elke aanroep een nieuwe ServiceID. De drie stappen beantwoorden drie verschillende vragen: is er nog een uitwisseling open, is de betaaltoepassing nog bezig en wat was het laatste resultaat.
Stap 1: is er nog een HTTP-uitwisseling open?
GET /blocked geeft elk endpoint met een open uitwisseling weer als gewone JSON:
GET http://<terminal-ip>:8080/blocked
200 {"blocked":["/payment"]} a result is waiting, or still being produced
200 {"blocked":[]} no open exchange-
/paymentwordt vermeld: hervatGET /paymenttotdat dit met200antwoordt. Dat antwoord is de uitkomst, ongeacht hoe lang geleden de betaling is beëindigd. Roep nu geen/statusaan: dit zou de vorige verkoop retourneren. Als de leeglopendeGETantwoordt met408of423, ga dan naar stap 2. -
De lijst is leeg: het laatste antwoord is al gelezen (een
200of een408), of de betaling is nooit gestart. Stap 2 vertelt u of de betaaltoepassing nog aan iets werkt.
Stap 2: is de betaaltoepassing nog bezig?
POST http://<terminal-ip>:8080/diagnosis
{ "MessageHeader": { ..., "MessageCategory":"Diagnosis", "ServiceID":"1042" },
"DiagnosisRequest": {"POIID":"POI_01"} }
200
{ "DiagnosisResponse": { "Response": {"Result":"Success"},
"POIStatus": {"GlobalStatus":"OK"}, ... } }GlobalStatus | Betekenis | Actie van de ECR |
|---|---|---|
OK | De betaaltoepassing is niet actief | Ga naar stap 3. |
Busy | Er loopt nog een transactie in de betaaltoepassing, zonder uitwisseling die nog kan worden gelezen (/status antwoordt in dezelfde situatie met InProgress) | Controleer elke 5 s opnieuw binnen de betalingsperiode. Nog steeds Busy na de periode: verzend één /abort met verwijzing naar de ServiceID van het actieve item en controleer opnieuw. Nog steeds Busy: markeer het item als UNRESOLVED en vraag de kassier om de terminal opnieuw op te starten; voer daarna de herstelprocedure opnieuw uit. |
Geen antwoord (transportfout of 408) | Terminal of betaaltoepassing antwoordt niet | Tel één poging; markeer het item na 3 pogingen als UNRESOLVED. |
Stap 3: lees de laatst voltooide transactie
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"}}, ... } } } }Waarschuwing
De buitenste Response.Result = Success betekent alleen dat de terminal een te retourneren transactie heeft gevonden. De uitkomst van de betaling staat in de binnenste PaymentResponse.Response, en pas na de consistentiecontrole.
De geretourneerde transactie komt alleen overeen met uw dagboekitem als RepeatedMessageResponse.MessageHeader.ServiceID, SaleTransactionID.TransactionID en SaleTransactionID.TimeStamp gelijk zijn, MessageCategory Payment is en het bedrag het aangevraagde bedrag is (of lager, bij een gedeeltelijke goedkeuring).
Wat /status retourneert
| Conclusie | Actie van de ECR |
|---|---|---|
Overeenkomst, binnenste Result: Success
| Betaling goedgekeurd | Sla POITransactionID op, druk de PaymentReceipt af en sluit de verkoop als betaald. |
Overeenkomst, binnenste Result: Failure (Aborted, Refusal, Cancel...)
| Betaling niet uitgevoerd | Sluit de poging als onbetaald af. Een nieuwe poging gebruikt een nieuwe ServiceID. |
Geen overeenkomst of NotFound, en sinds het verzenden van de aanvraag geen LoggedOut gezien | De terminal heeft uw betaling nooit gestart | Sluit de poging als onbetaald af. Een nieuwe poging is veilig met een nieuwe ServiceID. |
Failure met InProgress
| De betaling loopt nog | Wacht 5 s en vraag opnieuw binnen de betalingsperiode; volg daarna de regel Busy van stap 2. |
Failure met LoggedOut
| Geen sessie (herstart, update van de toepassing of SaleID/POIID verschilt van Login) | Verstuur onmiddellijk /login en herhaal daarna stap 1 tot en met 3 eenmaal. |
| Nog steeds geen overeenkomst na een herstart van de terminal | Onbekend: een autorisatie kan vóór de herstart uit de terminal zijn vertrokken | Markeer het item als UNRESOLVED. Behandel het niet als onbetaald. |
| Geen antwoord na 3 pogingen | Terminal onbereikbaar | Markeer het item als UNRESOLVED. |
Procedure voor onopgeloste gevallen
- Bewaar het item in persistente opslag met de status
UNRESOLVED, plus kassa, bedrag en tijd, en waarschuw de winkelverantwoordelijke of backoffice. Een andere betaalmethode is alleen via deze expliciete procedure toegestaan, nooit stilzwijgend. - Wanneer de terminal opnieuw bereikbaar en niet actief is, voert u stap 1 tot en met 3 automatisch uit voor elk item met de status
UNRESOLVED. Als de kaartbetaling goedgekeurd blijkt en een andere betaalmethode werd gebruikt, draait u de kaartbetaling terug (regel 6). - Als het resultaat niet langer van de terminal kan worden gelezen, stemt u af met het transactierapport van de handelaar voordat u de klant terugbetaalt.
Annuleren wanneer de klant afhaakt
Als de betaling na de betalingsperiode nog loopt (bijvoorbeeld omdat de klant is weggegaan bij de vraag om de pincode), kan de kassier annuleren: verstuur een AbortRequest waarvan MessageReference de ServiceID van de betaling bevat en blijf daarna GET /payment pollen. De laatste 200 met ErrorCondition: Aborted is de uitkomst. Gebruik na een kaartverwerving (betaling in twee stappen) de Admin-variant (AdminRequest met DualTransactionAborted) die in de Local API-specificatie wordt beschreven.
Waarschuwing
Een annulering is een aanvraag, geen garantie: als de autorisatie al bezig is, kan de betaling nog steeds goedgekeurd worden en op sommige terminals wordt de annulering niet verwerkt. Lees altijd de laatste 200; als die binnen 30 s niet komt, markeert u het item als UNRESOLVED. Als de uitwisseling van de betaling al is beëindigd met een 408, verstuurt u de annulering toch en leest u de uitkomst met /status.
Wat de kassier of klant ziet
| Toestand van de ECR | Bemande kassa | Zelfcheckout |
|---|---|---|
| Herstel wordt uitgevoerd (meestal enkele seconden) | "Betaling wordt gecontroleerd, even geduld". De knop Kaartbetaling blijft uitgeschakeld. | "Uw betaling wordt bevestigd" met een voortgangsindicator. De klant kan niet teruggaan. |
| Betaling loopt nog na het verstrijken van de periode | Slechts twee keuzes: Blijven wachten of Betaling annuleren (stuurt de annulering). | Automatische annulering, daarna "Betaling geannuleerd, probeer opnieuw". |
UNRESOLVED | "Betaling kon niet worden bevestigd". Een andere betaalmethode pas na bevestiging door de supervisor; elke bevestigde dubbele betaling wordt teruggedraaid. | Bel de medewerker. Bied niet automatisch een andere betaalmethode aan. |