Autor: Francisco

  • Formateo de números en SQL Server: FORMAT(), cultura y decimales

    Un error común al migrar lógica de formato desde otros lenguajes: intentar usar una máscara personalizada tipo ###,###,###.## dentro de la función FORMAT() de SQL Server. Esa sintaxis es válida en Excel, VBA, o C# con formato personalizado — pero SQL Server no la acepta, porque su función FORMAT() solo admite formatos estándar de .NET, no patrones de máscara personalizados con símbolos #.

    Formato que NO funciona

    FORMAT(valor, '###,###,###.##')   -- Error / resultado incorrecto en SQL Server

    Formatos válidos en SQL Server

    SQL Server usa los formatos estándar numéricos de .NET, combinados opcionalmente con un parámetro de cultura (idioma/región) que determina el separador de miles y decimales:

    -- Miles con coma, decimales con punto (formato en-US)
    FORMAT(valor, 'N2', 'en-US')
    
    -- Miles con punto, decimales con coma (formato es-ES)
    FORMAT(valor, 'N2', 'es-ES')

    El N indica formato numérico estándar, y el 2 especifica la cantidad de decimales a mostrar (se puede usar cualquier número, N0 para sin decimales, N4 para cuatro decimales, etc.). El tercer parámetro (la cultura) es el que realmente decide cómo se ven los separadores — sin especificarlo, FORMAT() usa la configuración regional por defecto del servidor, lo cual puede variar entre entornos y generar inconsistencias si no se fija explícitamente.

    CulturaEjemplo con FORMAT(1234567.5, ‘N2’, cultura)
    en-US1,234,567.50
    es-ES1.234.567,50
    es-EC1.234.567,50

    Consulta de ejemplo

    Aplicando el formato correcto a una agregación con SUM() y agrupación:

    SELECT
        codigo_vendedor,
        canal,
        FORMAT(SUM(monto_comision), 'N2', 'en-US') AS Total
    FROM comisiones_preliminar c
    WHERE c.fecha_registro >= '2026-01-01'
      AND (
            (codigo_vendedor IN ('1001','1002','1003') AND canal = 'P')
         OR (codigo_vendedor IN ('2001','2002') AND canal = 'N')
         OR (codigo_vendedor IN ('3001','3002','3003') AND canal = 'I')
          )
    GROUP BY codigo_vendedor, canal
    ORDER BY codigo_vendedor, canal;

    Cuidado: FORMAT() devuelve texto, no un número

    Un detalle importante que suele pasarse por alto: FORMAT() siempre retorna un valor de tipo NVARCHAR, no numérico. Esto significa que el resultado ya no se puede usar directamente en cálculos posteriores, comparaciones numéricas, ni ordenarse correctamente como número (un ORDER BY sobre esa columna ordenaría alfabéticamente, no numéricamente). FORMAT() debe reservarse para la capa de presentación final de un reporte, nunca para columnas intermedias que aún necesiten procesamiento numérico.

    Alternativa: construir un formato manual tipo ###,###.##

    Si se necesita replicar exactamente el comportamiento de una máscara personalizada (por ejemplo, para igualar un formato heredado de otro sistema), se puede construir manualmente combinando CONVERT con estilo de formato de moneda y limpieza de símbolos:

    SELECT REPLACE(CONVERT(VARCHAR, CAST(1234567.5 AS MONEY), 1), '.00', '.50') AS formato_manual

    En la práctica, es más simple y mantenible usar FORMAT(valor, 'N2', 'en-US') y, si el separador de miles debe ser distinto al que da esa cultura, hacer el reemplazo de caracteres directamente sobre el resultado con REPLACE():

    -- Ejemplo: forzar separador de miles con punto y decimales con coma, manualmente
    SELECT REPLACE(REPLACE(FORMAT(1234567.50, 'N2', 'en-US'), ',', '|'), '.', ',')
    

  • Diagnóstico y configuración de un Cloudflare Tunnel paso a paso

    Cloudflare Tunnel (cloudflared) permite exponer un servicio interno a internet sin abrir puertos en el router ni depender de una IP pública — el túnel establece una conexión saliente desde el servidor hacia la red de Cloudflare, y Cloudflare enruta el tráfico entrante hacia esa conexión. Este es un caso real de diagnóstico cuando el servicio falla al arrancar.

    Problema inicial

    El servicio cloudflared fallaba continuamente con este error:

    Tunnel credentials file '/root/.cloudflared/<tunnel-uuid>.json' doesn't exist or is not a file

    Causa

    El archivo de configuración /etc/cloudflared/config.yml tenía placeholders (texto de ejemplo) en lugar del UUID real del túnel — un error común al copiar una configuración de plantilla sin reemplazar los valores genéricos por los datos reales generados al crear el túnel.

    ¿Qué es cada pieza de la configuración?

    • UUID del túnel — identificador único que Cloudflare asigna al crear el túnel (cloudflared tunnel create nombre). Es lo que conecta la configuración local con el túnel registrado en la cuenta de Cloudflare.
    • Archivo de credenciales (.json) — se genera automáticamente en el mismo momento que el UUID, y contiene la clave privada que autentica al servidor ante Cloudflare como el dueño legítimo de ese túnel específico.
    • Ingress rules — las reglas que determinan a qué servicio local se debe redirigir cada hostname que llega a través del túnel.

    Solución aplicada

    1. Editar el archivo de configuración

    vi /etc/cloudflared/config.yml

    Contenido correcto (con el UUID real reemplazando el placeholder):

    tunnel: TUNNEL-UUID-REAL
    credentials-file: /root/.cloudflared/TUNNEL-UUID-REAL.json
    ingress:
      - hostname: subdominio.tudominio.com
        service: http://localhost:8443
      - service: http_status:404

    La última línea (service: http_status:404) es obligatoria en cualquier configuración de ingress: actúa como regla de respaldo, devolviendo un 404 a cualquier hostname que llegue por el túnel y no coincida con ninguna regla anterior — sin ella, cloudflared no arranca.

    2. Verificar permisos del archivo de credenciales

    chmod 600 /root/.cloudflared/TUNNEL-UUID-REAL.json

    El archivo de credenciales contiene una clave privada — permisos 600 (solo lectura/escritura para el propietario) son necesarios tanto por seguridad como porque algunas versiones de cloudflared se niegan a cargar el archivo si los permisos son demasiado permisivos.

    3. Reiniciar el servicio

    systemctl restart cloudflared
    systemctl status cloudflared

    Configuración DNS en Cloudflare

    Además de la configuración local, el túnel necesita un registro DNS que apunte hacia él. Se crea como un CNAME apuntando al dominio especial de Cloudflare Tunnels (cfargotunnel.com), nunca a una IP directa:

    CampoValor
    TypeCNAME
    Namesubdominio
    TargetTUNNEL-UUID-REAL.cfargotunnel.com
    Proxy statusProxied

    El estado Proxied (la nube naranja en el panel de Cloudflare) es lo que hace que el tráfico pase por la red de Cloudflare y llegue al túnel — si se deja en modo «DNS only», el CNAME resuelve a la URL de cfargotunnel.com directamente, lo cual no funciona como se espera para el usuario final.

    Comandos de verificación

    Estado del túnel

    systemctl status cloudflared
    journalctl -u cloudflared -f
    cloudflared tunnel info TUNNEL-UUID-REAL
    cloudflared tunnel list

    Probar conectividad desde el servidor

    # Servicio local, sin pasar por el túnel
    curl -I http://localhost:8443
    
    # A través del túnel, desde afuera
    curl -I https://subdominio.tudominio.com
    curl https://subdominio.tudominio.com

    Probar primero localhost aísla el problema: si el servicio local no responde, el problema es de la aplicación en sí, no del túnel. Si localhost responde pero el dominio público no, el problema está en la configuración del túnel o del DNS.

    Verificar DNS

    # Consulta general, usando el DNS configurado en el sistema
    nslookup subdominio.tudominio.com
    
    # Consulta directa a los nameservers de Cloudflare (evita cachés intermedias)
    dig @ns1.cloudflare.com subdominio.tudominio.com
    nslookup subdominio.tudominio.com ns1.cloudflare.com
    
    # Limpiar la caché DNS local (Linux con systemd-resolved)
    sudo systemd-resolve --flush-caches
    sudo systemd-resolve --statistics

    Consultar directamente a los nameservers de Cloudflare, en vez del resolver local, descarta que el problema sea una caché DNS local desactualizada mostrando un resultado viejo.

    Probar desde Windows

    # PowerShell nativo
    Invoke-WebRequest -Uri https://subdominio.tudominio.com
    
    # curl real de Windows (no el alias de PowerShell)
    curl.exe -I https://subdominio.tudominio.com
    
    # Test de conectividad a nivel de puerto TCP
    Test-NetConnection -ComputerName subdominio.tudominio.com -Port 443
    # Desde CMD
    curl -I https://subdominio.tudominio.com

    Resultado

    • Túnel configurado y funcionando correctamente.
    • Conexiones establecidas con los edge servers de Cloudflare más cercanos.
    • DNS configurado correctamente, resolviendo hacia el túnel.

    Nota importante: las VPNs corporativas pueden bloquear el acceso a dominios detrás de Cloudflare Tunnel, dependiendo de las políticas de firewall/proxy de esa red. Si un usuario reporta que no puede acceder estando conectado a la VPN de su empresa, conviene probar primero desde una conexión sin VPN (o datos móviles) antes de asumir que el problema es del túnel.

    Archivos involucrados

    • Config: /etc/cloudflared/config.yml
    • Credenciales: /root/.cloudflared/TUNNEL-UUID-REAL.json
    • Certificado: /root/.cloudflared/cert.pem
    • Servicio: /etc/systemd/system/cloudflared.service