Merge branch 'main' of https://git.onlinces.net/onlinces/hackathon-opti-1a67c90779374919b2f9ca0914dcd4b4
This commit is contained in:
@@ -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
|
|
||||||
@@ -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 "dotenv/config";
|
||||||
import { env } from "./config/env.js";
|
import { env } from "./config/env.js";
|
||||||
@@ -14,11 +8,10 @@ import { prisma } from "./data/postgres/index.js";
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
// Verificar conexión a la base de datos
|
|
||||||
await prisma.$connect();
|
await prisma.$connect();
|
||||||
console.log("✓ Database connected");
|
console.log("Database connected");
|
||||||
|
|
||||||
|
|
||||||
// Crear y iniciar el servidor
|
|
||||||
const server = new Server({
|
const server = new Server({
|
||||||
port: env.PORT,
|
port: env.PORT,
|
||||||
routes: AppRoutes.routes,
|
routes: AppRoutes.routes,
|
||||||
@@ -26,7 +19,7 @@ async function main() {
|
|||||||
|
|
||||||
await server.start();
|
await server.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("✗ Error starting application:", error);
|
console.error(" Error starting application:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 bcrypt from "bcryptjs";
|
||||||
import { env } from "./env.js";
|
import { env } from "./env.js";
|
||||||
|
|||||||
@@ -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 getEnvVar = (key: string, defaultValue?: string): string => {
|
||||||
const value = process.env[key];
|
const value = process.env[key];
|
||||||
|
|||||||
17
backend/src/data/cache/notification-cache.impl.ts
vendored
Normal file
17
backend/src/data/cache/notification-cache.impl.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { NotificationCacheRepository } from "../../domain/repositories/notification-cache.repository.js";
|
||||||
|
|
||||||
|
export class InMemoryNotificationCache implements NotificationCacheRepository {
|
||||||
|
private readonly sent = new Set<string>();
|
||||||
|
|
||||||
|
async wasSent(key: string): Promise<boolean> {
|
||||||
|
return this.sent.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async markSent(key: string): Promise<void> {
|
||||||
|
this.sent.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
this.sent.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
backend/src/data/mocks/colonias.mock.ts
Normal file
17
backend/src/data/mocks/colonias.mock.ts
Normal file
@@ -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)" },
|
||||||
|
];
|
||||||
47
backend/src/data/mocks/notification-types.mock.ts
Normal file
47
backend/src/data/mocks/notification-types.mock.ts
Normal file
@@ -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<NotificationType, NotificationPayload> = {
|
||||||
|
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<number, NotificationType> = {
|
||||||
|
2: NotificationType.ROUTE_START,
|
||||||
|
4: NotificationType.TRUCK_PROXIMITY,
|
||||||
|
8: NotificationType.ROUTE_COMPLETED,
|
||||||
|
};
|
||||||
259
backend/src/data/mocks/routes.mock.ts
Normal file
259
backend/src/data/mocks/routes.mock.ts
Normal file
@@ -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" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
19
backend/src/data/mocks/users.mock.ts
Normal file
19
backend/src/data/mocks/users.mock.ts
Normal file
@@ -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" },
|
||||||
|
];
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
/**
|
|
||||||
* login-user.dto.ts
|
|
||||||
* DTO para iniciar sesión.
|
|
||||||
* Define qué datos esperamos al hacer login.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface LoginUserDto {
|
export interface LoginUserDto {
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
/**
|
|
||||||
* register-user.dto.ts
|
|
||||||
* DTO para registrar un usuario.
|
|
||||||
* Define qué datos esperamos al registrar.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface RegisterUserDto {
|
export interface RegisterUserDto {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './auth/register-user.dto.js';
|
export * from './auth/register-user.dto.js';
|
||||||
export * from './auth/login-user.dto.js';
|
export * from './auth/login-user.dto.js';
|
||||||
|
export * from './tracking/gps-update.dto.js';
|
||||||
68
backend/src/domain/dtos/tracking/gps-update.dto.ts
Normal file
68
backend/src/domain/dtos/tracking/gps-update.dto.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
export class CustomError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
|
|||||||
@@ -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 {
|
export interface AuthRepository {
|
||||||
findByEmail(email: string): Promise<{ id: number; email: string; password: string; name: string } | null>;
|
findByEmail(email: string): Promise<{ id: number; email: string; password: string; name: string } | null>;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface NotificationCacheRepository {
|
||||||
|
|
||||||
|
wasSent(key: string): Promise<boolean>;
|
||||||
|
|
||||||
|
|
||||||
|
markSent(key: string): Promise<void>;
|
||||||
|
|
||||||
|
|
||||||
|
clear(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -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 { AuthRepository } from "../../repositories/auth.repository.js";
|
||||||
import { CustomError } from "../../errors/custom.error.js";
|
import { CustomError } from "../../errors/custom.error.js";
|
||||||
|
|||||||
@@ -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 { LoginUserDto, LoginUserDtoValidator } from "../../dtos/index.js";
|
||||||
import { AuthRepository } from "../../repositories/auth.repository.js";
|
import { AuthRepository } from "../../repositories/auth.repository.js";
|
||||||
|
|||||||
@@ -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 { RegisterUserDto, RegisterUserDtoValidator } from "../../dtos/index.js";
|
||||||
import { AuthRepository } from "../../repositories/auth.repository.js";
|
import { AuthRepository } from "../../repositories/auth.repository.js";
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<EvaluatedNotification[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ProcessGpsUpdateResponse> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { Response } from "express";
|
||||||
import { AuthRequest } from "../middlewares/auth.middleware.js";
|
import { AuthRequest } from "../middlewares/auth.middleware.js";
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
/**
|
|
||||||
* routes.ts
|
|
||||||
* Rutas de autenticación.
|
|
||||||
* Define los endpoints de register, login y getMe.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { AuthController } from "./controller.js";
|
import { AuthController } from "./controller.js";
|
||||||
|
|||||||
@@ -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 { Request, Response, NextFunction } from "express";
|
||||||
import { JwtAdapter, JwtPayload } from "../../config/jwt.js";
|
import { JwtAdapter, JwtPayload } from "../../config/jwt.js";
|
||||||
@@ -19,28 +12,28 @@ export interface AuthRequest extends Request {
|
|||||||
export class AuthMiddleware {
|
export class AuthMiddleware {
|
||||||
static async validate(req: AuthRequest, res: Response, next: NextFunction) {
|
static async validate(req: AuthRequest, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
// Obtener el token del header
|
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||||
throw CustomError.unauthorized("Missing or invalid Authorization header");
|
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);
|
const payload = JwtAdapter.validate(token);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
throw CustomError.unauthorized("Invalid token");
|
throw CustomError.unauthorized("Invalid token");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar que el usuario existe en BD
|
|
||||||
const repository = new AuthRepositoryImpl();
|
const repository = new AuthRepositoryImpl();
|
||||||
const user = await repository.findById(payload.id);
|
const user = await repository.findById(payload.id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw CustomError.unauthorized("User not found");
|
throw CustomError.unauthorized("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agregar el usuario al request
|
|
||||||
req.user = payload;
|
req.user = payload;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { AuthRoutes } from "./auth/routes.js";
|
import { AuthRoutes } from "./auth/routes.js";
|
||||||
|
import { TrackingRoutes } from "./tracking/routes.js";
|
||||||
|
|
||||||
export class AppRoutes {
|
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();
|
return router;
|
||||||
|
}
|
||||||
router.use('/api/auth', AuthRoutes.routes);
|
|
||||||
|
|
||||||
return router;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -24,9 +24,9 @@ export class Server {
|
|||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
/*middleware*/
|
/*middleware*/
|
||||||
// para recebir el body como json
|
|
||||||
this.app.use(express.json());
|
this.app.use(express.json());
|
||||||
// para recebir el body como urlencoded
|
|
||||||
this.app.use(express.urlencoded({ extended: true }));
|
this.app.use(express.urlencoded({ extended: true }));
|
||||||
//* Public folder
|
//* Public folder
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export class Server {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ponner servido a escuchar en el puerto 8080
|
|
||||||
this.app.listen(this.port, () => {
|
this.app.listen(this.port, () => {
|
||||||
console.log(`Server is running on port ${this.port}`);
|
console.log(`Server is running on port ${this.port}`);
|
||||||
});
|
});
|
||||||
|
|||||||
37
backend/src/presentation/tracking/controller.ts
Normal file
37
backend/src/presentation/tracking/controller.ts
Normal file
@@ -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" });
|
||||||
|
}
|
||||||
|
}
|
||||||
13
backend/src/presentation/tracking/routes.ts
Normal file
13
backend/src/presentation/tracking/routes.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,4 +43,4 @@
|
|||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist", "prisma.config.ts"],
|
"exclude": ["node_modules", "dist", "prisma.config.ts"],
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user