diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index c743b02..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# Server -PORT=3000 -NODE_ENV=development - -# Database -DATABASE_URL=postgresql://user:password@localhost:5432/optihack - -# JWT -JWT_SEED=your_super_secret_jwt_seed_here_change_in_production -JWT_EXPIRES_IN=7d - -# Bcrypt -BCRYPT_ROUNDS=10 diff --git a/backend/src/app.ts b/backend/src/app.ts index 40fd2ec..c29f054 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,10 +1,4 @@ -/** - * app.ts - * Punto de entrada de la aplicación. - * - Inicia el servidor - * - Conecta a la base de datos - * - Maneja errores globales - */ + import "dotenv/config"; import { env } from "./config/env.js"; @@ -14,11 +8,10 @@ import { prisma } from "./data/postgres/index.js"; async function main() { try { - // Verificar conexión a la base de datos await prisma.$connect(); - console.log("✓ Database connected"); + console.log("Database connected"); + - // Crear y iniciar el servidor const server = new Server({ port: env.PORT, routes: AppRoutes.routes, @@ -26,7 +19,7 @@ async function main() { await server.start(); } catch (error) { - console.error("✗ Error starting application:", error); + console.error(" Error starting application:", error); process.exit(1); } } diff --git a/backend/src/config/bcrypt.ts b/backend/src/config/bcrypt.ts index bcc6e62..53c4297 100644 --- a/backend/src/config/bcrypt.ts +++ b/backend/src/config/bcrypt.ts @@ -1,8 +1,4 @@ -/** - * bcrypt.ts - * BcryptAdapter: hashea y compara contraseñas. - * Encapsula toda la lógica de bcrypt en un lugar. - */ + import bcrypt from "bcryptjs"; import { env } from "./env.js"; diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts index b599db4..62a1d0e 100644 --- a/backend/src/config/env.ts +++ b/backend/src/config/env.ts @@ -1,8 +1,4 @@ -/** - * env.ts - * Lee y valida las variables de entorno. - * Centraliza toda la configuración que viene del .env - */ + const getEnvVar = (key: string, defaultValue?: string): string => { const value = process.env[key]; diff --git a/backend/src/data/cache/notification-cache.impl.ts b/backend/src/data/cache/notification-cache.impl.ts new file mode 100644 index 0000000..0f09cba --- /dev/null +++ b/backend/src/data/cache/notification-cache.impl.ts @@ -0,0 +1,17 @@ +import type { NotificationCacheRepository } from "../../domain/repositories/notification-cache.repository.js"; + +export class InMemoryNotificationCache implements NotificationCacheRepository { + private readonly sent = new Set(); + + async wasSent(key: string): Promise { + return this.sent.has(key); + } + + async markSent(key: string): Promise { + this.sent.add(key); + } + + async clear(): Promise { + this.sent.clear(); + } +} \ No newline at end of file diff --git a/backend/src/data/mocks/colonias.mock.ts b/backend/src/data/mocks/colonias.mock.ts new file mode 100644 index 0000000..04037c0 --- /dev/null +++ b/backend/src/data/mocks/colonias.mock.ts @@ -0,0 +1,17 @@ + + +export interface Colonia { + colonia: string; + routeId: string; + horarioEstimado: string; +} + +export const colonias: Colonia[] = [ + { colonia: "Zona Centro", routeId: "RUTA-01", horarioEstimado: "Matutino (06:30 - 07:15)" }, + { colonia: "Las Arboledas", routeId: "RUTA-01", horarioEstimado: "Matutino (07:00 - 07:30)" }, + { colonia: "Trojes", routeId: "RUTA-13", horarioEstimado: "Matutino (06:40 - 07:10)" }, + { colonia: "San Juanico", routeId: "RUTA-03", horarioEstimado: "Matutino (06:45 - 07:15)" }, + { colonia: "Los Olivos", routeId: "RUTA-04", horarioEstimado: "Matutino (07:00 - 07:40)" }, + { colonia: "Rancho Seco", routeId: "RUTA-05", horarioEstimado: "Vespertino (14:15 - 15:00)" }, + { colonia: "Las Insurgentes", routeId: "RUTA-12", horarioEstimado: "Matutino (06:35 - 07:10)" }, +]; \ No newline at end of file diff --git a/backend/src/data/mocks/notification-types.mock.ts b/backend/src/data/mocks/notification-types.mock.ts new file mode 100644 index 0000000..3d752ce --- /dev/null +++ b/backend/src/data/mocks/notification-types.mock.ts @@ -0,0 +1,47 @@ + + +export const NotificationType = { + ROUTE_START: "ROUTE_START", + TRUCK_PROXIMITY: "TRUCK_PROXIMITY", + ROUTE_COMPLETED: "ROUTE_COMPLETED", + DELAY: "DELAY", + MECHANICAL_FAILURE: "MECHANICAL_FAILURE", +} as const; + +export type NotificationType = + (typeof NotificationType)[keyof typeof NotificationType]; + +export interface NotificationPayload { + title: string; + body: string; +} + +export const notificationPayloads: Record = { + ROUTE_START: { + title: "¡Ruta Iniciada!", + body: "El camión recolector ha salido del Relleno Sanitario rumbo a tu sector. Asegúrate de tener listos tus residuos.", + }, + TRUCK_PROXIMITY: { + title: "Camión Cercano", + body: "El camión está a menos de 15 minutos de tu domicilio. Es momento de sacar tus bolsas a la acera.", + }, + ROUTE_COMPLETED: { + title: "Servicio Finalizado", + body: "El camión de tu sector ha concluido su jornada de recolección diaria.", + }, + DELAY: { + title: "Retraso en la ruta", + body: "La ruta presenta retraso por tráfico. Te avisaremos cuando el camión esté cerca.", + }, + MECHANICAL_FAILURE: { + title: "Falla mecánica", + body: "El camión de tu sector presenta una falla mecánica. La recolección puede retrasarse.", + }, +}; + + +export const positionTriggers: Record = { + 2: NotificationType.ROUTE_START, + 4: NotificationType.TRUCK_PROXIMITY, + 8: NotificationType.ROUTE_COMPLETED, +}; \ No newline at end of file diff --git a/backend/src/data/mocks/routes.mock.ts b/backend/src/data/mocks/routes.mock.ts new file mode 100644 index 0000000..1a61b93 --- /dev/null +++ b/backend/src/data/mocks/routes.mock.ts @@ -0,0 +1,259 @@ + +export interface RoutePosition { + positionId: number; + lat: number; + lng: number; + speed: number; + timestamp: string; +} + +export interface MockRoute { + routeId: string; + name: string; + truckId: number; + status: "EN_RUTA" | "DETENIDO" | "FINALIZADO" | "FALLA"; + positions: RoutePosition[]; +} + +export const routes: MockRoute[] = [ + { + routeId: "RUTA-01", + name: "Zona Centro - Las Arboledas", + truckId: 101, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:00:00Z" }, + { positionId: 2, lat: 20.5185, lng: -100.8450, speed: 45, timestamp: "2026-05-22T06:12:00Z" }, + { positionId: 3, lat: 20.5215, lng: -100.8142, speed: 22, timestamp: "2026-05-22T06:25:00Z" }, + { positionId: 4, lat: 20.5212, lng: -100.8175, speed: 15, timestamp: "2026-05-22T06:38:00Z" }, + { positionId: 5, lat: 20.5210, lng: -100.8210, speed: 0, timestamp: "2026-05-22T06:50:00Z" }, + { positionId: 6, lat: 20.5235, lng: -100.8212, speed: 18, timestamp: "2026-05-22T07:05:00Z" }, + { positionId: 7, lat: 20.5260, lng: -100.8215, speed: 20, timestamp: "2026-05-22T07:18:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 40, timestamp: "2026-05-22T07:40:00Z" }, + ], + }, + { + routeId: "RUTA-02", + name: "Sector Norte - Av. Tecnológico", + truckId: 102, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:05:00Z" }, + { positionId: 2, lat: 20.5280, lng: -100.8135, speed: 38, timestamp: "2026-05-22T06:18:00Z" }, + { positionId: 3, lat: 20.5410, lng: -100.8130, speed: 25, timestamp: "2026-05-22T06:30:00Z" }, + { positionId: 4, lat: 20.5445, lng: -100.8132, speed: 12, timestamp: "2026-05-22T06:45:00Z" }, + { positionId: 5, lat: 20.5480, lng: -100.8135, speed: 0, timestamp: "2026-05-22T06:58:00Z" }, + { positionId: 6, lat: 20.5515, lng: -100.8138, speed: 15, timestamp: "2026-05-22T07:10:00Z" }, + { positionId: 7, lat: 20.5540, lng: -100.8110, speed: 22, timestamp: "2026-05-22T07:25:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 45, timestamp: "2026-05-22T07:50:00Z" }, + ], + }, + { + routeId: "RUTA-03", + name: "Sector Poniente - San Juanico", + truckId: 103, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:10:00Z" }, + { positionId: 2, lat: 20.5250, lng: -100.8510, speed: 42, timestamp: "2026-05-22T06:20:00Z" }, + { positionId: 3, lat: 20.5290, lng: -100.8320, speed: 20, timestamp: "2026-05-22T06:35:00Z" }, + { positionId: 4, lat: 20.5315, lng: -100.8355, speed: 15, timestamp: "2026-05-22T06:48:00Z" }, + { positionId: 5, lat: 20.5340, lng: -100.8390, speed: 0, timestamp: "2026-05-22T07:00:00Z" }, + { positionId: 6, lat: 20.5362, lng: -100.8425, speed: 10, timestamp: "2026-05-22T07:15:00Z" }, + { positionId: 7, lat: 20.5330, lng: -100.8430, speed: 18, timestamp: "2026-05-22T07:28:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 35, timestamp: "2026-05-22T07:45:00Z" }, + ], + }, + { + routeId: "RUTA-04", + name: "Oriente - Los Olivos", + truckId: 104, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:15:00Z" }, + { positionId: 2, lat: 20.5260, lng: -100.8010, speed: 45, timestamp: "2026-05-22T06:30:00Z" }, + { positionId: 3, lat: 20.5295, lng: -100.7890, speed: 24, timestamp: "2026-05-22T06:45:00Z" }, + { positionId: 4, lat: 20.5320, lng: -100.7850, speed: 12, timestamp: "2026-05-22T06:58:00Z" }, + { positionId: 5, lat: 20.5350, lng: -100.7790, speed: 0, timestamp: "2026-05-22T07:12:00Z" }, + { positionId: 6, lat: 20.5310, lng: -100.7760, speed: 15, timestamp: "2026-05-22T07:25:00Z" }, + { positionId: 7, lat: 20.5270, lng: -100.7820, speed: 26, timestamp: "2026-05-22T07:38:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 48, timestamp: "2026-05-22T07:58:00Z" }, + ], + }, + { + routeId: "RUTA-05", + name: "Sector Sur - Rancho Seco", + truckId: 105, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:20:00Z" }, + { positionId: 2, lat: 20.5050, lng: -100.8620, speed: 35, timestamp: "2026-05-22T06:32:00Z" }, + { positionId: 3, lat: 20.5020, lng: -100.8350, speed: 22, timestamp: "2026-05-22T06:45:00Z" }, + { positionId: 4, lat: 20.4995, lng: -100.8210, speed: 14, timestamp: "2026-05-22T06:58:00Z" }, + { positionId: 5, lat: 20.4970, lng: -100.8150, speed: 0, timestamp: "2026-05-22T07:10:00Z" }, + { positionId: 6, lat: 20.5010, lng: -100.8120, speed: 16, timestamp: "2026-05-22T07:22:00Z" }, + { positionId: 7, lat: 20.5060, lng: -100.8160, speed: 25, timestamp: "2026-05-22T07:35:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 40, timestamp: "2026-05-22T07:55:00Z" }, + ], + }, + { + routeId: "RUTA-06", + name: "Norte Extremo - Rumbos de Roque", + truckId: 106, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:00:00Z" }, + { positionId: 2, lat: 20.5380, lng: -100.8380, speed: 40, timestamp: "2026-05-22T06:15:00Z" }, + { positionId: 3, lat: 20.5610, lng: -100.8370, speed: 30, timestamp: "2026-05-22T06:30:00Z" }, + { positionId: 4, lat: 20.5750, lng: -100.8360, speed: 15, timestamp: "2026-05-22T06:45:00Z" }, + { positionId: 5, lat: 20.5820, lng: -100.8350, speed: 0, timestamp: "2026-05-22T07:00:00Z" }, + { positionId: 6, lat: 20.5780, lng: -100.8310, speed: 20, timestamp: "2026-05-22T07:15:00Z" }, + { positionId: 7, lat: 20.5650, lng: -100.8320, speed: 28, timestamp: "2026-05-22T07:30:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 45, timestamp: "2026-05-22T07:55:00Z" }, + ], + }, + { + routeId: "RUTA-07", + name: "Nororiente - Ciudad Industrial", + truckId: 107, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:10:00Z" }, + { positionId: 2, lat: 20.5350, lng: -100.8050, speed: 44, timestamp: "2026-05-22T06:24:00Z" }, + { positionId: 3, lat: 20.5450, lng: -100.7950, speed: 25, timestamp: "2026-05-22T06:38:00Z" }, + { positionId: 4, lat: 20.5480, lng: -100.7850, speed: 18, timestamp: "2026-05-22T06:52:00Z" }, + { positionId: 5, lat: 20.5510, lng: -100.7750, speed: 0, timestamp: "2026-05-22T07:05:00Z" }, + { positionId: 6, lat: 20.5460, lng: -100.7720, speed: 12, timestamp: "2026-05-22T07:18:00Z" }, + { positionId: 7, lat: 20.5390, lng: -100.7820, speed: 30, timestamp: "2026-05-22T07:30:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 42, timestamp: "2026-05-22T07:52:00Z" }, + ], + }, + { + routeId: "RUTA-08", + name: "Suroriente - Universidad Latina", + truckId: 108, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:15:00Z" }, + { positionId: 2, lat: 20.5180, lng: -100.8310, speed: 38, timestamp: "2026-05-22T06:28:00Z" }, + { positionId: 3, lat: 20.5245, lng: -100.7980, speed: 30, timestamp: "2026-05-22T06:42:00Z" }, + { positionId: 4, lat: 20.5210, lng: -100.7995, speed: 14, timestamp: "2026-05-22T06:55:00Z" }, + { positionId: 5, lat: 20.5175, lng: -100.8010, speed: 0, timestamp: "2026-05-22T07:08:00Z" }, + { positionId: 6, lat: 20.5140, lng: -100.8030, speed: 18, timestamp: "2026-05-22T07:20:00Z" }, + { positionId: 7, lat: 20.5110, lng: -100.8055, speed: 22, timestamp: "2026-05-22T07:32:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 40, timestamp: "2026-05-22T07:54:00Z" }, + ], + }, + { + routeId: "RUTA-09", + name: "Poniente - Hospital General", + truckId: 109, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:02:00Z" }, + { positionId: 2, lat: 20.5210, lng: -100.8650, speed: 45, timestamp: "2026-05-22T06:12:00Z" }, + { positionId: 3, lat: 20.5260, lng: -100.8520, speed: 26, timestamp: "2026-05-22T06:24:00Z" }, + { positionId: 4, lat: 20.5275, lng: -100.8490, speed: 12, timestamp: "2026-05-22T06:36:00Z" }, + { positionId: 5, lat: 20.5285, lng: -100.8460, speed: 0, timestamp: "2026-05-22T06:48:00Z" }, + { positionId: 6, lat: 20.5250, lng: -100.8470, speed: 15, timestamp: "2026-05-22T07:00:00Z" }, + { positionId: 7, lat: 20.5220, lng: -100.8550, speed: 32, timestamp: "2026-05-22T07:12:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 44, timestamp: "2026-05-22T07:30:00Z" }, + ], + }, + { + routeId: "RUTA-10", + name: "Eje Juan Pablo II - Sede UG Sur", + truckId: 110, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:22:00Z" }, + { positionId: 2, lat: 20.5015, lng: -100.8520, speed: 40, timestamp: "2026-05-22T06:34:00Z" }, + { positionId: 3, lat: 20.4990, lng: -100.8390, speed: 28, timestamp: "2026-05-22T06:46:00Z" }, + { positionId: 4, lat: 20.4950, lng: -100.8320, speed: 18, timestamp: "2026-05-22T06:58:00Z" }, + { positionId: 5, lat: 20.4920, lng: -100.8280, speed: 0, timestamp: "2026-05-22T07:10:00Z" }, + { positionId: 6, lat: 20.4945, lng: -100.8240, speed: 14, timestamp: "2026-05-22T07:22:00Z" }, + { positionId: 7, lat: 20.4980, lng: -100.8300, speed: 30, timestamp: "2026-05-22T07:34:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 38, timestamp: "2026-05-22T07:52:00Z" }, + ], + }, + { + routeId: "RUTA-11", + name: "Zona de Oro - Torres Landa", + truckId: 111, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:04:00Z" }, + { positionId: 2, lat: 20.5240, lng: -100.8350, speed: 36, timestamp: "2026-05-22T06:16:00Z" }, + { positionId: 3, lat: 20.5280, lng: -100.8250, speed: 22, timestamp: "2026-05-22T06:29:00Z" }, + { positionId: 4, lat: 20.5295, lng: -100.8210, speed: 10, timestamp: "2026-05-22T06:42:00Z" }, + { positionId: 5, lat: 20.5310, lng: -100.8170, speed: 0, timestamp: "2026-05-22T06:55:00Z" }, + { positionId: 6, lat: 20.5290, lng: -100.8140, speed: 16, timestamp: "2026-05-22T07:08:00Z" }, + { positionId: 7, lat: 20.5260, lng: -100.8220, speed: 28, timestamp: "2026-05-22T07:21:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 42, timestamp: "2026-05-22T07:42:00Z" }, + ], + }, + { + routeId: "RUTA-12", + name: "Nororiente - Las Insurgentes", + truckId: 112, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:08:00Z" }, + { positionId: 2, lat: 20.5280, lng: -100.8080, speed: 40, timestamp: "2026-05-22T06:22:00Z" }, + { positionId: 3, lat: 20.5320, lng: -100.7980, speed: 24, timestamp: "2026-05-22T06:35:00Z" }, + { positionId: 4, lat: 20.5340, lng: -100.7940, speed: 15, timestamp: "2026-05-22T06:48:00Z" }, + { positionId: 5, lat: 20.5360, lng: -100.7900, speed: 0, timestamp: "2026-05-22T07:00:00Z" }, + { positionId: 6, lat: 20.5310, lng: -100.7920, speed: 12, timestamp: "2026-05-22T07:12:00Z" }, + { positionId: 7, lat: 20.5270, lng: -100.8020, speed: 26, timestamp: "2026-05-22T07:25:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 44, timestamp: "2026-05-22T07:48:00Z" }, + ], + }, + { + routeId: "RUTA-13", + name: "Sector Norte - Trojes e Irrigación", + truckId: 113, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:12:00Z" }, + { positionId: 2, lat: 20.5360, lng: -100.8190, speed: 35, timestamp: "2026-05-22T06:26:00Z" }, + { positionId: 3, lat: 20.5420, lng: -100.8080, speed: 28, timestamp: "2026-05-22T06:40:00Z" }, + { positionId: 4, lat: 20.5440, lng: -100.8040, speed: 14, timestamp: "2026-05-22T06:54:00Z" }, + { positionId: 5, lat: 20.5460, lng: -100.8000, speed: 0, timestamp: "2026-05-22T07:06:00Z" }, + { positionId: 6, lat: 20.5410, lng: -100.8020, speed: 18, timestamp: "2026-05-22T07:18:00Z" }, + { positionId: 7, lat: 20.5370, lng: -100.8120, speed: 25, timestamp: "2026-05-22T07:30:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 39, timestamp: "2026-05-22T07:54:00Z" }, + ], + }, + { + routeId: "RUTA-14", + name: "Sur Poniente - La Toscana", + truckId: 114, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:16:00Z" }, + { positionId: 2, lat: 20.5150, lng: -100.8580, speed: 42, timestamp: "2026-05-22T06:28:00Z" }, + { positionId: 3, lat: 20.5140, lng: -100.8390, speed: 26, timestamp: "2026-05-22T06:41:00Z" }, + { positionId: 4, lat: 20.5125, lng: -100.8310, speed: 16, timestamp: "2026-05-22T06:54:00Z" }, + { positionId: 5, lat: 20.5110, lng: -100.8250, speed: 0, timestamp: "2026-05-22T07:06:00Z" }, + { positionId: 6, lat: 20.5135, lng: -100.8280, speed: 12, timestamp: "2026-05-22T07:18:00Z" }, + { positionId: 7, lat: 20.5160, lng: -100.8420, speed: 32, timestamp: "2026-05-22T07:30:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 45, timestamp: "2026-05-22T07:51:00Z" }, + ], + }, + { + routeId: "RUTA-15", + name: "Norponiente - Camino a San José de Celaya", + truckId: 115, + status: "EN_RUTA", + positions: [ + { positionId: 1, lat: 20.5111, lng: -100.9037, speed: 0, timestamp: "2026-05-22T06:18:00Z" }, + { positionId: 2, lat: 20.5320, lng: -100.8590, speed: 38, timestamp: "2026-05-22T06:31:00Z" }, + { positionId: 3, lat: 20.5390, lng: -100.8480, speed: 24, timestamp: "2026-05-22T06:44:00Z" }, + { positionId: 4, lat: 20.5420, lng: -100.8440, speed: 15, timestamp: "2026-05-22T06:57:00Z" }, + { positionId: 5, lat: 20.5450, lng: -100.8410, speed: 0, timestamp: "2026-05-22T07:09:00Z" }, + { positionId: 6, lat: 20.5410, lng: -100.8430, speed: 14, timestamp: "2026-05-22T07:21:00Z" }, + { positionId: 7, lat: 20.5360, lng: -100.8520, speed: 28, timestamp: "2026-05-22T07:33:00Z" }, + { positionId: 8, lat: 20.5111, lng: -100.9037, speed: 41, timestamp: "2026-05-22T07:54:00Z" }, + ], + }, +]; \ No newline at end of file diff --git a/backend/src/data/mocks/users.mock.ts b/backend/src/data/mocks/users.mock.ts new file mode 100644 index 0000000..1627414 --- /dev/null +++ b/backend/src/data/mocks/users.mock.ts @@ -0,0 +1,19 @@ + + +export interface MockUser { + id: number; + name: string; + email: string; + colonia: string; + routeId: string; +} + +export const users: MockUser[] = [ + { id: 1, name: "Ana López", email: "ana@test.com", colonia: "Zona Centro", routeId: "RUTA-01" }, + { id: 2, name: "Carlos Pérez", email: "carlos@test.com",colonia: "Las Arboledas", routeId: "RUTA-01" }, + { id: 3, name: "María Ruiz", email: "maria@test.com", colonia: "Trojes", routeId: "RUTA-13" }, + { id: 4, name: "Jorge Martínez", email: "jorge@test.com", colonia: "San Juanico", routeId: "RUTA-03" }, + { id: 5, name: "Laura Díaz", email: "laura@test.com", colonia: "Los Olivos", routeId: "RUTA-04" }, + { id: 6, name: "Pedro Sánchez", email: "pedro@test.com", colonia: "Rancho Seco", routeId: "RUTA-05" }, + { id: 7, name: "Sofía Gómez", email: "sofia@test.com", colonia: "Las Insurgentes", routeId: "RUTA-12" }, +]; \ No newline at end of file diff --git a/backend/src/domain/dtos/auth/login-user.dto.ts b/backend/src/domain/dtos/auth/login-user.dto.ts index 9965c85..6de2ceb 100644 --- a/backend/src/domain/dtos/auth/login-user.dto.ts +++ b/backend/src/domain/dtos/auth/login-user.dto.ts @@ -1,8 +1,4 @@ -/** - * login-user.dto.ts - * DTO para iniciar sesión. - * Define qué datos esperamos al hacer login. - */ + export interface LoginUserDto { email: string; diff --git a/backend/src/domain/dtos/auth/register-user.dto.ts b/backend/src/domain/dtos/auth/register-user.dto.ts index 3266f8b..c2d2942 100644 --- a/backend/src/domain/dtos/auth/register-user.dto.ts +++ b/backend/src/domain/dtos/auth/register-user.dto.ts @@ -1,8 +1,4 @@ -/** - * register-user.dto.ts - * DTO para registrar un usuario. - * Define qué datos esperamos al registrar. - */ + export interface RegisterUserDto { name: string; diff --git a/backend/src/domain/dtos/index.ts b/backend/src/domain/dtos/index.ts index 8243531..8808ec7 100644 --- a/backend/src/domain/dtos/index.ts +++ b/backend/src/domain/dtos/index.ts @@ -1,2 +1,3 @@ export * from './auth/register-user.dto.js'; -export * from './auth/login-user.dto.js'; \ No newline at end of file +export * from './auth/login-user.dto.js'; +export * from './tracking/gps-update.dto.js'; \ No newline at end of file diff --git a/backend/src/domain/dtos/tracking/gps-update.dto.ts b/backend/src/domain/dtos/tracking/gps-update.dto.ts new file mode 100644 index 0000000..d6de60a --- /dev/null +++ b/backend/src/domain/dtos/tracking/gps-update.dto.ts @@ -0,0 +1,68 @@ + + +export type TruckStatus = "EN_RUTA" | "DETENIDO" | "FINALIZADO" | "FALLA"; + +export interface GpsUpdateDto { + truckId: string; + routeId: string; + lat: number; + lng: number; + speed: number; + status: TruckStatus; + positionId: number | undefined; + timestamp: string | undefined; +} + +const VALID_STATUSES: TruckStatus[] = ["EN_RUTA", "DETENIDO", "FINALIZADO", "FALLA"]; + +export class GpsUpdateDtoValidator { + static validate(data: unknown): GpsUpdateDto { + if (typeof data !== "object" || data === null) { + throw new Error("Invalid GPS update payload"); + } + + const obj = data as Record; + + const truckId = obj.truckId; + if (typeof truckId !== "string" && typeof truckId !== "number") { + throw new Error("truckId is required"); + } + + if (typeof obj.routeId !== "string" || obj.routeId.trim().length === 0) { + throw new Error("routeId is required"); + } + + if (typeof obj.lat !== "number" || Number.isNaN(obj.lat)) { + throw new Error("lat is required and must be numeric"); + } + + if (typeof obj.lng !== "number" || Number.isNaN(obj.lng)) { + throw new Error("lng is required and must be numeric"); + } + + const speed = + typeof obj.speed === "number" && !Number.isNaN(obj.speed) ? obj.speed : 0; + + const status: TruckStatus = + typeof obj.status === "string" && VALID_STATUSES.includes(obj.status as TruckStatus) + ? (obj.status as TruckStatus) + : "EN_RUTA"; + + const positionId = + typeof obj.positionId === "number" ? obj.positionId : undefined; + + const timestamp = + typeof obj.timestamp === "string" ? obj.timestamp : undefined; + + return { + truckId: String(truckId), + routeId: obj.routeId.trim(), + lat: obj.lat, + lng: obj.lng, + speed, + status, + positionId, + timestamp, + }; + } +} \ No newline at end of file diff --git a/backend/src/domain/errors/custom.error.ts b/backend/src/domain/errors/custom.error.ts index 4315215..06bcb6b 100644 --- a/backend/src/domain/errors/custom.error.ts +++ b/backend/src/domain/errors/custom.error.ts @@ -1,8 +1,4 @@ -/** - * custom.error.ts - * CustomError: clase base para errores personalizados. - * Facilita el manejo de errores en toda la aplicación. - */ + export class CustomError extends Error { constructor( diff --git a/backend/src/domain/repositories/auth.repository.ts b/backend/src/domain/repositories/auth.repository.ts index b89b37d..7fc9865 100644 --- a/backend/src/domain/repositories/auth.repository.ts +++ b/backend/src/domain/repositories/auth.repository.ts @@ -1,8 +1,4 @@ -/** - * auth.repository.ts - * AuthRepository (interfaz): define qué métodos debe tener el repositorio de auth. - * El use-case usa esta interfaz sin saber cómo se implementa. - */ + export interface AuthRepository { findByEmail(email: string): Promise<{ id: number; email: string; password: string; name: string } | null>; diff --git a/backend/src/domain/repositories/notification-cache.repository.ts b/backend/src/domain/repositories/notification-cache.repository.ts new file mode 100644 index 0000000..edc576d --- /dev/null +++ b/backend/src/domain/repositories/notification-cache.repository.ts @@ -0,0 +1,12 @@ + + +export interface NotificationCacheRepository { + + wasSent(key: string): Promise; + + + markSent(key: string): Promise; + + + clear(): Promise; +} \ No newline at end of file diff --git a/backend/src/domain/use-cases/auth/get-me.use-case.ts b/backend/src/domain/use-cases/auth/get-me.use-case.ts index dbc73d0..60d7a1d 100644 --- a/backend/src/domain/use-cases/auth/get-me.use-case.ts +++ b/backend/src/domain/use-cases/auth/get-me.use-case.ts @@ -1,9 +1,4 @@ -/** - * get-me.use-case.ts - * GetMeUseCase: obtiene los datos del usuario autenticado. - * - Busca el usuario por ID (que viene del JWT) - * - Retorna los datos del usuario - */ + import { AuthRepository } from "../../repositories/auth.repository.js"; import { CustomError } from "../../errors/custom.error.js"; diff --git a/backend/src/domain/use-cases/auth/login-user.use-case.ts b/backend/src/domain/use-cases/auth/login-user.use-case.ts index 1cd838c..387c7cc 100644 --- a/backend/src/domain/use-cases/auth/login-user.use-case.ts +++ b/backend/src/domain/use-cases/auth/login-user.use-case.ts @@ -1,11 +1,4 @@ -/** - * login-user.use-case.ts - * LoginUserUseCase: lógica para iniciar sesión. - * - Valida datos - * - Busca el usuario por email - * - Compara las contraseñas - * - Genera el JWT - */ + import { LoginUserDto, LoginUserDtoValidator } from "../../dtos/index.js"; import { AuthRepository } from "../../repositories/auth.repository.js"; diff --git a/backend/src/domain/use-cases/auth/register-user.use-case.ts b/backend/src/domain/use-cases/auth/register-user.use-case.ts index cdd649c..29fbacf 100644 --- a/backend/src/domain/use-cases/auth/register-user.use-case.ts +++ b/backend/src/domain/use-cases/auth/register-user.use-case.ts @@ -1,12 +1,4 @@ -/** - * register-user.use-case.ts - * RegisterUserUseCase: lógica para registrar un usuario. - * - Valida datos - * - Verifica si el email ya existe - * - Hashea la contraseña - * - Crea el usuario - * - Genera el JWT - */ + import { RegisterUserDto, RegisterUserDtoValidator } from "../../dtos/index.js"; import { AuthRepository } from "../../repositories/auth.repository.js"; diff --git a/backend/src/domain/use-cases/notifications/calculate-eta.use-case.ts b/backend/src/domain/use-cases/notifications/calculate-eta.use-case.ts new file mode 100644 index 0000000..31083da --- /dev/null +++ b/backend/src/domain/use-cases/notifications/calculate-eta.use-case.ts @@ -0,0 +1,67 @@ +import { CustomError } from "../../errors/custom.error.js"; +import { routes } from "../../../data/mocks/routes.mock.js"; +import { GpsUpdateDto } from "../../dtos/tracking/gps-update.dto.js"; + +export interface EtaResult { + etaMinutes: number; + arrivalWindow: { from: string; to: string }; + message: string; +} + +const PROXIMITY_POSITION_ID = 4; +const WINDOW_MARGIN_MIN = 5; + +export class CalculateEtaUseCase { + execute(gps: GpsUpdateDto): EtaResult { + const route = routes.find((r) => r.routeId === gps.routeId); + if (!route) { + throw CustomError.notFound(`Route ${gps.routeId} not found`); + } + + if (gps.status === "FALLA") { + return { + etaMinutes: -1, + arrivalWindow: { from: "--:--", to: "--:--" }, + message: + "La ruta presenta una falla mecánica. La recolección puede retrasarse.", + }; + } + + const target = route.positions.find( + (p) => p.positionId === PROXIMITY_POSITION_ID, + ); + if (!target) { + throw CustomError.internalServer( + `Route ${route.routeId} has no proximity position`, + ); + } + + const nowMs = new Date(gps.timestamp ?? new Date().toISOString()).getTime(); + const targetMs = new Date(target.timestamp).getTime(); + const etaMinutes = Math.round((targetMs - nowMs) / 60000); + + const fromDate = new Date(targetMs - WINDOW_MARGIN_MIN * 60000); + const toDate = new Date(targetMs + WINDOW_MARGIN_MIN * 60000); + const arrivalWindow = { + from: this.toHHMM(fromDate), + to: this.toHHMM(toDate), + }; + + let message: string; + if (etaMinutes < 0) { + message = `El camión ya pasó por tu zona (aprox. ${arrivalWindow.from} - ${arrivalWindow.to}).`; + } else if (etaMinutes <= 15) { + message = `Llega en aproximadamente ${etaMinutes} minutos.`; + } else { + message = `El camión llegará a tu zona entre las ${arrivalWindow.from} y ${arrivalWindow.to}.`; + } + + return { etaMinutes, arrivalWindow, message }; + } + + private toHHMM(date: Date): string { + const hh = String(date.getUTCHours()).padStart(2, "0"); + const mm = String(date.getUTCMinutes()).padStart(2, "0"); + return `${hh}:${mm}`; + } +} \ No newline at end of file diff --git a/backend/src/domain/use-cases/notifications/evaluate-notification.use-case.ts b/backend/src/domain/use-cases/notifications/evaluate-notification.use-case.ts new file mode 100644 index 0000000..a822ac9 --- /dev/null +++ b/backend/src/domain/use-cases/notifications/evaluate-notification.use-case.ts @@ -0,0 +1,55 @@ +import type { GpsUpdateDto } from "../../dtos/index.js"; +import type { NotificationCacheRepository } from "../../repositories/notification-cache.repository.js"; +import { + NotificationType, + notificationPayloads, + positionTriggers, +} from "../../../data/mocks/notification-types.mock.js"; +import { users } from "../../../data/mocks/users.mock.js"; + +export interface EvaluatedNotification { + userId: number; + type: NotificationType; + title: string; + body: string; +} + +export class EvaluateNotificationUseCase { + constructor(private readonly cache: NotificationCacheRepository) {} + + async execute(gps: GpsUpdateDto): Promise { + const targets = users.filter((u) => u.routeId === gps.routeId); + if (targets.length === 0) return []; + + const type = this.resolveType(gps); + if (!type) return []; + + const payload = notificationPayloads[type]; + const result: EvaluatedNotification[] = []; + + for (const user of targets) { + const key = `${user.id}-${gps.routeId}-${type}`; + if (await this.cache.wasSent(key)) continue; + + await this.cache.markSent(key); + result.push({ + userId: user.id, + type, + title: payload.title, + body: payload.body, + }); + } + + return result; + } + + private resolveType(gps: GpsUpdateDto): NotificationType | null { + if (gps.status === "FALLA") { + return NotificationType.MECHANICAL_FAILURE; + } + if (gps.positionId !== undefined && positionTriggers[gps.positionId]) { + return positionTriggers[gps.positionId] ?? null; + } + return null; + } +} \ No newline at end of file diff --git a/backend/src/domain/use-cases/notifications/process-gps-update.use-case.ts b/backend/src/domain/use-cases/notifications/process-gps-update.use-case.ts new file mode 100644 index 0000000..c953c0b --- /dev/null +++ b/backend/src/domain/use-cases/notifications/process-gps-update.use-case.ts @@ -0,0 +1,45 @@ +import { GpsUpdateDtoValidator } from "../../dtos/index.js"; +import { CalculateEtaUseCase, type EtaResult } from "./calculate-eta.use-case.js"; +import { + EvaluateNotificationUseCase, + type EvaluatedNotification, +} from "../notifications/evaluate-notification.use-case.js"; +import { routes } from "../../../data/mocks/routes.mock.js"; +import { CustomError } from "../../errors/custom.error.js"; + +export interface ProcessGpsUpdateResponse { + message: string; + truck: { truckId: number; routeId: string; status: string }; + eta: EtaResult; + notifications: EvaluatedNotification[]; +} + +export class ProcessGpsUpdateUseCase { + constructor( + private readonly calculateEta: CalculateEtaUseCase, + private readonly evaluateNotification: EvaluateNotificationUseCase, + ) {} + + async execute(data: unknown): Promise { + const dto = GpsUpdateDtoValidator.validate(data); + + const route = routes.find((r) => r.routeId === dto.routeId); + if (!route) { + throw CustomError.notFound(`Route ${dto.routeId} not found`); + } + + const eta = this.calculateEta.execute(dto); + const notifications = await this.evaluateNotification.execute(dto); + + return { + message: "GPS update processed", + truck: { + truckId: route.truckId, + routeId: route.routeId, + status: dto.status, + }, + eta, + notifications, + }; + } +} \ No newline at end of file diff --git a/backend/src/presentation/auth/controller.ts b/backend/src/presentation/auth/controller.ts index 3f9c941..e56dd50 100644 --- a/backend/src/presentation/auth/controller.ts +++ b/backend/src/presentation/auth/controller.ts @@ -1,11 +1,4 @@ -/** - * controller.ts - * AuthController: maneja las peticiones HTTP de autenticación. - * - Recibe los datos - * - Llama al use-case correspondiente - * - Responde al cliente - * - Maneja los errores - */ + import { Response } from "express"; import { AuthRequest } from "../middlewares/auth.middleware.js"; diff --git a/backend/src/presentation/auth/routes.ts b/backend/src/presentation/auth/routes.ts index e8808d4..92f81c3 100644 --- a/backend/src/presentation/auth/routes.ts +++ b/backend/src/presentation/auth/routes.ts @@ -1,8 +1,4 @@ -/** - * routes.ts - * Rutas de autenticación. - * Define los endpoints de register, login y getMe. - */ + import { Router } from "express"; import { AuthController } from "./controller.js"; diff --git a/backend/src/presentation/middlewares/auth.middleware.ts b/backend/src/presentation/middlewares/auth.middleware.ts index a605845..2dcf054 100644 --- a/backend/src/presentation/middlewares/auth.middleware.ts +++ b/backend/src/presentation/middlewares/auth.middleware.ts @@ -1,11 +1,4 @@ -/** - * auth.middleware.ts - * Middleware para validar el JWT. - * - Lee el token del header Authorization: Bearer TOKEN - * - Valida el token - * - Busca el usuario en base de datos - * - Agrega el usuario a req.user - */ + import { Request, Response, NextFunction } from "express"; import { JwtAdapter, JwtPayload } from "../../config/jwt.js"; @@ -19,28 +12,28 @@ export interface AuthRequest extends Request { export class AuthMiddleware { static async validate(req: AuthRequest, res: Response, next: NextFunction) { try { - // Obtener el token del header + const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { throw CustomError.unauthorized("Missing or invalid Authorization header"); } - const token = authHeader.substring(7); // Remover "Bearer " + const token = authHeader.substring(7); - // Validar el token + const payload = JwtAdapter.validate(token); if (!payload) { throw CustomError.unauthorized("Invalid token"); } - // Verificar que el usuario existe en BD + const repository = new AuthRepositoryImpl(); const user = await repository.findById(payload.id); if (!user) { throw CustomError.unauthorized("User not found"); } - // Agregar el usuario al request + req.user = payload; next(); diff --git a/backend/src/presentation/routes.ts b/backend/src/presentation/routes.ts index 0bf1063..c7aff04 100644 --- a/backend/src/presentation/routes.ts +++ b/backend/src/presentation/routes.ts @@ -1,14 +1,14 @@ import { Router } from "express"; import { AuthRoutes } from "./auth/routes.js"; +import { TrackingRoutes } from "./tracking/routes.js"; export class AppRoutes { + static get routes(): Router { + const router = Router(); - static get routes(): Router { + router.use('/api/auth', AuthRoutes.routes); + router.use('/api/tracking', TrackingRoutes.routes); - const router = Router(); - - router.use('/api/auth', AuthRoutes.routes); - - return router; - } + return router; + } } \ No newline at end of file diff --git a/backend/src/presentation/server.ts b/backend/src/presentation/server.ts index 10c0127..6c27b35 100644 --- a/backend/src/presentation/server.ts +++ b/backend/src/presentation/server.ts @@ -24,9 +24,9 @@ export class Server { async start() { /*middleware*/ - // para recebir el body como json + this.app.use(express.json()); - // para recebir el body como urlencoded + this.app.use(express.urlencoded({ extended: true })); //* Public folder @@ -36,7 +36,7 @@ export class Server { - // ponner servido a escuchar en el puerto 8080 + this.app.listen(this.port, () => { console.log(`Server is running on port ${this.port}`); }); diff --git a/backend/src/presentation/tracking/controller.ts b/backend/src/presentation/tracking/controller.ts new file mode 100644 index 0000000..6e3e348 --- /dev/null +++ b/backend/src/presentation/tracking/controller.ts @@ -0,0 +1,37 @@ +import { Request, Response } from "express"; +import { CustomError } from "../../domain/errors/custom.error.js"; +import { CalculateEtaUseCase } from "../../domain/use-cases/notifications/calculate-eta.use-case.js"; +import { EvaluateNotificationUseCase } from "../../domain/use-cases/notifications/evaluate-notification.use-case.js"; +import { ProcessGpsUpdateUseCase } from "../../domain/use-cases/notifications/process-gps-update.use-case.js"; +import { InMemoryNotificationCache } from "../../data/cache/notification-cache.impl.js"; + +export class TrackingController { + private cache = new InMemoryNotificationCache(); + private calculateEta = new CalculateEtaUseCase(); + private evaluateNotification = new EvaluateNotificationUseCase(this.cache); + private processGpsUpdate = new ProcessGpsUpdateUseCase( + this.calculateEta, + this.evaluateNotification, + ); + + gpsUpdate = async (req: Request, res: Response) => { + try { + const result = await this.processGpsUpdate.execute(req.body); + res.status(200).json(result); + } catch (error) { + this.handleError(error, res); + } + }; + + private handleError(error: unknown, res: Response): void { + if (error instanceof CustomError) { + res.status(error.statusCode).json({ error: error.message }); + return; + } + if (error instanceof Error) { + res.status(400).json({ error: error.message }); + return; + } + res.status(500).json({ error: "Internal server error" }); + } +} \ No newline at end of file diff --git a/backend/src/presentation/tracking/routes.ts b/backend/src/presentation/tracking/routes.ts new file mode 100644 index 0000000..2649edd --- /dev/null +++ b/backend/src/presentation/tracking/routes.ts @@ -0,0 +1,13 @@ +import { Router } from "express"; +import { TrackingController } from "./controller.js"; + +export class TrackingRoutes { + static get routes(): Router { + const router = Router(); + const controller = new TrackingController(); + + router.post("/gps-update", controller.gpsUpdate); + + return router; + } +} \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 57c5d55..c84af80 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -43,4 +43,4 @@ }, "exclude": ["node_modules", "dist", "prisma.config.ts"], "include": ["src/**/*.ts"] -} +} \ No newline at end of file