Volver a proyectos
En producción

Dalem Conecta

CRM para agencias de viaje mayoristas y minoristas en Colombia, en producción.

Rol: Diseño y desarrollo full stack

  • NestJS
  • Prisma
  • PostgreSQL
  • Angular
  • Ionic
  • Capacitor
Tablero de Dalem Conecta

Problema

Las agencias de viaje en Colombia suelen llevar su operación repartida entre hojas de cálculo, chats y la memoria de quien atiende. Una cotización vive en un mensaje, la venta en otro archivo y los pendientes en la cabeza de alguien. Cuando el equipo crece, o cuando el asesor que sabía todo está de vacaciones, se pierde información y se pierden ventas.

El seguimiento de los pagos es otro punto débil. Un viaje se paga en cuotas, a veces en dos monedas, y cada abono tiene que quedar asociado a la venta correcta. Si ese registro vive en una hoja suelta, nadie sabe con certeza cuánto falta por cobrar ni cuándo vence la siguiente cuota. Y después del viaje, la postventa casi siempre se olvida: no hay encuesta, no se registran las incidencias y el cliente no vuelve a saber de la agencia hasta que necesita otro viaje.

Además, hay un caso que casi ningún sistema genérico cubre bien: mayoristas y minoristas que trabajan juntos. El mayorista arma paquetes y el minorista los vende a sus propios clientes, y entre ambos hay un flujo de pedidos, cupos y pagos que hoy se coordina a mano, por mensajes y llamadas.

Quería un solo lugar donde una agencia pudiera llevar el viaje completo, desde la primera cotización hasta la postventa, sin que una agencia pudiera ver nunca los datos de otra. Esa última condición pesó en casi todas las decisiones técnicas que siguen.

Solución y funciones clave

Dalem Conecta es un CRM pensado para ese ciclo completo. La agencia entra desde el navegador o desde la app de Android y trabaja siempre con los mismos datos. Estas son las piezas principales:

Cada agencia tiene su equipo con roles distintos, y un administrador puede dar de alta nuevas agencias. El producto está en producción y sigo mejorándolo a partir de lo que enseña el uso. Cada decisión de arquitectura de las secciones siguientes nació de un problema concreto que había que resolver, y por eso las cuento por capas: primero la API, luego el cliente y al final el sitio que presenta el producto.

API

La API está hecha con NestJS y Prisma sobre PostgreSQL. Elegí NestJS porque su estructura en módulos obliga a ordenar el código desde el primer día, algo que se agradece cuando una sola persona mantiene todo el backend y tiene que poder volver a un módulo después de meses sin tocarlo. Prisma me da un esquema tipado y migraciones versionadas, y con eso el código de acceso a datos se mantiene corto y legible.

La decisión central es el multi-tenant. Todas las agencias comparten una misma base de datos, y el aislamiento entre ellas se aplica de forma automática. Cada petición lleva consigo el contexto de la agencia que la hace, usando nestjs-cls para guardarlo durante toda la vida de la petición, y una extensión de Prisma agrega el filtro por agencia a cada consulta sin que el desarrollador tenga que acordarse de escribirlo. Eso reduce mucho el riesgo de olvidar un filtro en una consulta nueva, que es el error típico de este tipo de sistemas.

Como un filtro automático sigue siendo código y el código puede fallar, agregué una segunda barrera en la propia base de datos: reglas de seguridad a nivel de fila de PostgreSQL (RLS). Si algún día una consulta se escapa del filtro de la aplicación, la base de datos se niega a devolver filas de otra agencia. Son dos capas independientes, y esa redundancia es deliberada. Para un producto donde una agencia jamás debe ver los datos de otra, prefiero pagar el costo de mantener las dos.

La autenticación usa JWT con tokens de refresco y roles por agencia. El token de acceso dura poco y el de refresco se rota en cada uso, así que un token robado tiene una ventana de uso muy limitada. Los roles distinguen lo que puede hacer el administrador, el asesor de ventas y el equipo de operaciones, y el sistema aplica esas reglas en el servidor, nunca solo en la interfaz. También hay un límite de intentos de inicio de sesión y un caso aparte para el superadministrador, que puede cambiar de agencia para dar soporte.

Por último, la API publica una especificación OpenAPI que funciona como contrato. No es solo documentación: de ahí sale el cliente tipado que usa la interfaz, como cuento en la siguiente sección. Los módulos por agencia se controlan con un guard global, de modo que una función desactivada para una agencia no responde ni aunque alguien conozca la ruta.

Cliente

El cliente está hecho con Angular, Ionic y Capacitor. Es un único código para la web y para la app de Android, y esa fue la decisión que más tiempo me ahorra. Una sola persona mantiene ambos clientes, con las mismas pantallas y la misma lógica, y cada mejora llega a los dos a la vez. Hoy el producto se usa en el navegador y en Android, que es lo que necesitan las agencias, y prefiero decirlo claro antes que prometer plataformas que no existen.

El precio de esa decisión es que la app no es nativa pura. En pantallas muy exigentes pierdo algo de fluidez, pero a cambio mantengo mucho menos código, y para un CRM de formularios, listas y tableros ese costo casi no se nota.

Los tipos y el cliente HTTP se generan a partir de la especificación OpenAPI de la API. Así el contrato entre cliente y servidor vive en un solo lugar, y un cambio en la API que rompa una pantalla se detecta al compilar, no en producción. El costo es un paso extra de generación en el flujo de trabajo y la disciplina de mantener la especificación al día, algo que acepto sin dudar.

Dos funciones muestran bien cómo se reparten las tareas entre cliente y servidor. La primera es el PDF de las cotizaciones: se genera en el navegador, no en el servidor, lo que evita cargar la API y mantiene el flujo rápido. Tuve que cuidar el diseño para que el documento se vea igual en cualquier dispositivo, porque el resultado depende de la máquina del asesor. La segunda son las plantillas de WhatsApp: las agencias ya cierran sus ventas por ese canal, así que desde la cotización el asesor envía el PDF con un mensaje armado a partir de una plantilla, y puede ajustarlo antes de mandarlo.

Esa separación sigue una regla simple: la API devuelve datos crudos y el cliente se encarga de la presentación, es decir, los textos, los formatos de moneda y fecha, y los documentos. La excepción son los correos, que salen del servidor con plantillas propias de cada agencia.

Sitio comercial

El producto tiene además un sitio web estático que lo presenta a las agencias interesadas, en dalemconecta.com, separado de la aplicación que vive en app.dalemconecta.com. Lo mantengo aparte a propósito: es rápido, no depende de la API y puede cambiar de texto o de diseño sin tocar el producto. Lo diseñé y lo construí yo también, con las mismas ganas de que cargue rápido y se entienda en pocos segundos.

Ver el caso del sitio →

Arquitectura

Arquitectura de Dalem ConectaDiagrama simplificado: el cliente web y Android llama a la API; la API usa la base de datos, la autenticación y los servicios externos de correo y WhatsApp. El sitio comercial es independiente.Web y AndroidAngular + IonicAPINestJS + PrismaRLS por tenantServicios externosCorreo y WhatsAppSitio comercialAstro estáticoBase de datosPostgreSQLAutenticaciónJWT + rolesArquitectura de Dalem ConectaDiagrama simplificado: el cliente web y Android llama a la API; la API usa la base de datos, la autenticación y los servicios externos de correo y WhatsApp. El sitio comercial es independiente.Web y AndroidAngular + IonicAPINestJS + PrismaRLS por tenantServicios externosCorreo y WhatsAppSitio comercialAstro estáticoBase de datosPostgreSQLAutenticaciónJWT + roles

Diagrama simplificado; omite detalles internos por privacidad.

Decisiones técnicas

  1. Varias agencias comparten una sola base de datos, con el aislamiento aplicado de forma automática.

    Por qué
    Cada consulta se filtra por agencia sin que el desarrollador tenga que acordarse, y las reglas de seguridad a nivel de fila de PostgreSQL actúan como segunda barrera si algo se escapa.
    Trade-off
    Un fallo en el filtro automático sería más grave que con bases separadas, así que hay que probar ese aislamiento con más rigor.
  2. Un único código Angular con Ionic y Capacitor para la web y la app de Android.

    Por qué
    Una sola persona mantiene ambos clientes con las mismas pantallas y la misma lógica, y cada mejora llega a los dos a la vez.
    Trade-off
    La app no es nativa pura: pierdo algo de fluidez en pantallas muy exigentes a cambio de mucho menos código que mantener.
  3. Los tipos del cliente se generan a partir de la especificación OpenAPI de la API.

    Por qué
    El contrato entre cliente y servidor queda en un solo lugar, y un cambio en la API que rompa una pantalla se detecta al compilar, no en producción.
    Trade-off
    Añade un paso de generación al flujo de trabajo y obliga a mantener la especificación al día.
  4. El PDF de la cotización se genera en el navegador y se envía con una plantilla de WhatsApp.

    Por qué
    Las agencias ya cierran sus ventas por WhatsApp, y generar el PDF en el cliente evita cargar al servidor y mantiene el flujo rápido.
    Trade-off
    El resultado depende del dispositivo del asesor, así que tuve que cuidar el diseño del PDF para que se vea igual en todos.

Mi rol

Diseñé y construí todo el producto: la API, el cliente web y Android, y el sitio de presentación. Tomé las decisiones de arquitectura, escribí el código y llevé el sistema hasta producción. Hoy lo mantengo y lo hago crecer según lo que enseña el uso.

Siguiente proyecto

En producciónSitio comercial de Dalem ConectaSitio web estático que presenta Dalem Conecta a las agencias de viaje, rápido, con buen SEO y en producción.Ver caso

¿Tienes un proyecto?

Hablemos de tu proyecto