# 02 · TRD — Documento de Requisitos Técnicos

**Proyecto:** Sistema Boutique (POS + Tienda Online)
**Base de datos:** PostgreSQL
**Moneda:** COP (entero, sin centavos)
**Versión:** 1.0

---

## 1. Decisión de arquitectura

**Monorepo con un backend único y dos frontends.**

```
Un backend (Fastify + TypeScript)  ← única fuente de verdad
        │
        ├── PostgreSQL (un solo inventario, un solo catálogo)
        │
        ├── Frontend POS      (React + TS) → usado en el mostrador
        └── Frontend Tienda   (React + TS) → usado por clientes
```

**Por qué monorepo y no dos proyectos separados:** el catálogo, el inventario, los clientes y la lógica de stock son **compartidos**. Si fueran dos proyectos, tendrías que duplicar modelos, validaciones y tipos, y mantenerlos sincronizados a mano — fuente garantizada de bugs. En un monorepo, el backend y los tipos de datos se definen **una sola vez** y ambos frontends los consumen.

Analogía automotriz: es un solo motor (backend) con dos tableros de instrumentos distintos (POS y tienda). No pones dos motores en un carro para tener dos tableros.

---

## 2. Stack tecnológico

### 2.1 Backend

| Componente | Elección | Justificación |
|------------|----------|---------------|
| Lenguaje | **TypeScript** (Node.js 20 LTS) | Tipado estricto, mismo lenguaje que el frontend → tipos compartidos |
| Framework | **Fastify** | Más rápido y liviano que Express; validación de esquemas nativa; ideal para VPS con RAM limitada |
| ORM | **Prisma** | Migraciones limpias y versionadas; consultas type-safe; previene inyección SQL por diseño |
| Validación | **Zod** | Valida y tipa la entrada en el borde de la API; una sola definición sirve de validación y de tipo |
| Auth | **JWT** (access + refresh) + **Argon2id** | Argon2id es el estándar OWASP actual para hashing de contraseñas |
| Arquitectura | **Clean Architecture** (capas) | Dominio aislado de framework y BD; el hardware de Fase 9 entra como "puerto" sin tocar el core |

### 2.2 Frontend (ambas apps)

| Componente | Elección | Justificación |
|------------|----------|---------------|
| Librería | **React 18 + TypeScript** | Ecosistema maduro, componentes reutilizables entre POS y tienda |
| Build | **Vite** | Arranque y build rápidos; ideal para dos apps en monorepo |
| Estilos | **Tailwind CSS** | Diseño consistente, modo claro/oscuro trivial, sin CSS suelto |
| Estado servidor | **TanStack Query** | Cache, refetch e invalidación de stock en tiempo real sin boilerplate |
| Formularios | **React Hook Form + Zod** | Reutiliza los mismos esquemas Zod del backend |
| Ruteo | **React Router** | Estándar, suficiente (no se necesita SSR para un POS ni para esta tienda) |

**Nota sobre Next.js:** el prompt lo mencionaba como opción. Se descarta a propósito — el POS no necesita SSR/SEO, y la tienda de una boutique pequeña tampoco lo justifica frente al costo de complejidad. Vite + React es más simple de desplegar en tu VPS con Apache. Si en el futuro el SEO de la tienda se vuelve crítico, se migra solo la tienda a Next.js sin tocar el backend.

### 2.3 Base de datos

- **PostgreSQL 11.22** (instalado localmente en el VPS en `/home/pgsql11_data`, escucha en `127.0.0.1:5432`).
  > **Nota de desviación aprobada:** el objetivo arquitectural es PostgreSQL 15+. La versión actual es 11.22 porque el binario disponible en el VPS es `postgresql11-server` y los repos de `postgresql11-contrib` (que contienen `pgcrypto`) están dados de baja. Esta versión cubre el 100 % de las funcionalidades requeridas (serializable, numeric, JSONB, CHECK, timestamptz). Migrar a PG 15 es una tarea de operaciones futura que no requiere cambios en el código de aplicación.
- Montos monetarios en `numeric(12,0)` — enteros exactos, **nunca `float`** (los flotantes pierden precisión con dinero).
- Integridad referencial con `foreign key` y `on delete restrict/cascade` según el caso.
- Índices en toda columna usada para búsqueda (código de barras, referencia, fechas de venta).
- Transacciones serializables para el descuento de stock (evita sobreventa en ventas concurrentes POS + online).
- **IDs como `String @id @default(uuid())`** en Prisma (sin `@db.Uuid`): Prisma genera el UUID v4 en Node.js vía `crypto.randomUUID()`, evitando la dependencia de `gen_random_uuid()` que requiere `pgcrypto` en PG < 13. El valor es un UUID v4 válido almacenado como `text`. Zod valida el formato UUID en el borde de la API. Cuando se migre a PG 15, una sola migración `ALTER COLUMN id TYPE uuid USING id::uuid` convierte las columnas sin tocar el código.

### 2.4 Servidor / Despliegue (tu VPS)

| Capa | Herramienta |
|------|-------------|
| Servidor web / reverse proxy | **Apache** (ya instalado) con `mod_proxy` hacia el backend Node |
| Proceso Node | **PM2** o systemd (mantiene el backend vivo y lo reinicia si cae) |
| Base de datos | **PostgreSQL 11.22** local (`127.0.0.1:5432`). Inicio manual post-reboot: `/home/pgsql11_data/pg_start.sh` |
| Archivos estáticos | Apache sirve el build de POS y de la tienda |
| TLS | Let's Encrypt (Certbot) sobre Apache |

### 2.5 Almacenamiento de archivos (imágenes de producto)

| Aspecto | Decisión |
|---------|----------|
| Almacenamiento | Disco local del VPS |
| Directorio físico | `/var/www/boutique/uploads/products/` |
| Servido por | Apache como estático (directiva `Alias` en cada vhost) |
| Referencia en BD | `product_images.file_path` guarda la ruta relativa (sin dominio ni protocolo) |
| Servicio externo | **Ninguno** — sin S3 ni Cloudinary en Fase 1 |

La ruta relativa permite que el mismo registro de BD funcione en ambiente de desarrollo (`demoboutique.alexanderpaez.com.ve`) y en producción (`<PROD_DOMAIN>`) sin ningún cambio en la base de datos.

---

### 2.6 Ambientes y dominios

Existen **dos ambientes**. Ningún dominio se incrusta en el código; todo viene de variables de entorno. El ambiente de **desarrollo** (`demoboutique.alexanderpaez.com.ve`) es exclusivamente para pruebas; **nunca es producción**.

| Ambiente | Tienda (raíz) | POS | API |
|----------|--------------|-----|-----|
| **Desarrollo** | `demoboutique.alexanderpaez.com.ve` | `pos.demoboutique.alexanderpaez.com.ve` | `api.demoboutique.alexanderpaez.com.ve` |
| **Producción** | `<PROD_DOMAIN>` *(por definir)* | `pos.<PROD_DOMAIN>` | `api.<PROD_DOMAIN>` |

**Reglas de implementación:**
- `CORS_ORIGINS` en `.env` lista los orígenes permitidos; el backend **nunca tiene dominios hardcodeados**.
- CORS en producción acepta únicamente `<PROD_DOMAIN>` y `pos.<PROD_DOMAIN>`; en desarrollo, los equivalentes de dev.
- Apache tendrá **un vhost por subdominio** en cada ambiente (3 vhosts × 2 ambientes = 6 en total cuando coexistan).
- El placeholder `<PROD_DOMAIN>` se reemplaza cuando el cliente provea el dominio definitivo.

**Variables de entorno (todas las apps las leen de `.env`):**

```
APP_DOMAIN=
API_URL=
STORE_URL=
POS_URL=
CORS_ORIGINS=
```

**Archivo `.env.example`** (versionado en Git, sin valores secretos):

```dotenv
# ── DESARROLLO ────────────────────────────────────────────────────────────
# APP_DOMAIN=demoboutique.alexanderpaez.com.ve
# API_URL=https://api.demoboutique.alexanderpaez.com.ve
# STORE_URL=https://demoboutique.alexanderpaez.com.ve
# POS_URL=https://pos.demoboutique.alexanderpaez.com.ve
# CORS_ORIGINS=https://demoboutique.alexanderpaez.com.ve,https://pos.demoboutique.alexanderpaez.com.ve

# ── PRODUCCIÓN (reemplazar <PROD_DOMAIN> con el dominio real del cliente) ─
# APP_DOMAIN=<PROD_DOMAIN>
# API_URL=https://api.<PROD_DOMAIN>
# STORE_URL=https://<PROD_DOMAIN>
# POS_URL=https://pos.<PROD_DOMAIN>
# CORS_ORIGINS=https://<PROD_DOMAIN>,https://pos.<PROD_DOMAIN>

# ── BASE DE DATOS ─────────────────────────────────────────────────────────
# DATABASE_URL=postgresql://USER:PASSWORD@localhost:5432/boutique

# ── JWT ──────────────────────────────────────────────────────────────────
# JWT_SECRET=<secreto-aleatorio-mínimo-256bits>
# JWT_ACCESS_EXPIRES=15m
# JWT_REFRESH_EXPIRES=7d

# ── WOMPI ────────────────────────────────────────────────────────────────
# WOMPI_PUBLIC_KEY=
# WOMPI_PRIVATE_KEY=
# WOMPI_WEBHOOK_SECRET=

# ── CORREO ───────────────────────────────────────────────────────────────
# MAILER_HOST=
# MAILER_PORT=587
# MAILER_USER=
# MAILER_PASS=
# MAILER_FROM=
```

---

## 3. APIs y servicios externos

| Servicio | Uso | Estrategia |
|----------|-----|------------|
| **Wompi** (Bancolombia) | Pasarela de pago de la tienda online | Integración por defecto. Se implementa detrás de un **puerto `PaymentGateway`** (interfaz), así que cambiarla por Mercado Pago es cambiar una clase, no reescribir el checkout. |
| **Mercado Pago Colombia** | Alternativa de pago | Segunda implementación del mismo puerto (opcional). |
| Envío de correo | Confirmaciones de pedido | SMTP del VPS o un proveedor (Resend/SES) tras un puerto `Mailer`. |
| Generación de código de barras | Etiquetas y variantes | Librería local (`bwip-js`), sin servicio externo. |

**Principio:** todo servicio externo entra por una **interfaz (puerto)**. El dominio no sabe si el pago es Wompi o Mercado Pago; solo sabe "cobrar". Esto es Clean Architecture aplicada — y es lo que te permite cambiar de proveedor sin romper nada.

---

## 4. Seguridad (OWASP Top 10 desde el día 1)

| Riesgo OWASP | Mitigación concreta |
|--------------|--------------------|
| A01 Control de acceso roto | RBAC por rol en cada endpoint; el backend valida el rol, nunca confía en el frontend |
| A02 Fallas criptográficas | Contraseñas con Argon2id; JWT firmado; TLS obligatorio |
| A03 Inyección | Prisma parametriza toda consulta; Zod valida toda entrada |
| A04 Diseño inseguro | Descuento de stock en transacción atómica; límites de descuento por rol |
| A05 Mala configuración | Variables sensibles en `.env` (fuera de Git); CORS restringido a los dominios del POS y la tienda |
| A07 Fallas de autenticación | Rate limiting en login; refresh tokens rotatorios; expiración corta del access token |
| A08 Integridad de datos | Validación de esquema en entrada y salida |
| A09 Registro y monitoreo | Logs estructurados (pino) de cada venta, login y error del servidor |
| A10 SSRF | Sin fetch de URLs provistas por el usuario |

Adicional:
- **Rate limiting** global y reforzado en `/auth`.
- **Helmet** (cabeceras de seguridad HTTP) vía `@fastify/helmet`.
- **CSRF / Almacenamiento de tokens:** el **access token** se guarda en memoria del cliente (nunca en `localStorage`). El **refresh token** se guarda en una cookie `httpOnly + Secure + SameSite=Strict`: es inaccesible para JavaScript y resiste ataques XSS y CSRF. Los endpoints `/auth/login` y `/auth/refresh` emiten y leen esta cookie; el cliente no la maneja manualmente. La API de datos sigue siendo stateless con `Authorization: Bearer <access_token>` en cabecera.
- Sanitización de toda entrada de texto (nombres, direcciones) contra XSS al renderizar.

---

## 5. Manejo de errores

- **Backend:** clase de error tipada (`AppError` con `code`, `httpStatus`, `message`). El cliente recibe un JSON normalizado `{ error: { code, message } }`; el servidor registra el stack completo en logs. Nunca se filtra el stack al cliente.
- **Frontend:** `TanStack Query` captura errores de red; los formularios muestran el mensaje claro del backend; un error boundary evita pantallas en blanco.

---

## 6. Rendimiento

- Consultas con Prisma usando `include`/`select` explícitos → **sin problema N+1**.
- Paginación por cursor en listados grandes (catálogo, kardex, ventas).
- Índices en `barcode`, `reference`, `sale.created_at`, `product_variant.product_id`.
- Respuestas JSON normalizadas (sin datos redundantes anidados innecesarios).
- Cache de catálogo en la tienda con TanStack Query; invalidación al vender.

---

## 7. Formato regional (Colombia)

| Aspecto | Valor |
|---------|-------|
| Locale | `es-CO` |
| Moneda | COP, símbolo `$`, separador de miles `.`, sin decimales → `$ 45.000` |
| IVA | 19% configurable |
| Zona horaria | `America/Bogota` |
| Pasarela | Wompi (COP nativo) |
