crm

CRM para pymes locales (proyecto de práctica)

CRM multi-tenant pensado para pequeños negocios locales (peluquerías, talleres, centros de estética, cristalerías, tiendas de ventanas/toldos, etc.): alta de clientes, registro de interacciones (llamadas, visitas), compras, devoluciones, puntos de fidelización configurables por negocio y segmentos de clientes pre-calculados.

Ver docs/ARCHITECTURE.md para las decisiones de arquitectura y seguridad, y por qué se tomaron.

Estado actual

Construido y probado de punta a punta:

Pendiente (próximas iteraciones): ninguno de los puntos planteados hasta ahora — el roadmap se irá ampliando según nuevas necesidades.

Requisitos

Arrancar con Docker Compose

cp .env.example .env
docker compose up --build

La primera vez que arranca el backend, aplica automáticamente la migración inicial (prisma migrate deploy) usando el rol administrador de Postgres. El backend en sí siempre se conecta con app_role, un rol sin privilegio de superusuario, para que las políticas de Row-Level Security se apliquen de verdad (ver docs/ARCHITECTURE.md).

Probar la API

# 0. Entrar como administrador de la plataforma (credenciales de
#    PLATFORM_ADMIN_EMAIL / PLATFORM_ADMIN_PASSWORD en tu .env)
curl -X POST http://localhost:3000/platform-admin/login \
  -H 'Content-Type: application/json' \
  -d '{"email": "admin@example.com", "password": "<tu PLATFORM_ADMIN_PASSWORD>"}'
# -> devuelve un accessToken de administrador de plataforma

# 0b. Autorizar un negocio nuevo (crea el tenant + su usuario "owner")
curl -X POST http://localhost:3000/platform-admin/tenants \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken de administrador>' \
  -d '{
    "slug": "peluqueria-marisa",
    "businessName": "Peluqueria Marisa",
    "businessType": "peluqueria",
    "ownerEmail": "marisa@example.com",
    "ownerPassword": "password123",
    "ownerFullName": "Marisa Gomez"
  }'

# 1. El owner ya puede hacer login normal (con su propio slug/email/password)
curl -X POST http://localhost:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"slug": "peluqueria-marisa", "email": "marisa@example.com", "password": "password123"}'
# -> devuelve un accessToken (JWT) de este negocio

# 2. Usar el token para crear un cliente
curl -X POST http://localhost:3000/customers \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <accessToken>' \
  -d '{"fullName": "Cliente de prueba", "phone": "600111222"}'

# 3. Listar clientes (solo verás los de tu propio negocio)
curl http://localhost:3000/customers -H 'Authorization: Bearer <accessToken>'

# 4. Configurar las reglas de puntos de tu negocio (solo owner/admin)
curl -X PATCH http://localhost:3000/points-config \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"pointsPerCurrencyUnit": 2, "minPurchaseAmount": 10}'

# 5. Registrar una visita de un cliente
curl -X POST http://localhost:3000/customers/<customerId>/interactions \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"type": "visit", "notes": "Revisión anual"}'

# 5b. Registrar una llamada comercial con resultado
curl -X POST http://localhost:3000/customers/<customerId>/interactions \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"type": "call", "outcome": "call_back", "followUpAt": "2026-10-01T10:00:00Z"}'

# 6. Crear un producto (solo owner/admin)
curl -X POST http://localhost:3000/products \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"name": "Ventana PVC 1x1m", "category": "Ventanas", "price": 150}'

# 7. Registrar una compra con líneas de producto (calcula el total y los puntos)
curl -X POST http://localhost:3000/customers/<customerId>/purchases \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"items": [{"productId": "<productId>", "quantity": 2}]}'

# 8. Registrar una devolución parcial de esa compra
curl -X POST http://localhost:3000/customers/<customerId>/purchases/<purchaseId>/returns \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"amount": 20, "reason": "Pieza defectuosa"}'

# 9. Ver los segmentos pre-creados (solo owner/admin)
curl http://localhost:3000/segments -H 'Authorization: Bearer <accessToken>'

# 10. Buscar clientes
curl "http://localhost:3000/customers?q=marisa" -H 'Authorization: Bearer <accessToken>'

# 11. Reportes (solo owner/admin)
curl http://localhost:3000/reports/customers -H 'Authorization: Bearer <accessToken>'
curl http://localhost:3000/reports/products -H 'Authorization: Bearer <accessToken>'

# 12. Filtro personalizado: clientes que compraron de la categoría "Ventanas"
#     en los últimos 90 días (solo owner/admin)
curl -X POST http://localhost:3000/custom-segments/preview \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{
    "periodDays": 90,
    "matchType": "all",
    "conditions": [{"field": "purchased_category", "operator": "eq", "value": "Ventanas"}]
  }'

# 12b. Guardar ese filtro para reutilizarlo
curl -X POST http://localhost:3000/custom-segments \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{
    "name": "Compradores de Ventanas",
    "periodDays": 90,
    "matchType": "all",
    "conditions": [{"field": "purchased_category", "operator": "eq", "value": "Ventanas"}]
  }'

# 13. Crear un lead y moverlo de fase
curl -X POST http://localhost:3000/leads \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"fullName": "Lead de prueba", "company": "Acme", "estimatedValue": 5000}'
curl -X PATCH http://localhost:3000/leads/<leadId>/stage \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"stage": "qualified"}'

# 13b. Convertir un lead en cliente real
curl -X POST http://localhost:3000/leads/<leadId>/convert-to-customer \
  -H 'Authorization: Bearer <accessToken>'

# 14. Contactos (partners / contactos interesantes)
curl -X POST http://localhost:3000/contacts \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"category": "partner", "fullName": "Partner de prueba", "company": "PartnerCo"}'
curl "http://localhost:3000/contacts?category=partner" -H 'Authorization: Bearer <accessToken>'

# 15. Editar un cliente
curl -X PATCH http://localhost:3000/customers/<customerId> \
  -H 'Content-Type: application/json' -H 'Authorization: Bearer <accessToken>' \
  -d '{"fullName": "Nombre corregido", "consentMarketing": true}'

# 16. RGPD: exportar todos los datos de un cliente (portabilidad)
curl http://localhost:3000/customers/<customerId>/export -H 'Authorization: Bearer <accessToken>'

# 17. RGPD: borrado físico definitivo (exige borrado lógico previo)
curl -X DELETE http://localhost:3000/customers/<customerId> -H 'Authorization: Bearer <accessToken>'
curl http://localhost:3000/customers/deleted -H 'Authorization: Bearer <accessToken>'
curl -X DELETE http://localhost:3000/customers/<customerId>/purge -H 'Authorization: Bearer <accessToken>'

Extensión de Chrome (captura de LinkedIn)

Carpeta extension/, no forma parte del backend/frontend — es una extensión de Chrome (Manifest V3) independiente que habla con la misma API. Ver extension/README.md para instalación paso a paso, uso, y los caveats de scraping de LinkedIn (extracción manual y a demanda, sin scripts en segundo plano, best-effort porque LinkedIn no documenta su HTML). Guarda lo extraído como lead o contact (partner/interesante) usando los mismos endpoints de arriba, con source: "linkedin_extension".

Desarrollo sin Docker (backend)

cd backend
npm install
npx prisma generate

# Aplicar la migración inicial usando el rol administrador de Postgres
# (crea también el rol "app_role" que usará la aplicación)
DATABASE_URL="postgresql://postgres:<tu_password>@localhost:5432/crm" \
  npx prisma migrate deploy

# Arrancar el backend conectado como app_role (no como superusuario)
DATABASE_URL="postgresql://app_role:<password_de_app_role>@localhost:5432/crm" \
  JWT_SECRET="cualquier-secreto-largo" \
  PLATFORM_ADMIN_JWT_SECRET="otro-secreto-distinto" \
  PLATFORM_ADMIN_EMAIL="admin@example.com" \
  PLATFORM_ADMIN_PASSWORD="cambia-esto" \
  npm run start:dev

Desarrollo sin Docker (frontend)

cd frontend
npm install
VITE_API_URL="http://localhost:3000" npm run dev

Seguridad — resumen

Este es un proyecto de práctica: las contraseñas de ejemplo en .env.example y en docker-compose.yml son válidas solo para desarrollo local y deben cambiarse antes de cualquier uso real.