# LexControl EC — Arquitectura técnica (v0.1)

> Nombre provisional. Estado: en construcción. Este documento describe lo que se está construyendo; las limitaciones reales están en `LIMITACIONES.md`.

## 1. Alcance de la primera versión

| Módulo | Incluye | No incluye |
|---|---|---|
| Clientes y expedientes | CRUD, búsqueda, filtros, archivar, historial | Integración con la Función Judicial |
| Agenda y tareas | Audiencias/reuniones/eventos, tareas, responsables, prioridades | Cálculo automático de términos procesales |
| Alertas | Recordatorios con anticipación configurable, aviso en la app, correo con reintentos | Afirmar que el correo fue leído |
| Plantillas | Plantillas con variables `{{cliente.nombre}}`, generación en DOCX | IA que redacte escritos |
| Cotizaciones | Ítems, descuento, IVA configurable, estados, exportación | Facturación electrónica |
| Archivos | Carga privada, vinculación a expediente/cliente, etiquetas, búsqueda por nombre/etiqueta | OCR, búsqueda dentro del contenido |
| Acceso | Web adaptable PC/móvil, instalable (PWA) | Apps nativas |

Las fechas de plazos son datos de organización ingresados por el usuario, no un cálculo ni una certificación de plazos procesales.

## 2. Pila tecnológica y por qué

- **Servidor:** Node 22 + TypeScript + Fastify. Una sola aplicación (monolito), sin microservicios.
- **Base de datos:** PostgreSQL (producción, vía `pg`). En desarrollo, pruebas e instalación local de un usuario se usa **PGlite** (PostgreSQL real compilado a WASM, mismo dialecto SQL). Ambos están detrás de una interfaz mínima (`Db`), por lo que no hay SQL distinto por entorno.
- **Validación:** Zod en cada entrada de la API.
- **Contraseñas:** `scrypt` de `node:crypto` (memoria-dura, sin dependencias nativas), sal única por usuario, parámetros guardados junto al hash para poder subirlos después.
- **Sesiones:** token aleatorio de 256 bits en cookie `HttpOnly; SameSite=Lax; Secure` (en producción). En la base solo se guarda su hash SHA-256, por lo que pueden revocarse una por una o todas las de un usuario.
- **Interfaz:** React + TypeScript + Vite, CSS propio mobile-first, PWA (manifiesto + service worker que solo guarda el cascarón de la app, **nunca** datos de la API).
- **Correo:** interfaz `MailProvider`; `log` (desarrollo) y SMTP (`nodemailer`, producción). Proveedor concreto pendiente de decisión.
- **Documentos:** DOCX con la librería `docx`.

## 3. Estructura de carpetas

```
lexcontrol/
  docs/                     ARQUITECTURA.md, LIMITACIONES.md
  server/
    src/
      config.ts             variables de entorno validadas
      db.ts                 interfaz Db + adaptadores PGlite / pg
      migrate.ts            ejecuta migrations/*.sql en orden
      migrations/           SQL versionado
      app.ts                construye Fastify (usado por servidor y pruebas)
      index.ts              arranque
      auth/                 contraseñas, sesiones, hook de autenticación
      permissions.ts        matriz rol → permisos
      routes/               un archivo por módulo
      services/             audit, search, alertas, plantillas, cotizaciones
      jobs/worker.ts        procesa recordatorios y cola de correo
      mail/                 proveedores de correo
      storage/              almacenamiento privado de archivos
    test/                   pruebas (node:test)
  web/                      aplicación React
```

## 4. Modelo de datos (resumen)

Toda tabla de negocio tiene `org_id`. Las referencias entre tablas son **compuestas** `(org_id, id)`, de modo que la propia base impide que un expediente de un despacho apunte a un cliente de otro.

- `organizations` (id, name, timezone, default_vat_rate_bp, created_at)
- `users` (id, org_id, email único, name, password_hash, role, active, created_at)
- `sessions` (token_hash, user_id, org_id, expires_at, revoked_at, user_agent)
- `invitations` (token_hash, org_id, email, role, expires_at, accepted_at)
- `audit_log` (id, org_id, user_id, action, entity_type, entity_id, summary, created_at) — **sin contenido sensible**, solo qué y quién.
- `clients` (id, org_id, full_name, id_number opcional, email, phone, notes, search_text, archived_at)
- `cases` (expedientes: id, org_id, internal_code, process_number opcional, title, client_id, matter, court, counterparty, responsible_id, status, opened_on, description, next_action, next_action_date, search_text)
- `tasks` (id, org_id, case_id opcional, title, description, assignee_id, due_at, priority, status, completed_at)
- `events` (id, org_id, case_id opcional, kind: audiencia|reunion|otro, title, starts_at, ends_at, location, notes, assignee_id)
- `reminders` (id, org_id, target_type, target_id, user_id, fire_at, state, fired_at)
- `notifications` (id, org_id, user_id, title, body, link, created_at, acknowledged_at)
- `email_outbox` (id, org_id, to_email, subject, body, dedupe_key único, state, attempts, next_attempt_at, last_error, provider_message_id, sent_at)
- `templates` (id, org_id, name, category, body)
- `generated_documents` (id, org_id, template_id, case_id, client_id, title, file_id, created_by)
- `quotes` (id, org_id, number, client_id, case_id, status, valid_until, items jsonb, discount_cents, vat_rate_bp, notes)
- `files` (id, org_id, case_id, client_id, name, mime, size, sha256, storage_key, tags[], uploaded_by, deleted_at)

Dinero en **centavos enteros**. Fechas en `timestamptz`; zona horaria del despacho `America/Guayaquil` por defecto.

## 5. Autenticación y permisos

- Registro crea un despacho y su primer usuario (rol `admin`). Los demás ingresan por invitación con token de un solo uso.
- Cada ruta (salvo registro, inicio de sesión y aceptación de invitación) pasa por el hook de autenticación, que carga `{userId, orgId, role}` **desde la sesión**; jamás se acepta un `org_id` enviado por el cliente.
- Permisos por rol, aplicados en el servidor en cada ruta (`requirePermission('cases:write')`):

| Permiso | admin | abogado | colaborador |
|---|---|---|---|
| clients read / write | ✔ / ✔ | ✔ / ✔ | ✔ / — |
| cases read / write / archive | ✔ / ✔ / ✔ | ✔ / ✔ / ✔ | ✔ / — / — |
| tasks y events (read/write) | ✔ | ✔ | ✔ |
| files read / write / delete | ✔ | ✔ | ✔ / ✔ / — |
| templates read / write | ✔ | ✔ | ✔ / — |
| quotes read / write | ✔ | ✔ | — |
| users y ajustes del despacho | ✔ | — | — |
| export | ✔ | ✔ | — |

## 6. Aislamiento entre despachos

1. `org_id` siempre sale de la sesión.
2. Toda consulta incluye `WHERE org_id = $n`; los accesos por id usan `WHERE id = $1 AND org_id = $2`.
3. Claves foráneas compuestas impiden vínculos entre despachos a nivel de base.
4. Pruebas automáticas: un usuario del despacho B intenta leer, editar, borrar, buscar y descargar datos del despacho A y debe recibir 404.
5. Endurecimiento posterior previsto: Row Level Security en PostgreSQL como segunda barrera (ver LIMITACIONES).

## 7. Archivos

Disco privado fuera de la carpeta pública (`STORAGE_DIR`), con clave aleatoria `org/<orgId>/<uuid>`; el nombre original solo se guarda como metadato. Descarga únicamente por endpoint autenticado que verifica despacho y permiso. Tipos permitidos por lista blanca, tamaño máximo configurable, hash SHA-256. Interfaz `Storage` para cambiar a almacenamiento de objetos privado más adelante. El contenido de los documentos no se registra en logs.

## 8. Alertas

1. Al guardar una tarea o evento con recordatorios (minutos de anticipación), se crean filas en `reminders` con `fire_at`.
2. El worker (cada 30 s) toma recordatorios vencidos (`FOR UPDATE SKIP LOCKED`), crea una **notificación dentro de la app** y un registro en `email_outbox` con `dedupe_key = reminder:<id>:<user>` único (evita duplicados aunque el worker se repita).
3. El envío de correo reintenta con espera creciente hasta 5 intentos.
4. Estados mostrados: `programada`, `enviada` (el proveedor aceptó el mensaje y devolvió identificador), `fallida` (agotó intentos; visible y reintentable manualmente), `confirmada` (**solo** cuando la persona confirma en la app). Nunca se afirma que el correo fue recibido o leído.

## 9. Despliegue y recuperación

- Un contenedor/proceso Node + PostgreSQL gestionado con HTTPS, copias automáticas y almacenamiento privado.
- Variables de entorno en `.env.example`; ningún secreto en el código ni en el frontend.
- Respaldo: `pg_dump` programado (modo PostgreSQL) o volcado del directorio de PGlite (modo local), más copia del directorio de archivos. Procedimiento de restauración documentado y probado en pruebas automáticas para el modo PGlite; la prueba contra PostgreSQL real está pendiente.
- La exportación CSV de reportes **no** es un respaldo completo.

## 10. Decisiones pendientes (requieren al usuario)

1. Proveedor de correo y dominio remitente.
2. Hosting y región (impacta transferencias internacionales de datos, LOPDP).
3. Tipo de IVA por defecto en cotizaciones (hoy configurable; confirmar con contador).
4. Textos legales (términos, política de privacidad) revisados por abogado.
5. Pasarela de pago (fuera de esta etapa).


## Ampliaciones (línea de tiempo, correo propio, marca)

- `case_milestones`: hitos por expediente (documento, trámite, audiencia, resolución, notificación, otro; realizado/pendiente; fecha; referencia; documento opcional del mismo expediente). Claves compuestas `(org_id, …)`; permiso `timeline:write`. API: `GET /cases/:id/timeline`, `POST /cases/:id/milestones`, `GET/PATCH/DELETE /milestones/:id` (el detalle trae anterior/siguiente). Al borrar un archivo, los hitos que lo usaban quedan sin documento.
- `org_mail_settings`: SMTP propio por despacho; contraseña cifrada AES-256-GCM con clave derivada de `APP_SECRET`; nunca se devuelve por la API ni se registra. El worker elige el proveedor del despacho y, si no hay, el global (`MAIL_PROVIDER`, `none` por defecto en producción). Se rechazan hosts internos (anti-SSRF) y puertos fuera de 25/465/587/2525.
- Marca: `organizations.logo_*` (PNG/JPG/WEBP ≤ 1 MB, validado por firma); `/api/brand` para la pantalla de acceso (nombre neutro si hay varios despachos).
