# 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