Cómo funciona el modelo de mensajes

  • Actualización

Antes de implementar un manejo real para pagos, reversos o cualquier otra operación, vale la pena entender el único patrón que rige toda la comunicación con la aplicación de pagos Market Pay. Cada operación en el SDK lo sigue. Una vez que lo comprendes, toda la API se vuelve predecible.

Esta guía explica tres cosas: por qué las respuestas no regresan del método que llamas, cómo distinguir las respuestas cuando todas llegan al mismo lugar, y por qué manejas los supertipos de respuesta — no sus subtipos de éxito.


Las solicitudes son fire-and-forget; las respuestas llegan por separado

Cuando llamas a algo como sendPaymentRequest, podrías esperar que devuelva el resultado del pago. No lo hace. La llamada envía la solicitud a la aplicación de pagos y regresa casi inmediatamente — no espera a que el terminal procese la tarjeta.

El resultado real llega después, de forma asíncrona, en el interceptor que registraste al inicializar el SDK. Esto es lo más importante que debes internalizar: el método que llamas y la respuesta que manejas están en dos lugares diferentes de tu código.

// Envías aquí…
viewModelScope.launch {
    clientSDK.sendPaymentRequest(/* … */)
}

// …y el resultado llega aquí, después, en el interceptor:
.bindInterceptor(
    interceptor = { message ->
        // la respuesta de pago aparece como `message`
        Result.success(message)
    }
)

¿Por qué está diseñado así? Un pago con tarjeta no es instantáneo — el terminal solicita al titular, se comunica con el adquirente y puede tardar varios segundos. Bloquear tu llamada durante todo ese tiempo congelaría tu app. Desacoplar la solicitud de la respuesta permite que el terminal tome el tiempo que necesite mientras tu app sigue respondiendo, y permite que la aplicación de pagos te envíe actualizaciones intermedias (ver "solicitudes de visualización" abajo) antes del resultado final.


Cada respuesta es un DomainMessage

El interceptor recibe un solo parámetro tipado como DomainMessage. Cada mensaje en la comunicación App2App — respuestas de inicio de sesión, respuestas de pago, errores, actualizaciones de progreso — es un subtipo de DomainMessage. Por eso un solo interceptor puede recibirlos todos.

Como todos llegan con el mismo tipo general, tu trabajo en el interceptor es verificar el tipo del mensaje entrante para decidir qué es y cómo manejarlo:

.bindInterceptor(
    interceptor = { message ->
        when (message) {
            is SuccessRetailerLoginResponse   -> { /* la sesión ahora está abierta */ }
            is SuccessRetailerPaymentResponse -> { /* pago aprobado */ }
            is ErrorRetailerPaymentResponse   -> { /* pago fallido */ }
            // …maneja los tipos de mensaje que tu app necesita
        }
        Result.success(message)
    }
)

Solo necesitas hacer coincidir los tipos de mensaje sobre los que tu aplicación actúa realmente. Los mensajes que no coinciden simplemente se ignoran — pero siempre devuelve Result.success(message) al final para que el SDK sepa que el mensaje fue aceptado.


Cada operación tiene su propio supertipo de respuesta

Las respuestas no son una lista plana de clases no relacionadas. Cada operación tiene un supertipo sellado, y bajo él están los resultados concretos — típicamente una variante de éxito y una de error. Hacer coincidir el supertipo captura todos los resultados de esa operación; hacer coincidir un solo subtipo captura solo uno.

OperaciónSupertipo de respuestaSubtipos que puedes recibir
Inicio de sesiónRetailerLoginResponse SuccessRetailerLoginResponse, ErrorRetailerLoginResponse
PagoRetailerPaymentResponse SuccessRetailerPaymentResponse, ErrorRetailerPaymentResponse, PartialRetailerPaymentResponse
ReversoRetailerReversalResponse SuccessRetailerReversalResponse, ErrorRetailerReversalResponse
ConciliaciónRetailerReconciliationResponse Success…, Error…
Estado de transacciónRetailerTransactionStatusResponse Success…, Error…
Adquisición de tarjetaRetailerAcquisitionResponse SuccessRetailerAcquisitionResponse, ErrorRetailerAcquisitionResponse
Diagnóstico (prueba de conexión)RetailerDiagnosisResponse Success…, Error…
Cierre de sesiónRetailerLogoutResponse Success…, Error…

Nota. Pago tiene un tercer subtipo, PartialRetailerPaymentResponse, que las otras operaciones no tienen. Esta es exactamente la razón por la que se razona en términos del supertipo: un manejador escrito solo para "éxito o error" perdería el caso parcial. La lista completa y autorizada de subtipos para cada operación está en la referencia de la API (sdk-doc.zip) — cuando tengas dudas, haz coincidir el supertipo.

Pago también envía actualizaciones de progreso

Durante un pago, antes del resultado final, la aplicación de pagos puede enviarte mensajes RetailerDisplayRequest. Estos no son el resultado del pago — te indican qué está haciendo el terminal en ese momento (esperando la tarjeta, contactando al host, etc.), lo cual es útil si quieres mostrar tu propia interfaz de progreso. La misma solicitud que produce una RetailerPaymentResponse puede generar varios mensajes RetailerDisplayRequest en el camino. Manéjalos si quieres progreso en vivo; ignóralos si no. El resultado final siempre es uno de los subtipos RetailerPaymentResponse.


Por qué esperas al supertipo, no al subtipo de éxito

Este es el error que causa los bugs más confusos, por eso tiene su propia sección. Es más importante cuando cambias de modo fire-and-forget a modo bloqueante — esperar una respuesta específica antes de continuar.

El SDK proporciona BlockingMessageGateway para esto. Le envías cada mensaje del interceptor, y luego puedes enviar una solicitud y suspender tu coroutine hasta que llegue la respuesta que coincide:

val gateway = BlockingMessageGateway()

// En el interceptor, pasa cada mensaje al gateway:
.bindInterceptor(
    interceptor = { message ->
        gateway.onNewMessage(message)
        Result.success(message)
    }
)

// Luego envía y espera, especificando el tipo de respuesta a esperar:
val response: RetailerLoginResponse = gateway.sendBlocking(
    timeoutMillis = 30000
) {
    clientSDK.sendLoginRequest()
}

when (response) {
    is SuccessRetailerLoginResponse -> { /* sesión iniciada */ }
    is ErrorRetailerLoginResponse   -> { /* inicio de sesión rechazado — manejarlo */ }
}

El tipo que pones entre los corchetes angulares / asignas es el tipo que sendBlocking espera. Aquí está la trampa:

  • Esperar RetailerLoginResponse (el supertipo) → la coroutine se reanuda en éxito o error. Manejas ambos. Correcto.
  • Esperar SuccessRetailerLoginResponse (el subtipo de éxito) → la coroutine se reanuda solo si el inicio de sesión tiene éxito. Si el inicio de sesión falla, nunca llega un SuccessRetailerLoginResponse, el gateway sigue esperando y tu coroutine se cuelga hasta que finalmente lanza TimeoutCancellationException. La respuesta de fallo llegó — solo que no la estabas escuchando.

En otras palabras: esperar el subtipo de éxito convierte silenciosamente cada fallo en un timeout. Siempre espera el supertipo para que ambos resultados reanuden tu coroutine, luego ramifica con un when como se muestra arriba.

sendBlocking usa por defecto un timeout de 60 segundos (timeoutMillis = 60000). Pasa un valor más corto para operaciones que deban fallar rápido. Cualquiera que sea el timeout, envuelve la llamada para manejar TimeoutCancellationException — un timeout significa que no llegó respuesta a tiempo, lo cual es una condición a la que tu app debe reaccionar.


El patrón en un párrafo

Envías una solicitud y regresa inmediatamente. La respuesta — y cualquier actualización de progreso antes de ella — llegan después en tu interceptor, todos tipados como DomainMessage. Verificas el tipo para encontrar los que te interesan. Cada operación tiene un supertipo sellado de respuesta que cubre todos sus resultados; razonas y (en modo bloqueante) esperas en términos de ese supertipo, nunca un solo subtipo, para que los fallos lleguen a tu código en lugar de desaparecer en un timeout. Cada operación en el SDK funciona exactamente así — así que una vez que manejas una, manejas todas.


Relacionado

  • Inicio rápido — el primer pago de extremo a extremo que sustenta este modelo.
  • Ciclo de vida de sesión e inicio de sesión — por qué el inicio de sesión debe tener éxito antes de que cualquier respuesta tenga sentido, y cuándo re-iniciar sesión.
  • Estado y manejo en primer plano — el otro canal asíncrono (el observador de eventos), separado del interceptor descrito aquí.
  • Solución de problemas: sendBlocking se cuelga hasta el timeout — el modo de fallo que la última sección de esta guía existe para prevenir.
  • Referencia de API (sdk-doc.zip) — la lista completa y autorizada de subtipos para cada operación.

¿Fue útil este artículo?

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