VPOSCOINGuía de integración
v2.6.0 · code 10

Integración con la terminal VPOSCOIN

La terminal es un puente: tu aplicación le manda comandos JSON y ella ejecuta las operaciones de pago contra el SDK de MIT sobre una PAX IM30. Un comando es una línea de JSON; una respuesta, otra.

Formato

Una línea de JSON por mensaje. La terminal responde siempre con un objeto que lleva el campo response y cierra con \r\n.

Orden

Los comandos entran a una cola FIFO con un único consumidor. Un comando lento no adelanta a otro, y una excepción no mata la cola.

Asincronía

Casi nada responde de forma síncrona. Un PAYMENT devuelve un eco al instante y el resultado real llega minutos después.

Lo primero que hay que entender. La respuesta a un comando y el resultado de la operación son cosas distintas. LOGIN_MIT, INIT, PAYMENT, REFUND y DEVICEINFO retornan de inmediato y su resultado llega después, por un callback del SDK, sin identificador que lo ate al comando que lo pidió. Si tu integración espera una respuesta síncrona, se va a quedar esperando.

Conexión

Hay dos enlaces físicos posibles y pueden estar activos a la vez. El modo se consulta con GETTRANSPORT y se cambia con SETTRANSPORT.

ModoEnlaceDel lado del PC / kiosko
usbhostpor omisión Puerto USB Host + adaptador FTDI o Prolific Cable null-modem contra tu puerto serie
usbc Puerto USB-C en modo periférico (CDC-ACM) Aparece como USB Virtual Serial Port (PAX), un COM estándar
both Los dos a la vez Las respuestas se difunden a todos los enlaces abiertos

Parámetros del puerto

Velocidad
115200 baudios
Formato
8 bits de datos, sin paridad, 1 de parada (8N1)
Terminador
\r\n al final de cada mensaje
Codificación
UTF-8
En modo both, cada kiosko ve respuestas a comandos que no mandó. No es un defecto: TRANSACTION_RESULT y los errores del SDK salen de callbacks asíncronos sin vínculo con quien los originó, así que contestarle solo al que preguntó significaría perder avisos. Tu integración debe ignorar lo que no reconozca.

Handshake mínimo

La terminal considera probado el enlace en cuanto recibe una línea de JSON válida, no antes — con cualquier línea valdría el ruido del puerto. A partir de ahí deja de avisar de cables incompatibles.

  1. {"action":"CONNECT"} — declara que hay alguien del otro lado.
  2. {"action":"LOGIN_MIT", …} — sesión y entorno.
  3. {"action":"INIT"} — lector y llaves de transacción.
  4. Ya se puede cobrar.

Cualquier comando desconocido recibido antes de un CONNECT se trata como si fuera CONNECT. Es tolerancia deliberada con kioskos que arrancan hablando de otra cosa.

Entornos DEV / QA / UAT / PROD

El entorno no depende del APK instalado. Es un parámetro en tiempo de ejecución que persiste en el dispositivo y sobrevive a las actualizaciones.

Omitir server en LOGIN_MIT no conserva el entorno actual: lo pone en PROD. La clave ausente llega como la cadena "null" y cae en la rama por omisión. Es el valor seguro, pero significa que cada LOGIN_MIT reescribe el entorno — para quedarte en QA hay que mandarlo explícito cada vez.

Dos formas de comprobar en qué entorno está una terminal:

  • GETCONFIG y mirar server, o GETSESSION y mirar server. Es la vía fiable.
  • Visualmente: tras un login, la pantalla muestra SERVIDOR <entorno> durante 4 segundos solo si no es PROD. En producción no aparece nada, así que la ausencia del aviso es la confirmación.

LOGIN_VENDING se comporta al revés y conserva el entorno si omites server. La inconsistencia es real y conviene tenerla presente.

Canal de administración remota

Además del cable, la terminal se suscribe a un canal de mensajería (Pusher, cluster us2) por el que acepta cualquier comando del protocolo. No hay lista blanca, con dos excepciones señaladas en la tabla de comandos.

ConceptoValor
Canal de comandosEl número de serie de la terminal (p. ej. 1640385865), más fleet-all para toda la flota
Evento entranterooster_pax_status
Evento de respuestaclient-vpos-result
Clusterus2

Formato del evento

{
  "action": "PAYMENT",
  "request_id": "de91f0a5-…",
  "respond": "both",
  "params": { "amount": "25.00" }
}

respond decide a dónde va la respuesta, y se lee tanto al nivel del evento como dentro de params:

ValorEfecto
remotepor omisiónRespuesta por HTTP al endpoint de resultados
serialSale por el cable, como si lo hubiera pedido el kiosko
bothLas dos
El destinatario remoto sobrevive al comando, y tiene que hacerlo. Como casi nada responde de forma síncrona, cerrar el canal de respuesta al terminar de ejecutar haría que un LOGIN_MIT remoto se ejecutara pero su respuesta se fuera solo por el cable. El destinatario vive 150 s — por encima del watchdog de 120 s — o hasta que llega otro comando.
Quien pueda publicar en el canal puede cobrar. Publicar exige el secreto de la aplicación de mensajería, que no viaja en el APK y vive solo en el servidor. Ese es el único límite de confianza. Suscribirse solo necesita la clave pública, y por eso las respuestas viajan por HTTP a la plataforma y no por el canal.

Si la terminal queda incomunicada

Tres vías de rescate, en orden:

  1. Serial, si aún hay enlace.
  2. El canal remoto — la única que no depende del cable.
  3. ADB, que no depende de ninguna de las dos:
    adb shell am start -n coincity.im30v25/mx.com.mit.mobile.integration.MainActivity \
      --es set_transport usbhost

Flujo de un cobro

Lo que ve tu integración de principio a fin, con un cobro aprobado.

Protección antiduplicado del SDK. Un cobro con el mismo monto y la misma tarjeta dentro de una ventana corta se rechaza con BAD_REQUEST y la descripción «La transacción ya fue aprobada / rechazada a las: HH:MM:SS». No es un defecto de esta app ni de tu integración: lo añadió el SDK de MIT en la versión 2.7. Cambiar el monto lo evita; reiniciar la app no, y la referencia es irrelevante. En pruebas estorba constantemente — varía el monto entre intentos.

Comandos

Todo lo que la terminal acepta. Los marcados remoto también funcionan por el canal de administración; los marcados reinicia reinician la aplicación al aplicarse.

Respuestas no solicitadas

Mensajes que la terminal emite sin que nadie los pida. Tu integración tiene que estar preparada para recibirlos en cualquier momento.

Errores y estados de rechazo

RespuestaCuándoQué hacer
PAYMENT_BUSY Llega un PAYMENT con otro cobro en curso Esperar al TRANSACTION_RESULT del anterior, o mandar STOP
ERROR / NO_SESSION Cobro sin sesión MIT cargada Mandar LOGIN_MIT y reintentar
ERROR / ER_0004 INIT no consigue las llaves de transacción Verificar el entorno con GETCONFIG. Si el login entra pero INIT falla, suele ser aprovisionamiento del lado de MIT
UNKOWN_ACTION Comando no reconocido Revisar el nombre. Si connectFlag es falso, en cambio, el comando se convierte en CONNECT en silencio
REMOTE_BUSY / REMOTE_ERROR Fallo procesando un comando llegado por el canal remoto Reintentar con otro request_id

Tiempos de espera

Hay dos temporizadores y no son lo mismo:

  • El watchdog de la app, 120 s desde que arranca la pantalla de espera. Si el SDK no responde — sin internet, por ejemplo — fuerza el reset y emite un TRANSACTION_RESULT no aprobado.
  • El del lector, configurable, que salta si el cliente no presenta la tarjeta. Llega como error del SDK con código 06.

Webhook de ventas

Alternativa al serial para recibir las ventas: la terminal las entrega por HTTP POST a una URL tuya. Está apagado mientras no configures una.

{
  "type": "sales",
  "count": 2,
  "records": [ { /* mismo objeto que TRANSACTION_RESULT */ } ]
}
  • Paquetes de máximo 50 registros por envío.
  • El keepalive va aparte: {"type":"keepalive","pending":N}.
  • La cola vive en disco, no en memoria: la terminal se reinicia con normalidad y una cola en RAM perdería ventas ya cobradas. Tope de 2000; al pasarse se descartan los más viejos.
  • El vaciado se detiene al primer paquete que falla, para conservar el orden.
  • Con send: "ALL" también llegan los cobros fallidos, marcados con payment_failed: true. Las devoluciones no entran.
El endpoint debe ser HTTPS. El tráfico en claro está bloqueado en la terminal, así que un http:// falla con «Cleartext HTTP traffic not permitted» y la cola crece sin que llegue nada. Se ve en last_error de GETWEBHOOK.