314 lines
11 KiB
Markdown
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
|