Saltar al contenido
Volver a proyectos
Cliente
2026

Plataforma omnicanal de servicio al cliente

Bandeja única que reúne WhatsApp, correo y llamadas de un ERP, con un agente de IA que responde y escala a un humano cuando no sabe.

3

canales unificados

19

documentos de diseño

TDD

prueba antes que código

100%

aislamiento verificado

El problema

El área de soporte de un ERP atendía por WhatsApp, por correo y por teléfono, cada canal en una herramienta distinta. Un mismo cliente escribía por tres vías y nadie veía la conversación completa. Además el producto es multiempresa: los datos de cada licencia deben quedar aislados de los demás sin excepción, porque una fuga entre empresas no es un error de software, es un incidente legal.

La solución

Backend en .NET 10 con Clean Architecture y Vertical Slice, desarrollado con TDD. Unifica los tres canales en una sola conversación por cliente, incorpora un agente de IA con recuperación sobre la base de conocimiento del producto (RAG) y un modo de contingencia que garantiza que, si la IA no está disponible, toda conversación escala a un asesor humano en lugar de quedarse sin respuesta.

La decisión técnica que lo sostiene

El aislamiento entre empresas no se confía al programador de turno: vive en un filtro global de consultas de la capa de datos, de modo que una consulta que olvide filtrar por empresa simplemente no puede devolver datos ajenos. Y no se verifica con un mock, sino con pruebas de integración contra una base de datos PostgreSQL real levantada en contenedor para cada ejecución. Los webhooks de los canales son idempotentes: el proveedor puede reintentar el mismo evento y el mensaje no se duplica.

El resultado

Trazabilidad completa de cada cliente en una sola bandeja, el aislamiento entre empresas verificado por pruebas automáticas en cada compilación, y despliegue en contenedores con telemetría y tableros de monitoreo. Diecinueve documentos de diseño técnico acompañan el código.

Cómo se ve por dentro

Ejemplos ilustrativos de los patrones aplicados, con la razón de cada decisión.

El aislamiento entre empresas vive en la infraestructura

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Conversacion>()
        .HasQueryFilter(c => c.TenantId == _tenantContext.TenantId);

    modelBuilder.Entity<Mensaje>()
        .HasQueryFilter(m => m.TenantId == _tenantContext.TenantId);
}

Un filtro global se aplica a todas las consultas de la entidad. Aunque un caso de uso olvide filtrar por empresa, la consulta no puede devolver datos de otra. La seguridad deja de depender de que nadie se equivoque.

Ejemplo ilustrativo del patrón aplicado; no es código de ningún producto.

Verificado contra una base de datos real, no contra un mock

csharp
[Fact]
public async Task Una_empresa_no_puede_leer_conversaciones_de_otra()
{
    await CrearConversacionComo(TenantA, asunto: "Factura 001");

    var resultado = await ClienteComo(TenantB)
        .GetFromJsonAsync<ConversacionDto[]>("/api/conversaciones");

    Assert.Empty(resultado!);
}

La prueba levanta un PostgreSQL efímero en contenedor, escribe con una empresa y lee con otra. Un mock habría pasado igual con el filtro roto; esta prueba no.

Ejemplo ilustrativo del patrón aplicado; no es código de ningún producto.

Stack del proyecto

¿Necesitas algo parecido?

Cuéntame tu caso y te digo con franqueza si es viable y cuánto toma.

Hablemos