Saltar al contenido principal
Experimento · Arquitectura

Connect Analyzer

Un dashboard de ventas en .NET 10 y Next.js construido como experimento de craftsmanship: la fuente de datos vive tras un puerto, así que pasar de un mock a SAP S/4HANA real o a Shopify es solo escribir un adaptador nuevo. El resto del sistema ni se entera.

El problema

Tengo que pintar un dashboard con datos de SAP. Hoy no hay SAP: hay un .txt que imita su export. Mañana habrá un sandbox de OData; pasado, una tienda real. ¿Cómo organizo el código para que ese baile no me obligue a reescribir el backend cada vez que cambia el origen?

La solución

  • Un puerto para el origen

    La interfaz ISalesRepository es el único contrato que conoce de dónde vienen los datos. Todo lo demás depende del contrato, nunca de la implementación.

  • Tres adaptadores, una interfaz

    Mock (.txt en Latin-1), SAP S/4HANA por OData y Shopify por Admin REST. Una variable de entorno (SalesSource) elige cuál se cablea en el composition root.

  • Persistencia tras un segundo puerto

    La ingesta lee de la fuente y guarda en SQLite (ISalesStore); la analítica lee solo del almacén. Cambiar a Postgres sería otro adaptador, sin tocar el dominio.

  • Desplegado de verdad

    Backend y mock en Google Cloud Run, frontend en Vercel, con deploy automático vía GitHub Actions y Workload Identity Federation (sin claves en secretos).

Stack técnico

Backend

  • .NET 10
  • C#
  • Hexagonal
  • Result/Error
  • SQLite
  • xUnit

Frontend

  • Next.js
  • TypeScript
  • App Router
  • Recharts

Infra & Deploy

  • Docker
  • Cloud Run
  • Vercel
  • GitHub Actions
  • OData (SAP)
  • Shopify

Decisiones técnicas

  • Origen de datos tras un puerto

    Regla inviolable: ISalesRepository es lo único que conoce el origen. Añadir una fuente nueva = un adaptador outbound + cambiar su registro en Program.cs. El dominio nunca se entera.

  • Result/Error en vez de excepciones

    Los errores esperables son valores (Result), no excepciones. El adaptador las captura en su borde; el controlador es el único punto que traduce Error → HTTP. Lo cuento en detalle en el post.

  • CI/CD sin claves

    El deploy a Cloud Run se autentica con Workload Identity Federation: GitHub Actions obtiene credenciales efímeras de Google Cloud sin guardar ninguna clave de service account como secreto del repo.

Capturas

Cabecera del dashboard de Connect Analyzer con filtros y tarjetas de KPIs: ingresos totales, ventas, ticket medio, unidades, clientes y productos

Cabecera del dashboard de Connect Analyzer con filtros y tarjetas de KPIs: ingresos totales, ventas, ticket medio, unidades, clientes y productos

Gráfico de área de ingresos en el tiempo del dashboard, con tooltip mostrando el valor de un día concreto

Gráfico de área de ingresos en el tiempo del dashboard, con tooltip mostrando el valor de un día concreto

Gráficos del dashboard: barras de ingresos con línea de unidades por producto, y donut del importe total por cliente

Gráficos del dashboard: barras de ingresos con línea de unidades por producto, y donut del importe total por cliente

¿Cómo está montado por dentro?

Escribí un post largo sobre la arquitectura: puertos, adaptadores, Result/Error y testing sin librerías de mocking.