Saltar a contenido

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

Selección de medio de pago en la terminal

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 IsarPendingPayment en 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.

  1. PagoTarjetaPage arranca con ApicardBloc.add(IdentificarCompra())POST localhost:50001/identificacion_compra_online/.
  2. Se muestran las cuotas (PlanPagoPage).
  3. El usuario elige cuotas → ApicardBloc.add(AutorizarCompra())POST .../autorizacion_compra_online/.
  4. 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