feat: implements notify sistem
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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];
|
||||
|
||||
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 {
|
||||
email: string;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
/**
|
||||
* register-user.dto.ts
|
||||
* DTO para registrar un usuario.
|
||||
* Define qué datos esperamos al registrar.
|
||||
*/
|
||||
|
||||
|
||||
export interface RegisterUserDto {
|
||||
name: string;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './auth/register-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 {
|
||||
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 {
|
||||
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 { 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 { 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 { 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 { 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 { 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 { 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();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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();
|
||||
|
||||
router.use('/api/auth', AuthRoutes.routes);
|
||||
router.use('/api/tracking', TrackingRoutes.routes);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user