lunes, 29 de octubre de 2018

Leyendo RSS desde SQL Server



En esta entrada quiero abordar una forma en la que podemos obtener información de un RSS e integrarla a una base de datos.

Es común que haya información que resulte relevante o interesante para la operación de una empresa y ésta se encuentre disponible a través de fuentes RSS, el cual es un formato bien definido a través de un esquema XML que podemos encontrar en RSS 2.0 at Harvard Law.

Elegí esa dirección para mostrar el esquema del documento porque siento que la documentación ahí mostrada está ordenada de una forma muy sencilla de entender.

Para implementar una solución que nos permita obtener fuentes RSS necesitamos de un módulo que lea la información de la URL donde está ubicado el XML y posteriormente se lo dé a SQL Server para que tome el XML y lo convierta a una representación entidad-relación que es mucho más manejable.

Este proyecto lo dividiremos en los siguientes pasos:

  1. Tomar una fuente RSS como muestra.
  2. Crear un módulo en .NET que pueda ser integrado a SQL Server.
  3. Crear un assembly en SQL Server y crear la función que devuelva la información contenida en la URL que reciba como parámetro.

URL de muestra

Tomaré como base el RSS de deportes del sitio web de ESPN, si abrimos la liga nos mostrará un XML con el contenido de la fuente.

Módulo de .NET

Voy a crear un proyecto del tipo Biblioteca de Clases en Visual Studio 2017 y lo llamaré SQLRSSReader, en él crearemos un par de clases:

  1. RSSItem para representar cada entrada de la fuente RSS.
  2. Lector para implementar la función que SQL Server llamará.

A continuación el código de RSSItem:

namespace SQLRSSReader
{
    public class RSSItem
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public string Link { get; set; }
        public string PubDate { get; set; }
    }
}

Para exponer una función tabular a SQL Server, es necesario que se den las siguientes condiciones:

  1. Tener el atributo SqlFunctionAttribute.
  2. Pública y estática.
  3. Devolver IEnumerable.
  4. Los tipos de parámetros de entrada deben ser de SQL Server.

La función quedaría de la siguiente manera:

[SqlFunction(FillRowMethodName = "LlenarItem")]
public static IEnumerable ObtenerRSS(SqlString url)
{
    HttpWebRequest webRequest = HttpWebRequest.Create(url.Value) as HttpWebRequest;
    HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse;
    List<RSSItem> result = null;

    using (StreamReader sr = new StreamReader(webResponse.GetResponseStream()))
    {
        XmlDocument xdRSS = new XmlDocument();
        xdRSS.LoadXml(sr.ReadToEnd());
        result = new List<RSSItem>();
        foreach (XmlNode xnItem in xdRSS.SelectNodes("/rss/channel/item"))
        {
            result.Add(new RSSItem()
            {
                Description = (xnItem.SelectSingleNode("./description") != null ? xnItem.SelectSingleNode("./description").InnerText : null),
                Link = (xnItem.SelectSingleNode("./link") != null ? xnItem.SelectSingleNode("./link").InnerText : null),
                PubDate = (xnItem.SelectSingleNode("./pubDate") != null ? xnItem.SelectSingleNode("./pubDate").InnerText : null),
                Title = (xnItem.SelectSingleNode("./title") != null ? xnItem.SelectSingleNode("./title").InnerText : null)
            });
        }
    }

    return result;
}

Lo que hace nuestra función es descargar el RSS, llenar una lista de objetos RSSItem y devolverla como resultado.

Notarás que el atributo SqlFunction tiene un parámetro llamado FillRowMethodName, en él se coloca el nombre de la función que se mandará a llamar para llenar la tabla resultante.    Por cada objeto que contenga IEnumerable se llamará a la función a la que apunta FillRowMethodName.

Ahora veamos la función LlenarItem:

public static void LlenarItem(object rssItemObject, out SqlString Title, out SqlString Description, out SqlString Link, out SqlString PubDate)
{
    RSSItem rssItem = rssItemObject as RSSItem;
    if (rssItem.Title != null)
        Title = new SqlString(rssItem.Title);
    else
        Title = SqlString.Null;
    if (rssItem.Description != null)
        Description = new SqlString(rssItem.Description);
    else
        Description = SqlString.Null;
    if (rssItem.Link != null)
        Link = new SqlString(rssItem.Link);
    else
        Link = SqlString.Null;
    if (rssItem.PubDate != null)
        PubDate = new SqlString(rssItem.PubDate);
    else
        PubDate = SqlString.Null;
}

Vamos a analizar los parámetros uno por uno:

  1. rssItemObject.   Es donde recibiremos la instancia de RSSItem que se quiere operar en ese momento.
  2. Title.    Primera columna de la tabla resultante.
  3. Description.    Segunda columna de la tabla resultante.
  4. Link.    Tercera columna de la tabla resultante.
  5. PubDate.    Cuarta columna de la tabla resultante.

También debemos notar que cada una de las columnas tiene "out", eso nos obliga a darle un valor (aunque sea null) antes de terminar la ejecución de la función.

Compilemos nuestro proyecto para generar el DLL correspondiente.

Función en SQL Server

Para probar nuestra función vamos a crear una base de datos llamada SQLBlog y le habilitaremos la opción TrustWorthy para que se le considere una base de datos de confianza.

USE master
GO
IF EXISTS (SELECT name FROM sys.databases WHERE name = N'SQLBlog')
BEGIN
ALTER DATABASE SQLBlog
    SET SINGLE_USER
    WITH ROLLBACK IMMEDIATE;
    DROP DATABASE SQLBlog
END
GO
CREATE DATABASE SQLBlog
ON PRIMARY (
    NAME = 'SQLBlog_dat',
    FILENAME = 'C:\LaCarpeta\SQLBlog.mdf'
)
LOG ON (
    NAME = 'SQLBlog_log',
    FILENAME = 'C:\LaCarpeta\SQLBlog.ldf'
)
GO
ALTER DATABASE SQLBlog
SET TRUSTWORTHY ON

Ahora vamos a crear el ensamblado que alojará el DLL que acabamos de crear en .NET:

USE SQLBlog
GO
CREATE ASSEMBLY SQLRSSReader
FROM 'C:\ElEnsamblado\SQLRSSReader.dll'
WITH PERMISSION_SET = UNSAFE

Finalmente creamos la función y la probamos:

CREATE FUNCTION udfGetRSS(
    @URL NVARCHAR(256)
)
RETURNS TABLE(
    Title NVARCHAR(200),
    Description NVARCHAR(4000),
    Link NVARCHAR(256),
    PubDate NVARCHAR(50)
)
AS EXTERNAL NAME SQLRSSReader.[SQLRSSReader.Lector].ObtenerRSS
GO
SELECT * FROM udfGetRSS(N'http://www.espn.com/espn/rss/news')

Antes de terminar con esta entrada quiero que notes lo siguiente:

  1. El ensamblado debe ser creado con PERMISSION_SET en UNSAFE porque va a realizar lectura de sitios web, es decir que necesita salir del servidor.
  2. Las cadenas expuestas por la función deben ser del tipo UNICODE, por ello notarás que son NVARCHAR.
  3. La notación para apuntar a la función es la siguiente: AssemblyName.[Namespace.Class].Function

Prueba con otra dirección, por ejemplo: https://www.tendencias21.net/xml/syndication.rss

Espero te haya resultado de utilidad esta entrada, ¡saludos!



jueves, 25 de octubre de 2018

Vistas, funciones y procedimientos almacenados



¿Para qué utilizar vistas, funciones y procedimientos almacenados?, ¿solamente para que se vea bonita la base de datos con muchos objetos de todos los tipos posibles?

Tenemos dos objetivos principales cuando ocupamos estos objetos en nuestra base de datos:

  1. Encapsular la forma en que se obtienen los datos de la base de datos.
  2. Implementar una capa de seguridad por encima del acceso a los datos.

Encapsulamiento

Para atacar este tema lo voy a dividir en dos partes, en "Vistas y funciones" y en "Procedimientos almacenados".

Funciones y vistas

No sé si te haya tocado trabajar con la base de datos de algún ERP o con la base de datos de un sistema a cuyo programador le resultó divertido ofuscar los nombres de los objetos.

La irremediable consecuencia de lo anterior es que la extracción de datos se vuelve un dolor de cabeza debido a que los nombres de los objetos no es descriptivo por sí mismo.

Supongamos que tenemos una tabla de este estilo:

CREATE TABLE XT56(
    A INT IDENTITY(1, 1),
    B VARCHAR(80) NOT NULL,
    C DECIMAL(8, 2) NOT NULL,
    D BIT NOT NULL DEFAULT 1,
    CONSTRAINT pkXT56 PRIMARY KEY (Id)
)

Resulta muy complejo establecer para qué es la tabla XT56 y el objetivo funcional de la información que está guardando.    Si después de un trabajo de revisión del sistema y de la base de datos determinamos que es la tabla de productos, bien podríamos hacer lo siguiente para clarificar la extracción de los productos:

CREATE VIEW vProductos
AS
    SELECT A AS Id, B AS Nombre, C AS Precio, D AS Activo
    FROM XT56

Al utilizar esta vista podremos obtener la información de una forma mucho más clara y entendible.

¿Qué pasaría si frecuentemente estamos obteniendo la lista de productos activos cuyo precio sea mayor o igual a un valor X?    Podemos encapsular el código para clarificar la forma en que devuelve la información y también para reutilizar un algoritmo que ya resolvió un problema en específico:

CREATE FUNCTION udfDameProductosPorPrecio(
    @Precio DECIMAL(8, 2)
)
RETURNS TABLE
AS
    RETURN
    SELECT A AS Id, B AS Nombre, C AS Precio
    FROM XT56
    WHERE C = @Precio
    AND D = 1

A través del encapsulamiento podemos proyectar la información de la forma en la que lo necesitamos y también crear objetos que nos permitan reutilizar algo que ya fue resuelto previamente.

Procedimientos almacenados

En un grupo de programación en T-SQL siempre habrá alguien que tenga una forma óptima de codificar y que resuelva los problemas utilizando la menor cantidad de recursos posibles.

Una excelente idea es que esta persona se encargue de resolver los problemas operativos (en la base datos) más complejos y encapsule el algoritmo dentro de un procedimiento almacenado, de manera tal que éste pueda ser utilizado por otros sin tener que estarse quebrando la cabeza en cómo resolver un problema.

Planes de ejecución

Otra gran ventaja de los objetos parametrizados (funciones y procedimientos almacenados) es que permiten que se almacene el plan de ejecución de éstos y así no tenga que recalcularse la siguiente ocasión que se requiera su utilización.

El caché de planes de ejecución es una cosa muy valiosa de SQL Server debido a que reduce el trabajo de uno de los procesos más caros, el cálculo del plan de ejecución.

Capa de seguridad

Habitualmente no es buena idea que se tenga acceso directo de lectura y/o escritura a las tablas de una base de datos.   En este momento se me ocurren por lo menos las siguientes X razones:

  1. El usuario debe poder leer datos de una tabla, pero ella contiene información confidencial a la que no debería tener acceso el usuario.    Es decir, solamente necesita acceso a un subconjunto de columnas.
  2. El usuario debe poder insertar datos pero necesitamos estar absolutamente seguros que la información que ingrese sea consistente con la especificación de requerimientos.
  3. Si el usuario tiene permiso de escritura sobre la tabla, entonces podrá eliminar información y hacerlo de forma equivocada.

Seguramente habrá más de uno que inmediatamente pensó "pero si le puedo dar permisos muy granulares a un usuario de manera tal que solamente pueda leer o escribir ciertas columnas".    Coincido totalmente en que se podría, pero la administración de la seguridad se haría un dolor de cabeza y el dar esos permisos granulares no nos asegura que la información insertada sea adecuada.

Supongamos que tenemos una tabla como la siguiente:

CREATE TABLE Productos(
    Id INT IDENTITY(1, 1),
    Nombre VARCHAR(80) NOT NULL,
    Precio DECIMAL(8, 2) NOT NULL,
    Activo BIT NOT NULL DEFAULT 1,
    CONSTRAINT pkProductos PRIMARY KEY (Id)
)

Si un usuario únicamente tuviera permitido actualizar precios, podríamos crearle un procedimiento como el siguiente:

CREATE PROC pActualizaPrecioProducto(
    @Id INT,
    @Precio DECIMAL(8, 2)
)
AS
BEGIN
    UPDATE Productos
    SET Precio = @Precio
    WHERE Id = @Id
END

Vamos a mejorar este tema, vamos a crear una tabla de histórico de precios guardando auditoría de quién lo ejecutó:

CREATE TABLE ProductosHistoricoPrecio(
    Id INT IDENTITY(1, 1)
    Id_Producto INT NOT NULL,
    PrecioAnterior DECIMAL(8, 2),
    PrecioNuevo DECIMAL(8, 2),
    Usuario SYSNAME,
    Fecha DATETIME2(0) NOT NULL DEFAULT GETDATE(),
    CONSTRAINT pkProductosHistoricoPrecio PRIMARY KEY (Id),
    CONSTRAINT fkProductosHistoricoPrecio_Productos FOREIGN KEY (Id_Producto) REFERENCES Productos (Id)
)

Y nuestro procedimiento almacenado quedaría de la siguiente manera:

CREATE PROC pActualizaPrecioProducto(
    @Id INT,
    @Precio DECIMAL(8, 2)
)
AS
BEGIN
    BEGIN TRAN
    BEGIN TRY
        INSERT INTO HistoricoProductosPrecio (Id_Producto, PrecioAnterior, PrecioNuevo, Usuario)
        SELECT Id, Precio, @Precio, SUSER_NAME()
        FROM Productos
        WHERE Id = @Id

        UPDATE Productos
        SET Precio = @Precio
        WHERE Id = @Id

        COMMIT
    END TRY
    BEGIN CATCH
        ROLLBACK

        DECLARE @Error VARCHAR(4000) = ERROR_MESSAGE()
        THROW 50001, @Error, 1
    END CATCH
END

Si a nuestro usuario le damos permiso de ejecución sobre el procedimiento almacenado:

  1. No será necesario que tenga permiso de escritura sobre HistoricoProductosPrecio ni sobre Productos.
  2. No es necesario que sepa que se está guardando un histórico de precios, tarea que podría olvidar realizar si él actualizara directamente el precio del producto.

Espero te haya resultado de utilidad esta entrada, ¡saludos!



miércoles, 24 de octubre de 2018

optimize for ad hoc workloads



Ya tiene mucho tiempo que no me metía a publicar alguna entrada en mi blog. Hay muchas razones para explicarlo, pero intentaré tener ventanas de tiempo que me permitan seguir creando más entradas y compartir un poco de lo que he podido aprender a lo largo de estos años.

Durante los proyectos de consultoría en SQL Server en los que he estado trabajando de un tiempo para acá, me he encontrado algo muy interesante, que la memoria de SQL está sumamente ocupada almacenando planes de ejecución.

Antes de abordar el tema de este blog de forma directa, me gustaría platicar un poco de los planes de ejecución para que se entienda de mejor manera el impacto que tendrá la opción optimize for ad hoc workloads de SQL Server.

Cuando mandas a ejecutar un query, SQL server realiza una revisión sintáctica y semántica del mismo, una vez que ha pasado la validación procede a calcular un plan de ejecución que resulte óptimo para el query en cuestión.    Para este cálculo toma en cuenta estadísticas, índices, llaves, cantidad de filas, tipos de dato, etc.

El proceso del cálculo de plan de ejecución es de lo más pesado y costoso para SQL Server, realmente mucha parte de la magia de SQL se encuentra ahí.    Debido a este costo SQL Server guarda en memoria el plan de ejecución para que pueda ser reutilizado por un query posterior, es decir que se ahorrará el proceso de cálculo y simplemente se avocará a la ejecución del mismo, optimizando el uso de recursos y mejorando los tiempos de respuesta.

Hasta aquí todo pinta re bien, el problema es cuando nuestra memoria se llena de planes de ejecución de uso único, es decir que el plan de ejecución únicamente ha sido utilizado una vez y nunca volvió a ser reutilizado por otro query.

Para ejemplificar esto vamos a hacer un ejercicio sobre la base de datos AdventureWorks2012 (pueden elegir otra versión de AdventureWorks y no habrá mayor problema)

Es indispensable que no realices este ejercicio en un servidor productivo.

Lo primero será vaciar la memoria de planes de ejecución y el buffer de datos para que tengamos el espacio en blanco:

DBCC FREEPROCCACHE
DBCC DROPCLEANBUFFERS

Después nos conectaremos a la base de datos AdventureWorks2012 y ejecutaremos primero este query:

SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 321

Y después este otro query:

SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 328

Recordemos que vaciamos la memoria de planes de ejecución y por lo tanto SQL Server tuvo que calcular el mejor plan de ejecución (de los que encontró)    Vamos a revisar la memoria de nuestros planes de ejecución para ver qué tiene:

SELECT CP.usecounts, CP.size_in_bytes, CP.cacheobjtype, CP.objtype, QP.query_plan
FROM sys.dm_exec_cached_plans CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) QP

Nos deberá dar un resultado como el que muestro en la siguiente imagen:

Podemos ver que tenemos tres planes de ejecución que han sido ocupados una sola vez.    Abre cada uno de los planes de ejecución y notarás que:

  • Uno corresponde al query que buscó al producto 321.
  • Uno corresponde al query que buscó al producto 328
  • Uno corresponde al query que obtuvo la información de los planes de ejecución.

Si ejecutamos ahora el siguiente query:

SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 329

Y volvemos a obtener el contenido del caché de planes de ejecución, ahora veremos que se incrementó el usecounts del plan de ejecución que obtiene el contenido del caché de planes de ejecución y que tenemos un nuevo plan almacenado resultado de la búsqueda del producto 329.

¿Estamos de acuerdo que pudo haberse utilizado el mismo plan de ejecución para buscar los productos 321, 328 y 329?, finalmente la búsqueda se hizo en base al CLUSTERED INDEX.    Peor aún, si nos pusiéramos a buscar muchos productos por su identificador acabaríamos con muchos planes de ejecución idénticos en la memoria, desperdiciando recursos que podríamos ocupar en cosas más valiosas.

Aquí es donde entra la configuración del optimize for ad hoc workloads.    Esta opción de SQL Server busca parametrizar planes de ejecución para que puedan ser reutilizados varias veces y no terminar con una memoria atascada de basura.

Para probar esto, vamos a habilitar la opción optimize for ad hoc workloads de la siguiente manera:

sp_configure 'show advanced options', 1
GO
RECONFIGURE
GO
sp_configure 'optimize for ad hoc workloads', 1
GO
RECONFIGURE

Ahora vamos a limpiar la memoria:

DBCC FREEPROCCACHE
DBCC DROPCLEANBUFFERS

Acto seguido ejecutaremos las cosas en el mismo orden que lo hicimos antes de habilitar la opción optimize for ad hoc workloads:

SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 321

Después:

SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 328

Después:

SELECT CP.usecounts, CP.size_in_bytes, CP.cacheobjtype, CP.objtype, QP.query_plan
FROM sys.dm_exec_cached_plans CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) QP

Después:

SELECT ProductID, ListPrice, Color FROM Production.Product WHERE ProductID = 329

Y finalmente:

SELECT CP.usecounts, CP.size_in_bytes, CP.cacheobjtype, CP.objtype, QP.query_plan
FROM sys.dm_exec_cached_plans CP
CROSS APPLY sys.dm_exec_query_plan(CP.plan_handle) QP

Si revisamos y comparamos la memoria utilizada del antes y el después de habilitar la opción optimize for ad hoc workloads notaremos un ahorro ¡del 39.68%!

Esto resulta especialmente útil en entornos donde SQL Server recibe solicitudes enviadas desde sistemas que utilizan el .NET Entity Framework.

¡Aguas!, no es la cura para todos los males, esto nos obligará a realizar las mejores prácticas y crear una capa de acceso a datos mediante vistas, procedimientos almacenados y funciones para obtener la información de nuestras bases de datos.    Aunque, siendo sincero, la opción de la capa de datos la considero obligatoria independientemente del estado de la opción optimize for ad hoc workloads.

De hecho es considerada una buena práctica el habilitar esta opción en toda instalación de SQL Server.

Espero te haya resultado de utilidad esta entrada, ¡saludos!



jueves, 1 de febrero de 2018

¿Qué PAC debo elegir?



Desde la entrada de la facturación electrónica a México, ha ido creciendo el número de proveedores certificados que ofrecen sus servicios para poder realizar el proceso de timbrado y/o cancelación de tus comprobantes fiscales.

La elección del proveedor te puede significar una enorme diferencia entre detener la operación de embarques de tu empresa y tener fluidez en dicha tarea, entre tener dolores de cabeza interminables y pagar un poco más pero tener tranquilidad.

Lo más probable es que te inclines por buscar a uno económico, que por la cantidad de timbres que quieras comprar te puedan dar un precio bajo y así te ahorres una buena lana. Antes de elegir un proveedor, debes preguntarte lo siguiente:
  1. ¿Puedo soportar todo un fin de semana sin respuesta de soporte?
  2. ¿Estoy dispuesto a modificar los precios de mis productos para que el proveedor me timbre mi comprobante?
  3. ¿Significaría un problema para la operación que no se pueda facturar?

La facturación no siempre significa un proceso restrictivo en las empresas, algunas pueden esperar horas o días para poder emitir o cancelar su factura y otras dependen de la emisión para poder continuar con su operación.

Es importante señalar que he colaborado en muchos sistemas diferentes que están relacionados con la facturación electrónica, desde procesamiento de archivos planos, lectura de bases de datos del ERP, obtención de los datos desde Web Services, recepción de datos por Webhooks, captura en sistemas, etc.   Me ha tocado convivir (y sufrir) a algunos PAC, de los cuales en esta entrada platicaré de 4 en particular: Edicom, SolucionFactible.com, Expide tu Factura y Profact.

¿Por dónde comenzamos?, ¿por las buenas noticias o por las malas?   Para no terminar con un mal sabor de boca esta entrada, comenzaremos con los peores y terminaremos con el mejor (desde mi punto de vista)

Expide tu factura
El compromiso en este PAC es inexistente y su personal de soporte se ve desbordado por la cantidad de problemas que tienen (o tenían hasta que tuve la lamentable experiencia de utilizar sus servicios), lo cual se traduce en respuestas al vuelo y promesas que escribieron en el hielo.

El año pasado se había comenzado con la implementación del CFDI v3.3 con meses de anticipación, esto con el fin de evitar las sorpresas que se pudieran recibir durante las validaciones ejecutadas por el PAC.

En agosto de 2017 se había obtenido el dato de que su servicio web de timbrado quedaría en productivo durante la primera semana de septiembre. En cada comunicación que se tenía con ellos al llegar la fecha comprometida, lo único que se recibía era un "el departamento de sistemas está trabajando duro", al final tiraron la toalla y el compromiso ya fue imposible de obtener (porque no lo iban a cumplir)

Llegó el Día D, ¡y qué crees!, lo que ya sabíamos que iba a pasar pero nos negábamos a creer, ¡no servía el servicio web!, ya en fechas de productivo obligatorias por el SAT, su servicio seguía inundado de problemas y el personal de soporte estaba que se arrancaba las pestañas de la cantidad de trabajo que les cayó encima.

¿Los utilizaría?, no, en definitiva perdieron el rumbo desde la versión 3.3 del CFDI. Antes estaban bien, funcionaba en niveles muy aceptables pero se vinieron abajo con la nueva versión.

Profact
Este proveedor se compromete a tener el servicio activo el 99.9% del tiempo, te comentan que utilizarán 8 horas al mes para mantenimiento y éstas no deberán considerarse dentro del 99.9%, es decir que 96 horas al año no estarán disponibles por cuestiones de mantenimiento.

Por el mantenimiento no le veo mayor problema, todo sistema necesita mantenimiento y lo más probable es que tenga que estar fuera de línea para recibirlo.   Tomando en cuenta esto, el 99.9% se deberá calcular sobre las 8664 horas restantes del año.

Con la caída que tuvieron apenas ya andan en 99.9569%, lo cual los deja con poco margen para lo que resta del año, pero no se les puede reclamar aún porque siguen dentro del parámetro anunciado. Es curioso que en su SLA le hacen pensar al usuario que lograrán un 99.9999%, pero con inteligencia únicamente se comprometen al 99.9%

Tienen errores en su codificación, errores que no reconocen aunque se les demuestre que sí existen y que son latentes... así que si eres programador y te gira un poco el coco, te causarán muchos disgustos estos personajes (si es que encuentras los errores)

El personal con el que se tuvo intercambio de información demostró un conocimiento bastante cortito, tan corto que no sabían lo que es el timbrado del Sector Primario, se quedaron con la mente en blanco y no supieron qué contestar.

¿Los utilizaría?, solamente si la facturación fuera algo casi sin importancia en la operación y vendiera en precios que tengan una posición decimal como máximo.

SolucionFactible.com
Tiene un nivel de servicio muy aceptable, al menos en el tiempo que se ha estado utilizando. Su personal de soporte atiende las solicitudes de información y se esfuerzan en apoyarte en todo lo posible.

Timbran todos los complementos y también el sector primario, cuentan con librerías para todos los lenguajes o conectividad por Web Service si quieres establecer la comunicación por esa vía.

¿Por qué no lo coloco como el mejor?, porque el que le sigue solamente me ha tocado verlo caer una vez en los años que llevo en esto.

¿Lo utilizaría y lo recomendaría?, seguro que sí, la experiencia ha sido muy grata.

Edicom
Como lo he mencionado antes, solamente me ha tocado verlo caer una vez en años. La caída fue por un error en sus bases de datos, error que se corrigió después de unas horas, pero cumplieron con su SLA al final del año.

Hay poco qué decir de este proveedor, porque casi todo es positivo. La única mosca en el arroz es que el soporte lo prefieren telefónico, y hay algunas personas que preferimos la comunicación vía correo elctrónico.

¿Lo utilizaría y lo recomendaría?, ¡por supuesto!, ciertamente tendrá un costo más elevado, pero si tu operación está muy ligada a tu proceso de facturación, este es el proveedor por excelencia.

Comentarios finales
Es importante notar que cada quién habla de cómo le fue en la fiesta, estos comentarios son expuestos a partir de la experiencia que he tenido con cada uno de ellos. Es muy probable que tú no tengas la misma percepción de alguno de ellos si es que ya te tocó que te causaran un problema.

La persona que se dedica al desarrollo sabe que todos los sistemas fallan, lo importante es que:

  1. El impacto sea tan mínimo como sea posible.
  2. El tiempo de respuesta para solucionarlo sea reducido.
  3. Se acepten los errores y se corrijan.
Espero te haya resultado de utilidad esta entrada, ¡saludos!