Lectura de tarjetas

Lectura de tarjetas

ℹ️

Este proceso es obligatorio

En esta fase se procede a la recuperación de datos de la tarjeta mediante la implementación del estándar EMV, haciendo uso del kernel del dispositivo.

Requisitos

Resultado

  • Después del proceso de lectura, se obtendrá un objeto con todos los datos de la tarjeta requeridos para procesar operaciones de pago, anulaciones o devoluciones.

Paso a paso

  1. Instanciar el objeto CardProcessData.
  2. Invocar el método findCardProcess y pasarle como parámetro:
    1. El objeto OperationFlow
    2. El contexto
    3. El modo de lectura a procesar
    4. El número de tarjeta cifrado: Aplica para anulaciones y devoluciones. Enviar el número de tarjeta cifrado retornado en la consulta de transacciones, aplica para validar que se está usando la misma tarjeta del pago original, en caso de no requerirlo, enviar null.
  3. Configurar los observers para recibir el resultado de la lectura.

Instanciar objeto OperationFlow

  • Amount: Objeto que contiene información del monto de la transacción en formato ISO. Es decir, los 2 últimos dígitos representan los decimales y no lleva punto decimal. Ejemplo: $10.00 = 1000
  • Capture: Objeto que contiene información para la captura de los datos de la tarjeta. Se requiere instanciar los objetos:
    • Capture(): Instanciar el objeto así: operationFlow.capture = Capture()
    • Card(): Instanciar el objeto. Así: operationFlow.capture!!.card = Card()
    • Holder(): Instanciar el objeto. Así: operationFlow.capture!!.card.holder = Holder()
    • Terminal(): Instanciar el objeto. Así: operationFlow.terminal = Terminal()
  • TransactionType: Tipo de transacción. Este objeto es un enum con los siguientes valores:
enum class OperationType {
   PAYMENT,
   PREAUTHORIZATION,
   REFUND,
   ANNULMENT,
   POSTAUTHORIZATION
}

Instanciar objeto Amount

  • Breakdown: Listado de desglose de montos. Está compuesta por monto y descripción. Listado de valores:

    • OPERATION: si la transacción tiene propina, aquí va el valor base. Si la transacción no tiene propina, aquí va el valor total.
    • TIP: (aplica cuando la transacción tiene propina, aquí solo va el valor de la propina).
  • Currency: Código de moneda. Posibles valores:

    • ARS = Pesos Argentinos
    • MX = Pesos Mexicanos
    • USD = Dólares americanos. Disponible para Argentina.
  • Total: Monto total de la transacción en formato ISO. Ejemplo: $25.50 = 2550

Ejemplo de implementación

val cardProcessData = CardProcessData()
 cardProcessData.findCardProcess(
            operationFlow = doOperationFlow(amount),
            context = this,
            inputModeType = InputMode.ALL
        )
cardProcessData.selectApp.observe(this, selectAppObserver)
cardProcessData.navigate.observe(this, navigateObserver)
 
    private fun doOperationFlow(
        baseAmount: String,
        tipAmount: String,
        totalAmount: String
    ): OperationFlow {
        operationFlow.amount = Amount()
        
        // Crear breakdown para el monto base (siempre presente)
        val breakdownAmount = Breakdown()
        breakdownAmount.description = OPERATION
        breakdownAmount.amount = StringUtils.notFormatAmount(baseAmount)
        Log.i(TAG, "AMOUNT: ${breakdownAmount.amount}")
 
        // Crear lista de breakdowns
        val breakdownList = mutableListOf<Breakdown>()
        breakdownList.add(breakdownAmount)
 
        // Agregar breakdown de propina solo si es mayor a 0
        val tipAmountValue = tipAmount.toIntOrNull() ?: 0
        if (tipAmountValue > 0) {
            val breakdownTipAmount = Breakdown()
            breakdownTipAmount.description = TIP
            breakdownTipAmount.amount = StringUtils.notFormatAmount(tipAmount)
            breakdownList.add(breakdownTipAmount)
            Log.i(TAG, "TIP: ${breakdownTipAmount.amount}")
        } else {
            Log.i(TAG, "No tip amount")
        }
 
        operationFlow.capture = Capture()
        operationFlow.capture!!.card = Card()
        operationFlow.apply {
            amount?.let {
                it.total = StringUtils.notFormatAmount(totalAmount)
                it.currency = currency
                it.breakdown = breakdownList
            }
        }
        if (operationType.isNotNull()) {
            if (operationType != "PAYMENT" && operationType != "PREAUTHORIZATION") {
                if (currency == CURRENCY_LABEL_MX) {
                    operationFlow.transactionType = OperationType.REFUND
                } else {
                    if (isToday(transaction.operation.datetime)) {
                        operationFlow.transactionType = OperationType.ANNULMENT
                    } else {
                        operationFlow.transactionType = OperationType.REFUND
                    }
                }
                //Se agregan identificadores del pago original
                operationFlow.acquirer_id = transaction.operation.acquirer_id
                operationFlow.payment_id = transaction.operation.id
            } else {
                when (operationType) {
                    OperationType.PAYMENT.name -> operationFlow.transactionType =
                        OperationType.PAYMENT
 
                    OperationType.PREAUTHORIZATION.name -> operationFlow.transactionType =
                        OperationType.PREAUTHORIZATION
                }
            }
        } else {
            operationFlow.transactionType = OperationType.PAYMENT
        }
 
        //inicializar otros objetos
        operationFlow.capture!!.card?.holder = Holder()
        operationFlow.terminal = Terminal()
 
        OperationFlowHolder.operationFlow = operationFlow
        return operationFlow
    }
 
    private val navigateObserver: (Any) -> Unit = {
        when (it) {
            is Bundle -> {
        
                val status = it.get("status")
                val statusResult: StatusResult = status as StatusResult
 
                val action = ActionType.valueOf(statusResult.readerAction ?: EMPTY)
                when (action) {
                     ActionType.START_READER_AGAIN_WITH_SWIPE -> {
                        cardProcessData.setFallbackFlag() // Importante para notificar al adquirente que fue fallback banda, no requerido para los otros actions, solo para START_READER_AGAIN_WITH_SWIPE
                        instructionMessageState.value = status.message ?: "Use la banda"
                        cardProcessData.findCardProcess(
                            operationFlow = operationFlow,
                            context = this,
                            inputModeType = InputMode.SWIPE,
                            encryptedPanToValidate = if (operationType != "PAYMENT" && operationType != "PREAUTHORIZATION") {
                                transaction.card.pan
                            } else null
                        )
                    }
                    
                    ActionType.START_READER_AGAIN_WITH_CONTACT -> {
                        // Action para solicitar nuevamente la lectura insertando la tarjeta 
                    }
                   
                    ActionType.START_READER_AGAIN_WITH_CONTACTLESS -> {
                        // Action para solicitar nuevamente la lectura acercando la tarjeta 
                    }
 
                    else -> {
                        // Mostrar mensaje de error: statusResult.description
                    }
                }
            }
 
            is String -> {
                //La lectura fue exitosa, continuar a validar BIN
                val bundle = Bundle().apply {
                    putString("bin", it)
                }
                val intent = Intent(this, CardRulesValidationActivity::class.java).apply {
                    putExtras(bundle)
                }
                startActivity(intent)
            }
        }
    }

Observadores de respuesta

El resultado se recibe mediante 2 observadores: observer navigateObserver y readerInfoMessage. En la siguiente tabla se muestran los posibles objetos que se pueden recibir como respuesta de navigateObserver:

ObjetoDescripción
Bundle de tipo StatusResultEste objeto se devuelve en caso de fallo durante el proceso de lectura.
StringEste objeto se devuelve cuando el proceso de lectura es exitoso, retorna el BIN de la tarjeta, los primeros 8 dígitos del número de la tarjeta.

Para el observer readerInfoMessage el tipo de objeto a recibir es un String. Estos son notificaciones del kernel que se puede o no mostrar al usuario. Por ejemplo, notifican cuando la tarjeta se detectó ya sea por banda, contactless o chip. Este código de ejemplo muestra cómo implementarlos:

cardProcessData.readerInfoMessage.observe(this) { infoMessages ->
    infoMessages?.let { infoMessagesObserver(it) }
}
private val infoMessagesObserver: (DataToast) -> Unit = { infoMessages ->
    Log.i(TAG, "${infoMessages.message}")
}

Objeto StatusResult

CampoDescripción
codeCampo de tipo String. Contiene los posibles errores en la lectura de tarjetas.
descriptionCampo de tipo String. Contiene una descripción del código de error.
readerStatusTypeCampo de tipo String que se puede mapear el Enum MessageType. Contiene el tipo de error: INFO es mensaje informativo. ERROR es el tipo de mensaje que requiere evaluar la acción sugerida por el SDK.
readerActionCampo de tipo String que se puede mapear el Enum ActionType. Contiene la acción que se espera de la App.

Campo readerAction

ValorDescripción
START_READER_AGAIN_WITH_CONTACTSe recomienda iniciar nuevamente la lectura insertando la tarjeta.
START_READER_AGAIN_WITH_SWIPESe recomienda iniciar nuevamente la lectura deslizando la tarjeta.
START_READER_AGAIN_WITH_CONTACTLESSSe requiere acercar nuevamente la tarjeta.
RETRYSe recomienda intentar nuevamente la lectura.
TRY_ANOTHER_CARDSe recomienda intentar nuevamente la lectura usando una tarjeta diferente.

Para la implementación en Apps externas, se recomienda evaluar el readerAction para tomar una decisión, sin embargo, a continuación se muestran los posibles errores que puede retornar el kernel en el campo code:

Lista de posibles códigos de error

CódigoDescripción
PLS_USE_CONTACT_IC_CARDPruebe insertando la tarjeta.
SEE_PHONE_REMOVE_AND_PRESENT_CARDLea las instrucciones en su teléfono.
PLS_SECOND_TAP_CARDAcerque la tarjeta nuevamente.
USE_MAG_STRIPEUse banda.
TAP_CARD_DETECTEDTarjeta detectada.
INSERTED_CARDTarjeta insertada.
MSRTarjeta detectada.
OFFLINE_DECLINEDTarjeta no aceptada, intente con otra tarjeta.
DECLINE_OFFLINEIntente con otra tarjeta.
END_APPLICATIONPruebe insertando la tarjeta.
DISPLAY_BALANCEIntente con otra tarjeta.
APPLICATION_BLOCKEDTarjeta bloqueada, intente con otra tarjeta.
TRY_AGAIN_RESENT_CARDPruebe insertando la tarjeta.
INSERT_SWIPE_OR_TRY_ANOTHER_CARDPruebe insertando la tarjeta.
TERMINATEPruebe insertando la tarjeta.
PROCESSING_ERRORIntente con otra tarjeta.
OTHER_INTERFACESPruebe insertando la tarjeta.
RETRYOcurrió un error, intente nuevamente.
NO_CARDTarjeta no detectada, pruebe insertando la tarjeta.
NOT_ICCNo se pudo leer la tarjeta, use banda.
BAD_SWIPELa banda está dañada, pruebe insertando o acercando la tarjeta.
USE_ICC_CARDPruebe insertando la tarjeta.
NEED_FALLBACKUse banda.
TIMEOUTSe agotó el tiempo de lectura.
DEVICE_BUSYEl lector está ocupado, intente nuevamente.
MULT_CARDPresente sólo una tarjeta.
TERMINATEDPruebe insertando la tarjeta.
CANCELEDSe canceló la lectura de tarjeta.
CANCELED_OR_TIMEOUTSe canceló la lectura de tarjeta.
NO_EMV_APPSIntente deslizando la tarjeta.
SELECT_APP_FAILIntente con otra tarjeta.
CARD_ERRORTarjeta bloqueada.
INVALID_ICC_DATAIntente con otra tarjeta.
APPLICATION_BLOCKED_APP_FAILTarjeta bloqueada.
CARD_BLOCKED_APP_FAILTarjeta bloqueada.
KERNEL_ERRIntente con otra tarjeta.
ERR_MULT_CARDIntente con otra tarjeta.
ERR_CHECK_CARDIntente con otra tarjeta.