Files
hackathon-opti-1a67c9077937…/backend/README.md
2026-05-23 12:24:52 -06:00

314 lines
11 KiB
Markdown

# Backend — OptiRuta
API REST en **Node.js + Express + TypeScript** con **Prisma 7** sobre **PostgreSQL**.
> Para el README general del proyecto, ver [../README.md](../README.md).
---
## Tabla de contenidos
- [Stack](#stack)
- [Estructura](#estructura)
- [Arquitectura Clean](#arquitectura-clean)
- [Variables de entorno](#variables-de-entorno)
- [Modelo de datos (Prisma)](#modelo-de-datos-prisma)
- [Cómo correr](#cómo-correr)
- [Módulos](#módulos)
- [El simulador](#el-simulador)
- [Reglas de arquitectura](#reglas-de-arquitectura)
- [Troubleshooting](#troubleshooting)
---
## Stack
- **TypeScript** estricto (`exactOptionalPropertyTypes: true`)
- **Node.js 20+**, modules ESM
- **Express 5** para HTTP
- **Prisma 7** (cliente generado en `src/generated/prisma`)
- **PostgreSQL 15** vía Docker Compose
- **jsonwebtoken** para JWT, **bcryptjs** para hashes
- **tsx** como dev runner (sin Babel, sin reload manual)
---
## Estructura
```
backend/
├── docker-compose.yml ← Postgres en 5433:5432
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── prisma.config.ts
└── src/
├── app.ts ← entry point: conecta DB, seed, lanza simulador, server
├── config/
│ ├── env.ts ← lectura de variables del .env
│ ├── jwt.ts ← JwtAdapter (sign / validate)
│ └── bcrypt.ts ← BcryptAdapter (hash / compare)
├── data/
│ ├── cache/ ← caches en memoria (notification-cache, route-state, inbox)
│ ├── mocks/ ← rutas, colonias, users de prueba
│ ├── postgres/ ← Prisma client singleton
│ ├── repositories/ ← impls reales (Prisma)
│ ├── seed/ ← seed del admin
│ └── simulation/ ← RouteSimulator (cron interno)
├── domain/
│ ├── dtos/ ← DTOs con validators estáticos
│ ├── errors/ ← CustomError
│ ├── repositories/ ← interfaces abstractas
│ └── use-cases/ ← lógica de negocio
└── presentation/
├── admin/ ← controller + routes
├── auth/
├── addresses/
├── feedback/
├── middlewares/ ← AuthMiddleware (validate + requireAdmin)
├── tracking/
├── routes.ts ← AppRoutes: une todos los módulos
└── server.ts ← clase Server (Express setup)
```
---
## Arquitectura Clean
```
HTTP request
presentation (Express controller)
domain (use-case + DTO validator)
data (repository impl / mock / cache)
```
### Reglas
1. El **controller** solo recibe `req`, llama al use-case, responde JSON, maneja errores con `handleError()`.
2. Los **use-cases** llevan toda la lógica de negocio.
3. Los **DTOs** validan datos de entrada con métodos estáticos `static validate(data: unknown)`.
4. Los **repositories** son interfaces en `domain/repositories/`; las implementaciones reales viven en `data/`.
5. Los **errores** se manejan con `CustomError`: `badRequest`, `unauthorized`, `forbidden`, `notFound`, `conflict`, `internalServer`.
6. JWT solo se maneja desde `JwtAdapter`. Bcrypt solo desde `BcryptAdapter`.
7. Variables de entorno solo en `config/env.ts` (excepción: `.env`).
---
## Variables de entorno
Archivo `.env` (no se sube al repo):
```dotenv
PORT=8080
NODE_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5433/optihack
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=optihack
JWT_SEED=your_super_secret_jwt_seed_here_change_in_production
JWT_EXPIRES_IN=7d
BCRYPT_ROUNDS=10
```
| Variable | Default | Notas |
|---|---|---|
| `PORT` | `3000` | Aquí usamos `8080` porque otra app ocupa 3000 |
| `DATABASE_URL` | — | Apunta a Postgres en 5433 (Docker mapping) |
| `JWT_SEED` | — | Requerido. Cambiar en producción |
| `JWT_EXPIRES_IN` | `7d` | Duración del token |
| `BCRYPT_ROUNDS` | `10` | Costo del hash |
| `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB` | — | Usados por `docker-compose.yml` |
---
## Modelo de datos (Prisma)
```prisma
enum UserRole { USER ADMIN }
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
phone String? @unique
role UserRole @default(USER)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
addresses Address[]
@@map("users")
}
model Address {
id Int @id @default(autoincrement())
userId Int
label String
street String
neighborhood String?
postalCode String?
latitude Float?
longitude Float?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("addresses")
}
```
> En el MVP solo usamos `User`. `Address` está modelada pero los domicilios se guardan en mocks por ahora — se puede migrar sin tocar los controllers.
---
## Cómo correr
### 1. Postgres con Docker
```powershell
docker compose up -d
docker ps # verifica que postgres:15.3 esté Up
```
### 2. Migraciones + cliente
```powershell
npm install
npx prisma migrate deploy
npx prisma generate
```
### 3. Dev
```powershell
npm run dev
```
Output esperado:
```
Database connected
Seed admin: creado admin@test.com con password "admin123" (cambiar en producción)
RouteSimulator started — tick every 30s
Server is running on port 8080
```
### 4. Production build
```powershell
npm run build # compila a dist/
npm start # corre node dist/app.js
```
### Scripts disponibles
| Script | Hace |
|---|---|
| `npm run dev` | `tsx watch src/app.ts` — hot reload |
| `npm run build` | `tsc` — emite a `dist/` |
| `npm start` | `node dist/app.js` |
---
## Módulos
### Auth (`/api/auth/*`)
- **Registro**: hashea password con `BcryptAdapter`, crea con Prisma, agrega al mock de service con `routeId` default = `RUTA-01`. Siempre rol `USER`.
- **Login**: compara password con bcrypt, genera JWT con `JwtAdapter.generate({ id, email })`. El login response incluye el `role` del DB.
- **GetMe**: protegido con `AuthMiddleware.validate`, devuelve datos del user logueado.
- Si un user legacy no estaba en el mock, el login lo agrega (idempotente con `upsertUser`).
### Tracking (`/api/tracking/*`)
- **POST `/gps-update`**: punto de entrada del simulador (o admin manual). Calcula ETA, evalúa notificaciones, escribe en `route-state` + `inbox`. Respeta cancelación.
- **GET `/status`**: protegido. Devuelve la **visión de túnel** del user — solo su ruta, su ETA, sus notificaciones.
- **POST `/reset-demo`**: limpia caches y estados en memoria sin reiniciar el server.
### Direcciones (`/api/addresses/*`)
- **GET `/colonias`**: catálogo público (mock).
- **GET `/me`**: dirección actual del user.
- **PUT `/me`**: cambia la colonia del user. Valida contra el catálogo; rechaza colonias inexistentes (cumple "validación de zona permitida" del reto).
### Feedback (`/api/feedback/*`)
- **POST**: envía feedback con tipo + mensaje + rating opcional. Tipos: `TRUCK_DID_NOT_PASS`, `RATING`, `SUGGESTION`, `OTHER`.
- **GET `/me`**: feedbacks del user logueado (RBAC).
### Admin (`/api/admin/*`)
Todos requieren `AuthMiddleware.requireAdmin`.
- **GET `/routes`**: lista todas las rutas con estado actual (status, currentPositionId, arrivalResult, cancelled).
- **POST `/routes/:routeId/cancel`**: cancela la ruta. El simulador la pausa. Los users de esa ruta reciben notificación.
- **POST `/routes/:routeId/resume`**: reanuda. Simulator vuelve a positionId 1 y se limpia el dedup cache.
---
## El simulador
`data/simulation/route-simulator.ts`
- Corre `setInterval` cada **30 segundos**
- Solo simula `RUTA-01` por defecto (configurable en `SIMULATED_ROUTE_IDS`)
- Cada tick: avanza el positionIndex de la ruta, dispara `ProcessGpsUpdateUseCase` con la posición correspondiente del mock
- Cuando el ciclo se completa (positionIndex wrap a 0), limpia el dedup cache para que la siguiente vuelta vuelva a emitir notificaciones
- Respeta el flag `cancelled` del estado de ruta: si está cancelada, no avanza
- El admin puede llamar `resetPosition(routeId)` para volver el ciclo a 0
```
positionId 1 → (sin notif, salida del relleno)
positionId 2 → ROUTE_START "¡Ruta Iniciada!"
positionId 3 → (sin notif, en camino)
positionId 4 → TRUCK_PROXIMITY "Camión Cercano" (≈ 12 min de llegada)
positionId 5 → TRUCK_ARRIVED "El camión ya está aquí" (ETA = 0, arrivalResult = ARRIVED)
positionId 8 → ROUTE_COMPLETED "Servicio Finalizado"
wrap → cache cleared, loop back
```
---
## Reglas de arquitectura
Las reglas que mantenemos a lo largo del proyecto:
1. **No lógica pesada en controllers.** Solo: recibir, validar, llamar use-case, responder.
2. **Lógica de negocio en use-cases.**
3. **Prisma solo en `data/repositories` o `data/postgres`.**
4. **Use-cases dependen de interfaces** (`domain/repositories/*`), nunca de Prisma.
5. **DTOs validan entrada** con `static validate()`.
6. **Errores con `CustomError`**.
7. **JWT solo via `JwtAdapter`. Bcrypt solo via `BcryptAdapter`**.
8. **No regresar passwords en ninguna respuesta**.
9. **Las rutas en `presentation`**. Middlewares en `presentation/middlewares`.
10. **Variables de entorno solo en `config/env.ts`**.
11. **No editar archivos generados** (`src/generated/prisma/*`).
---
## Troubleshooting
| Síntoma | Causa | Fix |
|---|---|---|
| `Can't reach database server at localhost:5432` | DATABASE_URL apunta a 5432 pero Docker mapea 5433 | Cambiar puerto en `.env` a `5433` |
| `Prisma client not found` | Cliente no generado | `npx prisma generate` |
| Server arranca pero `/api/...` da 404 | Olvidaste registrar la ruta en `presentation/routes.ts` | Agregar `router.use('/api/X', XRoutes.routes)` |
| Login devuelve 400 con error de Prisma | Tabla `users` no existe | `npx prisma migrate deploy` |
| Admin tab no aparece en el front | Login devolvió role distinto a "ADMIN" | Verifica que el seed haya corrido (mira el log de arranque) |
| Notificaciones repetidas no salen | Dedup cache | Esperar al wrap del simulador o llamar `POST /api/tracking/reset-demo` |
---
## Para extender
- Añadir nuevo módulo X: copia la estructura de `feedback/` (DTO → repo → use-case → controller → route → registrar en `AppRoutes`)
- Migrar un mock a DB real: cambia solo la **impl** en `data/`, las interfaces y use-cases no se tocan
- Conectar Redis: nueva impl de `NotificationCacheRepository` y `RouteStateRepository`, cambia el `new InMemory...` por `new Redis...` en el controller