domingo, 13 de junio de 2010

Mensajería ISO 8583 para la Banca

Uno de los proyectos mas importantes que he participado es el desarrollo de un internet banking, en el cual el corazón del mismo
se basa en la comunicación con el core Bancario utilizando el estandar ISO 8583

En la siguiente dirección se puede ver un poco de teoría de la definición de ISO 8583: http://es.wikipedia.org/wiki/ISO_8583

Para implementar el proyecto nos basamos en la librería http://sourceforge.net/projects/trx-framework/ la cual es muy buena para el tema de armar los mensajes ISO8583

Crear los mensajes y procesar las respuestas es muy facil, lo interesante en este proyecto fue el manejo de los puertos de comunicación con el core.

Basicamente para mandar un mensaje hay que:

Iso8583Message MensajeEnviar = new Iso8583Message(2000);
MensajeEnviar.Fields.Add(32, 310000);
MensajeEnviar.Fields.Add(2, sNumTarjetaDebito);
Comun Sender = new Comun();
RespuestaDTO = Sender.EnviarMensaje(MensajeEnviar);
//Si la respuesta es exitosa vamos a revisar el mensaje
if (RespuestaDTO.bExitoso)
                {
//Se verifica el campo 11 que es el utilizado como identificador unico del mensaje
                    if (RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field11Trace] != null)
//Revisamos el campo 39 que contiene el código de respuesta del mensaje
                        switch (int.Parse(RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field39Respuesta].ToString()))
                        {
//En caso que sea exitoso (00) procesamos el mensaje
//La respuesta del mensaje generalmente viene como un string en uno de los campos, en este caso
//La respuesta es todos la consulta consolidada del cliente dado la cual viene en el campo 48
//En este ejemplo está definido que cada registro de la respuesta es de 126 caracteres con lo cual
//procedemos a construir un array con cada uno de los registros del campo 48 y de ahí sacamos la data
                            case CodigoRespuesta.RespuestaExitosa:
                                ProductoDTO.bExitoso = true;
                                ModeloObjetos.Banca.Cliente Cliente = new ModeloObjetos.Banca.Cliente();
                                string sProducto = RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field48Productos].ToString();

                                //Se crea el array de de registros (a través de un metodo utilitario)
Array sRegistros = Comun.ProcesarResultado(RespuestaDTO.MensajeRecibido.Fields[TipoCampo.Field48Productos].ToString(), 126, 0);
//se recorren cada uno de los registros

                                foreach (string sRegistro in sRegistros)
                                {
                                    ModeloObjetos.Banca.Producto Producto = new ModeloObjetos.Banca.Producto();
//Extraemos la data de cada campo de acuerdo a lo especificado por el core
                                    Producto.sNumeroProducto = sRegistro.Substring(0, 20).Trim();
                                    Producto.sCodProducto = sRegistro.Substring(20, 6).Trim();
                                    Producto.sProductoNombre = sRegistro.Substring(26, 40).Trim();
                                    Producto.enumTipoProducto = Comun.ObtenerTipoProducto(Producto.sCodProducto);
                                    Producto.dSaldo1 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(66, 12)));
                                    Producto.dSaldo2 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(78, 12)));
                                    Producto.dSaldo3 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(90, 12)));
                                    if (Producto.enumTipoProducto == enumTipoProducto.DPF)
                                    {
                                        Producto.dtFecha = Comun.FormatearFechaStringCorta(sRegistro.Substring(106, 8));
                                        Producto.dSaldo4 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(114, 12)));
                                    }
                                    else if (Producto.enumTipoProducto == enumTipoProducto.TDC)
                                    {
                                        Producto.dSaldo4 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(102, 12)));
                                        Producto.dtFecha = Comun.FormatearFechaStringCorta(sRegistro.Substring(118, 8));
                                    }
                                    else
                                    {
                                        Producto.dSaldo4 = double.Parse(Comun.FormatearMontoIso(sRegistro.Substring(102, 12)));
                                    }
                                    ProductoDTO.Items.Add(Producto);
                                }
                                ProductoDTO.Items.Sort(ProductoDTO);
                                break;
                            default:
                                ProductoDTO.bExitoso = false;
                                ProductoDTO.sMensaje = RespuestaDTO.sMensaje;
                                break;
                        }
                    else
                    {
                        ProductoDTO.bExitoso = false;
                        ProductoDTO.sMensaje = "Error campo de respuesta (39) nulo";
                    }
                }
                else
                {
                    // El mensaje ha expirado
                    ProductoDTO.bExitoso = false;
                    ProductoDTO.sMensaje = "MensajeExpiro";
                }


Como comenté antes, lo interesante es el manejo de los puertos, que aunque en realidad es sencillo, tuvimos que darle muchas vueltas para asegurar la confiabilidad y estabilidad del servicio

La solución implementada se basa en utilizar un pool de puertos para comunicarnos con el core, este pool fue implementado como una cola:

static Queue CanalesComunicacion = new Queue();

De está cola, antes de enviar un mensaje el código agarra un puerto, verifica que esté bueno, se envía el mensaje y se devuelve el puerto a la cola.
Si el mensaje falla o expira, se considera que el puerto está malo y por ende se agrega el mismo a otra cola de puertos en cuarentena.

Cada cierto tiempo se intenta mandar un mensaje ISO 8583 de prueba para los puertos en cuarenta, si el mensaje es exitoso, se saca el puerto de cuarentena y se devuelve el mismo a la cola de puertos disponibles para los mensajes

        ///
        /// Metodo encargado del envío de mensasjes ISO8583
        ///
        ///
Mensaje ISO 8583 a ser enviado
        /// DTO con mensaje enviado y recibido asi como la info sobre el envío
        public ModeloObjetos.DTO.MensajeDTO EnviarMensaje(Iso8583Message Mensaje)
        {
            int campoConsecutivo = 11;
//se agrega el campo 11 que sirve como identificador unico del mensaje (es obligatorio)
            Mensaje.Fields.Add(campoConsecutivo, NuevoNumeroSecuencia.ToString());
            ModeloObjetos.DTO.MensajeDTO RespuestaDTO = new ModeloObjetos.DTO.MensajeDTO();
            Trx.Messaging.Iso8583.Iso8583Message Respuesta = null;
            int iNumIntentos = 0;
            ClientPeer Canal = null;
            int canalesDisponibles;
//se asegura la exclusividad mientras se revisa si hay puertos disponibles para el envío del mensaje
            lock (SyncRoot)
            {
                while (CanalesComunicacion.Count == 0 && iNumIntentos < _NumeroIntentosEsperaCanalISO)
                {
                    System.Threading.Thread.Sleep(_TiempoEsperaCanalDisponibleISO);
                    iNumIntentos++;
                }
                if ((canalesDisponibles = CanalesComunicacion.Count) > 0)
                {
                    iNumIntentos = 0;
                    while (iNumIntentos < _NumeroIntentosEsperaCanalISO)
                    {
                        for (int i = 0; i < canalesDisponibles; i++)
                        {
//al tener el canal disponible lo saco de la cola para usarlo
                            Canal = CanalesComunicacion.Dequeue();
                            if (Canal.IsConnected)
                            {
                                break;
                            }
                            else
                            {
                                CanalesComunicacion.Enqueue(Canal);
                            }
                        }
                        if (Canal.IsConnected)
                        {
                            break;
                        }
                        System.Threading.Thread.Sleep((int)System.Math.Pow(2, iNumIntentos) * 1000);
                        iNumIntentos++;
                    }
                }
            }
            bool canalQuebrado = false;
//cuando se determina que hay un canal disponible nos disponemos a usarlo
            if (canalesDisponibles > 0)
            {
                try
                {

                    if (Canal.IsConnected)
                    {
                        RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "MensajeAenviar:" + Mensaje.ToString()));
//Asigno el canal al request a enviar
                        PeerRequest request = new PeerRequest(Canal, Mensaje);
//Se envía el mensaje
                        request.Send();
//esperamos por la respuesta del mensaje ( de acuerdo al timeout establecido)
                        request.WaitResponse(_TimeoutRecepcionMensajeISO);
//Si el request no expiró, entonces está todo bien y guardamos la respuesta ISO en nuestro DTO de respuesta
                        if (!request.Expired)
                        {
                            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                                ServiciosISOServer.Comun.Logger.Debug("Respuesta: " + request.ResponseMessage.ReceivedData);
                            Respuesta = (Iso8583Message)request.ResponseMessage;
                            RespuestaDTO.bExitoso = true;
                            RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "RespuestaMensaje:" + Respuesta.ToString()));
                        }
                        else
                        {
//Mandamos un correo avisando que el puerto está malo (para que la gente del core lo revise)
                            RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "MensajeExpiro"));
                            string puerto = ((TcpChannel)Canal.Channel).Port.ToString();
                            string correo = ConfigurationManager.AppSettings["CorreoOperador"];
                            string sText = String.Format("Expiró un mensaje enviado al Servidor {0} Puerto: {1}", ((TcpChannel)Canal.Channel).HostName, puerto);
                            string sSubject = String.Format(ConfigurationManager.AppSettings["Subject"], puerto);
                            string sResultado = EnviarCorreo.EnvioCorreo(correo, sSubject, sText);
                            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                                ServiciosISOServer.Comun.Logger.Debug(sText + " enviado a " + correo + " Resultado: " + sResultado);
                            canalQuebrado = true;


                        }
                        //if(request.ResponseMessage != null)
                        //    RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO( false, "DataRecibida:" + request.ResponseMessage.ReceivedData));
                    }
                    else
                    {
                        RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "CanalNoConectado"));
                    }
                }
                finally
                {
                    lock (SyncRoot)
                    {
//Si el canal fue utilizado satisfactoriamente, lo agregamos de nuevo a la cola de puertos disponibles para envío de mensajes
                        if (canalQuebrado == false)
                        {
                            CanalesComunicacion.Enqueue(Canal);
                        }
                        else
                        {

                            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                            {
                                ServiciosISOServer.Comun.Logger.DebugFormat("El canal {0} ha sido movido a la cola de cuarentena", Canal.Name);
                            }
//Si el mensaje expiró, agregamos el puerto a la cola de cuarentena (para que no sea nuevamente usado para enviar mensajes de la app hasta que esté sano)
                            MonitorCuarentena.Current.AgregarObjetoCuarentena(Canal);
                        }
                    }
                }
            }
            else
            {
                RespuestaDTO.Trazas.Add(new ModeloObjetos.DTO.TrazaOperacionDTO(false, "CanalesNoDisponibles"));
            }
            RespuestaDTO.MensajeEnviado = Mensaje;
            RespuestaDTO.MensajeRecibido = (Respuesta != null ? Respuesta : new Iso8583Message());
            if (ServiciosISOServer.Comun.Logger.IsDebugEnabled)
                ServiciosISOServer.Comun.Logger.Debug("Retornando: " + RespuestaDTO.bExitoso);
            return RespuestaDTO;

        }


Otra cosa a tener en cuenta es que seguro tendrá que hacer cambios en la especificación del mensaje ISO8583 en la clase: Iso8583Ascii1987MessageFormatter, en esta clase
están definidos como debe ser cada uno de los campos de los mensajes ISO por ejemplo:

FieldFormatters.Add( new StringFieldFormatter(
                48, new VariableLengthManager(0, 9999, StringLengthEncoder.GetInstance(9999)),
                StringEncoder.GetInstance(), "Additional data - private" ) );

Aquí se indica que el campo 48 es tipo String de longitud variable y en este caso con una longitud maxima de 9999 caracteres

En este otro ejemplo el campo 42 es string pero con longitud fija de 15
 FieldFormatters.Add( new StringFieldFormatter(
                42, new FixedLengthManager( 15 ), StringEncoder.GetInstance(),
                "Card acceptor identification code" ) );

Estas definiciones son muy importantes ya que son las primeras que arrojan errores en las pruebas ya que cada server implementa a veces el estandar a su manera

Si está interesado en consultoría en comunicación ISO 8583 o necesita implementar una solución de este tipo, por favor no dude en contactarme.
Luego publicaré una aplicación de pruebas llamada ISO Tester que sirve para hacer pruebas puntuales de este tipo de mensajería

19 comentarios:

  1. Hola Omar Rodríguez esta interesante el codigo ,mira yo estoy desarrollando la aplicacion completa en vb.net 2008 empaquetado desempaquetado el socket y todo lo demas , me puedes colaborar en algo por favor en decime que elementos o posiciones llevan cada uno de los siguientes mensajes
    200,210,400,410,800,810 y cuales son las funciones del mensaje
    te estare agradesido
    Saludos..
    atte: David Rodriguez

    ResponderBorrar
  2. Buen dia Omar, estoy esperando que publique la aplicacion de iso tester, queria saber si puede mandarme una pagina donde encontrar informacion, sobre http://sourceforge.net/projects/trx-framework/, como utilizar la dll en una aplicacion web en visual studio 2005, buen dia y gracias.

    ResponderBorrar
  3. David, no entiendo la ayuda que necesitas, a que te refieres con los elementos o posiciones de esos números de mensajes que indicas. Dame por favor mas información y contexto para poder ayudarte

    ResponderBorrar
  4. Carlos, tratare de estar publicando mi IsoTester este fin de semana en este Blog

    ResponderBorrar
  5. Buen Dia omar.
    Me refiero al DataElemen que arma el mapa de Bit,cada mensaje indica en el mapa de bit
    los elementos que tiene el mensaje.. por ejemplo la siguiene cadena indica el mensaje 200 seguidos de dos mapas de bit .
    MTI MAPA 1 MAPA 2
    0200 B23A800128A180180000000014000000
    este mapa al desempaquetarlo trae los siguienetes dataelemen
    1,3,4,7,11,12,13,15,17,32,35,37,41,43,48,49,60,
    61,100,102
    bueno en lo que necesito ayuda es en saber que data element traen los mensajes 300,310,400,410,,800,810

    SALUDES
    Atte: David R.

    ResponderBorrar
  6. Acabo de publicar el IsoTester

    David con respecto al mapa de bit, de verdad no te podría ayudar sin investigar primero, te aconsejo que utilices un framework como el que coloco en el blog para que tu no tengas que preocuparte en armar el mapa de bits, el framework lo hace por ti.

    Asumiendo que cuando dices DataElements te refieres a los campos del mensaje, entonces te digo que debes solicitar la especificación de tu proveedor de mensajes ISO ya que aunque es un estandar, todo el mundo tiene su propia implementación

    ResponderBorrar
  7. Gracias Omar, si fijate que descargue el trx-framework que utilizas tu , tienes informacion o manual que me ayude a utilizar el trx-framework para manejar el envio y recepcion de mensajes.

    Saludes.

    ResponderBorrar
  8. He seguido tu consejo y me descargue el iso tester
    se ve muy bien tu iso tester por ahora lo estoy probando y cambiando varias rutas de archivos..
    necesito saber donde recibo ese mensaje que envio en el iso tester para probar bien la aplicacion (creo falta la parte servidor). quiero hacer la prueba de enviar un mensaje del iso tester y recibir una respuesta simulando una transaccion bancaria
    . ayudame Por Favor en eso omar

    Saludes
    ATTE : J. David Rodriguez

    ResponderBorrar
  9. Buen dia Omar, he bajado el Iso tester y si lo hice funcionar pero la verdad no se como fucniona, bueno no se quien recibe el mensaje, y al parecer David tiene el mismo problema segun lei el blog, si me pudiera ayudar con eso le agradeceria mucho y gracias por publicar el Iso Tester

    ResponderBorrar
  10. Les publico un server ISO de pruebas en http://iso8583tester.codeplex.com/releases/view/49475 es el mismo que provee la gente del Framework TRX

    ResponderBorrar
  11. descargue el ejemplo de: http://iso8583tester.codeplex.com/releases/view/49475

    y lo intento ejecutar en vs.net 2008, me salen varios errores de que no puede encontrar las referencias a:"name space trx could not be found" trx.xxxxx, ademas en las referencias no se pudo encontrar la referencia a: tcpipframework.

    ResponderBorrar
  12. Te agradezco mucho el post, eres todo un profesional. saludos

    ResponderBorrar
  13. Hola Omar. Ando buscando a alguien quien me pueda desarrollar todo el armado de la aplicación por medio de ISO8583 para una App en un HandHeld y meter proceso de pagos. Tengo el contacto con PROSA, ya solo requiero de una API que haga todo el proceso de pago y esta poderla integrar a nuestra App. Te dejo mi correo para ponernos en contacto y platicar sobre una propuesta. Salu2. Fernando Martínez G. fermaga@qsistemas.mx

    ResponderBorrar
  14. Hola Omar, estoy realizando el desarrollo de una aplicacion para Android en donde necesito implementar la norma ISO8583. Como pudieras ayudarme para dicha implementacion de codigo para cada campo de la norma, ya que soy un poco principiante en este cuento. Te agradesco mucho. Att. Sebastian.

    ResponderBorrar
  15. Hola Omar buen dia mi nombre es Eduardo me gustaria platicar contigo ya que estoy desarrollando una aplicacion. hypsyst@hotmail.com

    ResponderBorrar
  16. Saludos, a la trama ISO8583 que construye y a la vez se enviá, es posible asignar un campo TPDU con la librería que utiliza?.

    ResponderBorrar
  17. Hola amigos, estoy muy intersado en el manejo de puertos, quien me puede ayudar por favor. mi correo es jaaf2000@hotmail.com

    ResponderBorrar
  18. Hola podrías mostrar un ejemplo para decodificar la trama iso8583 con este framework gracias.

    ResponderBorrar
  19. Hola Omar, del core me esta llegando un campo nuevo y lo observo en el ISO el campo 54, pero me esta llegando truncado, se que debo definir , pero no se donde ni como. gracias.

    ResponderBorrar