Pagos¶
El punto más delicado del sistema. Un bug acá no es un pixel mal puesto: es un doble cobro o un pago que queda en el limbo. Leé esta página entera antes de tocar cualquier flujo de cobro.
Los tres medios de pago¶
| Medio | Procesador | Dónde corre | Timeout en la terminal |
|---|---|---|---|
| QR | MercadoPago | Backend → MercadoPago; el cliente escanea con su app | ~210 s |
| Point Smart | MercadoPago Point | Terminal física (pinpad) de MercadoPago | ~90 s |
| Tarjeta | ApiCard | Daemon local en la terminal (localhost:50001) |
— |
En la terminal, los medios disponibles se cargan con PaymentMeansBloc (QR = 9001, Point Smart = 9002). El enum de procesadores es PaymentProcessors { mercadopago, pointsmart, apicard }.

La pantalla /mediosPago real: QR y Tarjetas (Point Smart), con el total a pagar. Cada opción abre el flujo de cobro correspondiente.
La regla de oro: idempotencia¶
El riesgo central de cualquier pago es cobrar dos veces cuando la red corta entre "cobré" y "recibí la confirmación". La defensa es el paymentAttemptId:
- Se genera una sola vez por intención de pago (
idProcessorPayment). - Se reusa en todos los reintentos del mismo intento.
- El backend deduplica por ese id: el mismo intento reenviado N veces produce un solo cobro.
Nunca generes un paymentAttemptId nuevo en un reintento
Si en un reintento generás un id nuevo, rompés la dedup y habilitás el doble cobro. El id es la identidad del intento, no del request. Esto es el fix INT-001 y es no negociable.
Recuperación de pagos in-flight¶
Un corte de luz o un crash en medio de un cobro no puede dejar la plata en el limbo. Por eso:
- Al iniciar un pago, la terminal persiste un
IsarPendingPaymenten Isar. - Al arrancar,
PendingPaymentService.pendientes()lee esos registros y reconcilia los pagos que quedaron sin resolver en la sesión anterior (fix INT-002). - Cuando el pago se resuelve, el registro se elimina.
Flujo QR (MercadoPago)¶
sequenceDiagram
participant POS as PagoQRPage
participant MP as MercadopagoService
participant API as Backend
POS->>MP: createIntent()
MP->>API: POST /payTicket (intent)
API-->>MP: QR + idProcessorPayment
POS->>POS: muestra el QR
Note over POS: cliente escanea con su app
loop hasta processed / timeout 210s
POS->>API: consulta estado (executeTransactionQuery)
API-->>POS: estado
end
POS->>POS: navega a ResultPaymentPage
La pantalla usa ConnectionRecoveryMixin para manejar timeout + reintento de la consulta sin colgarse.
Flujo Point Smart (MercadoPago Point)¶
La terminal física tiene su propia máquina de estados de orden (documentada en docs/mp-point_estados_order_validaciones.md):
stateDiagram-v2
[*] --> created
created --> at_terminal
at_terminal --> processed
at_terminal --> failed
at_terminal --> action_required
at_terminal --> expired
at_terminal --> canceled
processed --> refunded
| Estado | Qué hacer en la terminal |
|---|---|
created |
"Esperando terminal…", permitir cancelar. |
at_terminal |
"Complete en la terminal", bloquear duplicados. |
processed |
Éxito: conciliar monto/moneda, idempotencia en el cierre (no reimprimir). |
failed |
Mostrar motivo, ofrecer reintento o cambiar de medio. |
action_required |
Incierto (~40 s): "Revisar terminal", reintentar consulta, protocolo manual. |
expired |
>15 min: informar y, si sigue, generar nueva orden. |
canceled |
Desbloquear la UI. |
refunded |
Guardar referencia y ajustar conciliación. |
action_required y expired son los traicioneros
Son los estados donde el dinero puede estar cobrado pero la UI no lo sabe. Ante la duda, concilia por payment_id contra MercadoPago antes de dar la venta por perdida o por hecha. Nunca asumas el resultado: consultá.
Flujo Tarjeta (ApiCard)¶
ApiCard no pasa por el backend: la terminal habla con un daemon local.
PagoTarjetaPagearranca conApicardBloc.add(IdentificarCompra())→POST localhost:50001/identificacion_compra_online/.- Se muestran las cuotas (
PlanPagoPage). - El usuario elige cuotas →
ApicardBloc.add(AutorizarCompra())→POST .../autorizacion_compra_online/. - La anulación va por
.../autorizacion_anulacion_compra_online/.
La dirección del daemon es configurable (fix TPOS-016): apicardServer / apicardPort / apicardSsl en IsarConnectionConfig, con fallback a localhost:50001.
Resultado del cobro¶
ResultPaymentPage cierra el flujo con la animación correspondiente (confetti en éxito) y auto-redirige tras unos segundos. Recibe title, isError, errorMessage, paymentName, la ruta de redirect, etc.
Antes de tocar pagos, repasá también
- Comunicación POS ↔ Backend — resiliencia y polling.
- El estado
VOUCHERPENDINGdel ticket en el Glosario: pagado pero con el comprobante fiscal aún sin confirmar.