Evitar pagos duplicados desde el ECR

Un pago duplicado casi siempre comienza de la misma manera: el ECR pierde la respuesta de un pago que ya envió y luego lo vuelve a enviar o permite que el cliente pague de otra forma. Este artículo proporciona las reglas que debe implementar su ECR y la lógica de recuperación que debe ejecutar con la API local (Nexo Retailer 3.1, JSON sobre HTTP, puerto 8080).

Por qué ocurren los duplicados

El terminal de pago y el adquirente solo conocen las solicitudes que recibieron. No pueden saber si un segundo pago de la misma cesta es un error o una nueva compra. Solo el ECR lo sabe, por lo que la prevención de duplicados es responsabilidad del ECR. Causas habituales:

  • El pago con tarjeta se aprueba, la respuesta se pierde (corte de red, tiempo de espera agotado del ECR o bloqueo) y el cajero pulsa de nuevo Pago con tarjeta, o el cliente paga en efectivo. Nadie sabe que se cargó la tarjeta, por lo que nadie revierte el pago.
  • El cajero hace doble clic o la biblioteca HTTP vuelve a intentar silenciosamente un POST después de un error de conexión: la misma solicitud llega dos veces al terminal.
  • El ECR se reinicia durante un pago y comienza la siguiente venta sin leer el resultado de la anterior.

Nota

La única regla: nunca vuelva a enviar, sustituya ni abandone un pago hasta que el ECR conozca su resultado. Todas las demás reglas de este artículo sirven a ese único propósito.

Lo que el terminal de pago hace y no hace por usted

Dos componentes responden a sus solicitudes: el servidor HTTP del terminal, que mantiene un intercambio por endpoint (un POST lo abre y el 200 final en GET lo cierra), y la aplicación de pago, que ejecuta la transacción. Pueden encontrarse en estados diferentes, y la lógica de recuperación comprueba ambos.

SituaciónComportamiento del terminalQué significa para el ECR
Segundo POST /payment mientras hay un intercambio abierto en ese endpoint 403 Forbidden, independientemente del ServiceID Los duplicados en paralelo se bloquean. Un 403 no es un rechazo: el primer pago sigue abierto. Ejecute la lógica de recuperación.
POST /payment reenviado con un ServiceID ya utilizado, después de que finalizara el primer pago 202 Accepted: se procesa como un pago nuevo, con un nuevo POITransactionID El terminal no elimina duplicados. Los valores únicos de ServiceID son responsabilidad suya.
GET /payment después de haber leído el 200 final, o cuando no se ha iniciado nada 423 Locked: no hay ningún intercambio abierto en este endpoint El resultado se puede leer una sola vez mediante GET /payment. Después, utilice POST /status.
GET /payment cuando la aplicación de pago no entregó la respuesta final dentro del tiempo de espera del endpoint del terminal 408 Request Timeout, enviado por el terminal (no por su cliente HTTP). Es la respuesta final de ese intercambio: al igual que un 200, debe leerse. Hasta entonces, el endpoint permanece en /blocked y un nuevo POST responde 403. El 408 no detiene el pagoEl pago continúa ejecutándose hasta su propio final; su respuesta no llegará mediante GET /payment. Ejecute la lógica de recuperación: /status responde InProgress hasta que finaliza y después devuelve su resultado.
GET /blockedEnumera los endpoints con un intercambio abierto (la respuesta final aún no se ha leído). Una respuesta final espera hasta que se lea y no caduca. /blocked solo refleja el servidor HTTP, no la aplicación de pago, y no activa ninguna acción en el terminalUtilícelo para encontrar un resultado que aún no haya leído. No le indica si el terminal está libre: eso se comprueba mediante /diagnosis.
POST /diagnosisResponde incluso durante un pago y sin una sesión. POIStatus.GlobalStatus es OK o Busy Busy con una lista /blocked vacía significa que la aplicación de pago aún está procesando (se leyó un 408 mientras el pago estaba en curso o la aplicación está bloqueada). Ninguna solicitud del ECR puede liberarla.
POST /status (TransactionStatusRequest)Siempre HTTP 200. Devuelve el último pago, adquisición de tarjeta o reversión completados en RepeatedMessageResponse, aunque su intercambio HTTP nunca se haya leído. El MessageReference de la solicitud se ignoraSolo devuelve un resultado, que podría pertenecer a otra venta. Ejecute siempre la comprobación de coherencia.
POST /status mientras un pago sigue ejecutándose en la aplicación de pago Result: Failure, ErrorCondition: InProgress, sin RepeatedMessageResponse El pago no ha terminado. Siga consultando GET /payment si el intercambio está abierto; de lo contrario, espere y vuelva a preguntar.
POST /payment mientras el pago anterior sigue ejecutándose en la aplicación de pago (posible después de leer un 408) 202, seguido inmediatamente de un 200 con Result: Failure, ErrorCondition: Busy, AdditionalResponse TransactionInProgress No es un rechazo del nuevo pago ni el resultado del anterior. Espere a que termine el pago anterior y después recupere su resultado.
No hay una sesión válida: el terminal o la aplicación se reiniciaron, una actualización de la aplicación reinició el servidor, o SaleID y POIID difieren de los utilizados en el inicio de sesión Result: Failure, ErrorCondition: LoggedOut en cada solicitud hasta un nuevo /login. Una respuesta final producida antes de un reinicio se conserva y aún puede leerse mediante GET /payment; un pago interrumpido antes de producir una respuesta no deja nada (423) y /status responde entonces NotFound o devuelve una transacción anteriorEnvíe /login inmediatamente y después recupere el resultado. Tras un reinicio, NotFound significa desconocido, no no pagado.
POST /abort durante un pago 202, sin GET /abort (405). El pago termina en unos segundos con Result: Failure, ErrorCondition: Aborted en GET /payment, bajo el ServiceID del pago. Una cancelación enviada cuando no hay nada en curso también responde 202 y no tiene ningún efectoSiga consultando el endpoint de pago después de una cancelación.

Nota

Nexo Retailer también exige que el receptor rechace una solicitud cuyo ServiceID ya se haya utilizado (mensaje repetido). Actualmente, el terminal de pago no aplica esta comprobación: una solicitud reenviada es una transacción nueva. Un ECR creado según las reglas siguientes funciona en ambos casos.

Reglas que debe implementar en el ECR

1. Escriba un registro de transacciones antes de enviar

Antes de cada POST de un pago, una adquisición de tarjeta o una reversión, escriba una entrada en un almacenamiento persistente: ServiceID, SaleTransactionID (TransactionID y TimeStamp), endpoint, importe, divisa y estado IN_FLIGHT. En el 200 final, almacene POIData.POITransactionID y el resultado, y cierre la entrada. Después de un reinicio, una entrada que aún esté en IN_FLIGHT corresponde a un pago con resultado desconocido: resuélvalo antes que cualquier otra cosa en ese puesto.

2. Utilice un ServiceID nuevo para cada solicitud

  • Cada solicitud debe recibir un ServiceID nuevo, incluidas /status, /diagnosis, /abort, /login y un reintento de un pago cuyo procesamiento se haya confirmado como no realizado. Haga persistente el contador; nunca utilice una constante como "0".
  • El SaleTransactionID identifica la venta: debe ser único por cesta, no debe cambiar entre intentos para la misma cesta y siempre debe enviarse con su TimeStamp.

3. Bloquee la venta mientras su resultado sea desconocido

Desactive el botón de Pago con tarjeta en cuanto se pulse. Mientras una entrada esté en IN_FLIGHT para una cesta: no debe haber ninguna nueva solicitud de pago, ningún otro método de pago (efectivo, vale, segunda tarjeta) ni ninguna venta nueva en ese puesto. El bloqueo siempre termina con un resultado final, una cancelación controlada o el procedimiento sin resolver que se indica a continuación.

4. No permita que la capa HTTP reintente un POST

Desactive los reintentos automáticos en su cliente HTTP y en cualquier proxy entre el ECR y el terminal. Un POST que falle a nivel de transporte debe pasar a la lógica de recuperación, no a un reintento.

5. Consulte hasta obtener 200, con temporizadores adecuados para un pago real

POST /payment responde 202. Después, llame a GET /payment (sin cuerpo) hasta que responda 200.

Respuestas de GET /payment SignificadoAcción del ECR
200 PaymentResponse final Almacene el resultado, cierre la entrada del registro y desbloquee el puesto.
201Mensaje intermedio (por ejemplo, un DisplayRequest)Muestre el texto al cajero, siga consultando y reinicie el temporizador del pago.
204Pago en curso, aún no hay nada que leerSiga consultando.
206Se devuelve un mensaje y hay más mensajes en esperaProcéselo, reinicie el temporizador del pago y llame de nuevo inmediatamente a GET /payment.
408La aplicación de pago no respondió dentro del tiempo de espera del endpoint del terminal. La lectura del 408 cierra el intercambio; el pago aún puede estar en cursoVaya al paso 2 de la lógica de recuperación.
423No hay ningún intercambio abierto en este endpointVaya a la lógica de recuperación.
TemporizadorValor
Ventana total del pago (PIN, propina, selección de divisa, autorización en línea)120 s, nunca menos de 90 s
Pausa entre dos consultas GET De 0,5 a 1 s
Tiempo de espera de una sola solicitud HTTP (consulta GET, /blocked, /diagnosis, /status)10 s
Espera del 200 final después de una cancelación30 s, después procedimiento sin resolver
Intentos de recuperación antes de declarar que el terminal no es accesible3, después procedimiento sin resolver

El terminal aplica su propio tiempo de espera por endpoint, configurado en su servidor HTTP: 180 s en /payment, el mismo en todos los terminales. Es el tiempo que el terminal espera la respuesta final de la aplicación de pago; una vez transcurrido, GET responde 408 en lugar de la respuesta. Mantenga la ventana del pago por debajo de este valor. Cuando expire la ventana del pago, no libere la pantalla ni vuelva a enviar la solicitud: vaya a la lógica de recuperación.

6. Revierta con el POITransactionID si se utilizaron dos métodos de pago

Si un pago con tarjeta resulta aprobado después de que el cliente ya haya pagado de otra forma, envíe una ReversalRequest indicando OriginalPOITransaction.POITransactionID del registro o de /status. Nunca revierta utilizando el SaleTransactionID.

La lógica de recuperación

Ejecútela cuando el ECR se inicie con una entrada IN_FLIGHT, cuando falle una consulta o expire la ventana del pago, cuando un POST responda 403 o falle a nivel de transporte, y cuando GET /payment responda 408 o 423 con una entrada aún en IN_FLIGHT. Utilice un ServiceID nuevo para cada llamada. Los tres pasos responden a tres preguntas diferentes: si sigue abierto un intercambio, si la aplicación de pago sigue ocupada y cuál fue el último resultado.

Paso 1: ¿sigue abierto algún intercambio HTTP?

GET /blocked enumera todos los endpoints con un intercambio abierto, como JSON sin formato:

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

200 {"blocked":["/payment"]}     a result is waiting, or still being produced
200 {"blocked":[]}               no open exchange
  • /payment aparece en la lista: reanude GET /payment hasta obtener 200. Esa respuesta es el resultado, independientemente de cuánto tiempo haya pasado desde que terminó el pago. No llame ahora a /status: devolvería la venta anterior. Si la consulta GET que está vaciando el intercambio responde 408 o 423, vaya al paso 2.
  • La lista está vacía: la respuesta final ya se leyó (un 200 o un 408) o el pago nunca se inició. El paso 2 le indica si la aplicación de pago sigue trabajando en algo.

Paso 2: ¿la aplicación de pago sigue ocupada?

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

200
{ "DiagnosisResponse": { "Response": {"Result":"Success"},
                         "POIStatus": {"GlobalStatus":"OK"}, ... } }
GlobalStatusSignificadoAcción del ECR
OKLa aplicación de pago está inactivaVaya al paso 3.
BusyTodavía se está ejecutando una transacción dentro de la aplicación de pago, sin ningún intercambio desde el que pueda leerse (/status responde InProgress en la misma situación)Vuelva a comprobar cada 5 s dentro de la ventana del pago. Si sigue en Busy una vez superada la ventana: envíe un /abort que haga referencia al ServiceID en curso y vuelva a comprobar. Si sigue en Busy: marque la entrada como UNRESOLVED y pida al cajero que reinicie el terminal; después, vuelva a ejecutar la lógica de recuperación.
Sin respuesta (error de transporte o 408)El terminal o la aplicación de pago no respondenCuente un intento; después de 3, marque la entrada como UNRESOLVED.

Paso 3: lea la última transacción completada

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

Advertencia

El Response.Result = Success externo solo indica que el terminal encontró una transacción que devolver. El resultado del pago es el PaymentResponse.Response interno, y solo después de realizar la comprobación de coherencia.

La transacción devuelta coincide con la entrada del registro solo si RepeatedMessageResponse.MessageHeader.ServiceID, SaleTransactionID.TransactionID y SaleTransactionID.TimeStamp son iguales, MessageCategory es Payment y el importe es el solicitado (o inferior, en caso de una aprobación parcial).

Lo que devuelve /status ConclusiónAcción del ECR
Coincide, Result: Success interno Pago aprobadoAlmacene POITransactionID, imprima el PaymentReceipt y cierre la venta como pagada.
Coincide, Result: Failure interno (Aborted, Refusal, Cancel...)Pago no realizadoCierre el intento como no pagado. Un reintento utiliza un ServiceID nuevo.
No coincide o NotFound, sin haber visto LoggedOut desde que se envió la solicitudEl terminal nunca inició su pagoCierre el intento como no pagado. Es seguro reintentar con un ServiceID nuevo.
Failure con InProgress El pago sigue en cursoEspere 5 s y vuelva a preguntar, dentro de la ventana del pago; una vez superada, siga la línea Busy del paso 2.
Failure con LoggedOut No hay sesión (reinicio, actualización de la aplicación o SaleID/POIID diferente del utilizado en el inicio de sesión)Envíe /login inmediatamente y después repita los pasos 1 a 3 una vez.
Sigue sin haber coincidencia después de reiniciar el terminalDesconocido: es posible que una autorización haya salido del terminal antes del reinicioMarque la entrada como UNRESOLVED. No la trate como no pagada.
Sin respuesta después de 3 intentosTerminal inaccesibleMarque la entrada como UNRESOLVED.

Procedimiento sin resolver

  • Conserve la entrada en el almacenamiento persistente con el estado UNRESOLVED, además del puesto, el importe y la hora, y avise al supervisor de la tienda o al sistema administrativo. Solo se permite otro método de pago mediante este proceso explícito, nunca de forma silenciosa.
  • Cuando el terminal vuelva a estar accesible e inactivo, ejecute automáticamente los pasos 1 a 3 para cada entrada UNRESOLVED. Si el pago con tarjeta resulta aprobado y se aceptó otro método de pago, reviértalo (regla 6).
  • Si ya no se puede leer el resultado desde el terminal, concilie con el informe de transacciones del comercio antes de reembolsar al cliente.

Cancelar cuando el cliente desiste

Si el pago sigue en curso una vez superada la ventana del pago (por ejemplo, el cliente se alejó durante la solicitud del PIN), el cajero puede cancelarlo: envíe una AbortRequest cuyo MessageReference contenga el ServiceID del pago y siga consultando GET /payment. El 200 final con ErrorCondition: Aborted es el resultado. Después de una adquisición de tarjeta (pago en dos pasos), utilice la variante Admin (AdminRequest con DualTransactionAborted) descrita en la especificación de la API local.

Advertencia

Una cancelación es una solicitud, no una garantía: si la autorización ya está en curso, el pago aún puede terminar aprobado y, en algunas pantallas del terminal, la cancelación no se procesa. Lea siempre el 200 final; si no llega en 30 s, marque la entrada como UNRESOLVED. Si el intercambio del pago ya terminó con un 408, envíe la cancelación de todos modos y lea el resultado mediante /status.

Lo que ve el cajero o el cliente

Estado del ECRPuesto atendidoAutopago
Recuperación en curso (normalmente unos segundos)"Comprobando el pago, espere". El botón de Pago con tarjeta permanece desactivado."Confirmando su pago" con un indicador de progreso. El cliente no puede volver atrás.
El pago sigue en curso una vez superada la ventanaSolo hay dos opciones: Seguir esperando o Cancelar el pago (envía la cancelación).Cancelación automática y después "Pago cancelado, inténtelo de nuevo".
UNRESOLVED"No se ha podido confirmar el pago". Solo se permite otro método de pago después de la confirmación del supervisor; cualquier pago doble confirmado se revertirá.Llame al encargado. No ofrezca automáticamente otro método de pago.

 

¿Fue útil este artículo?

Usuarios a los que les pareció útil: 0 de 0