278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
import http from 'http';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { connectDatabase } from './config/database';
|
|
import { poolingService } from './services/pooling.service';
|
|
import { messagingFleteService } from './services/messaging-flete.service';
|
|
import { adminApprovalService } from './services/admin-approval.service';
|
|
import { cancellationService } from './services/cancellation.service';
|
|
import { userAdminService } from './services/user-admin.service';
|
|
import { boldPaymentService } from './services/bold.service';
|
|
import { adminDashboardService } from './services/admin-dashboard.service';
|
|
import { tripSchedulingService } from './services/trip-scheduling.service';
|
|
import { Trip } from './models/Trip';
|
|
|
|
const PORT = process.env.PORT || 4000;
|
|
const PUBLIC_DIR = path.resolve(__dirname, '../public');
|
|
|
|
const MIME_TYPES: Record<string, string> = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.js': 'application/javascript; charset=utf-8',
|
|
'.json': 'application/json',
|
|
'.png': 'image/png',
|
|
'.jpg': 'image/jpeg',
|
|
'.jpeg': 'image/jpeg',
|
|
'.svg': 'image/svg+xml',
|
|
'.ico': 'image/x-icon',
|
|
'.apk': 'application/vnd.android.package-archive',
|
|
'.woff2': 'font/woff2'
|
|
};
|
|
|
|
// Servidor HTTP nativo Node.js sin dependencias pesadas extras
|
|
const server = http.createServer(async (req, res) => {
|
|
// CORS Headers
|
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, OPTIONS');
|
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key');
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
const pathname = url.pathname;
|
|
|
|
// Helper para leer body JSON
|
|
const getJsonBody = (): Promise<any> => {
|
|
return new Promise((resolve) => {
|
|
let data = '';
|
|
req.on('data', (chunk) => (data += chunk));
|
|
req.on('end', () => {
|
|
try {
|
|
resolve(data ? JSON.parse(data) : {});
|
|
} catch {
|
|
resolve({});
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
const sendJson = (status: number, data: any) => {
|
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(data));
|
|
};
|
|
|
|
try {
|
|
// 1. Healthcheck
|
|
if (pathname === '/api/health' && req.method === 'GET') {
|
|
return sendJson(200, { status: 'ONLINE', service: 'Pacifictrans Go API - Transporte & Mensajería Cali - Buenaventura' });
|
|
}
|
|
|
|
// 2. Pooling inteligente (Transporte)
|
|
if (pathname === '/api/trips/pool' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const result = await poolingService.processBookingAndPool(body);
|
|
return sendJson(result.success ? 200 : 400, result);
|
|
}
|
|
|
|
// 3. Cotizar / Despachar Encomienda (Mensajería 100% Prepagada con Bold)
|
|
if (pathname === '/api/trips/package' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const result = await messagingFleteService.registrarEnvioPaquete(body);
|
|
return sendJson(200, result);
|
|
}
|
|
|
|
// 4. Panel Admin: Listar viajes pendientes de aprobación
|
|
if (pathname === '/api/trips/pending' && req.method === 'GET') {
|
|
const pending = await adminApprovalService.getTripsPendientesAprobacion();
|
|
return sendJson(200, { success: true, count: pending.length, trips: pending });
|
|
}
|
|
|
|
// 5. Panel Admin: Aprobar y Activar Viaje (Asignando conductor y disparando Push)
|
|
if (pathname === '/api/trips/approve' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const approved = await adminApprovalService.aprobarYActivarViaje(body);
|
|
return sendJson(200, { success: true, trip: approved });
|
|
}
|
|
|
|
// 5.1. Panel Admin: Listar viajes con cupo lleno (4/4) listos para programación
|
|
if (pathname === '/api/trips/ready-to-schedule' && req.method === 'GET') {
|
|
const trips = await tripSchedulingService.getViajesListosParaProgramar();
|
|
return sendJson(200, { success: true, count: trips.length, trips });
|
|
}
|
|
|
|
// 5.2. Panel Admin: Programar formalmente horario de salida, escalonar paradas y notificar
|
|
if (pathname === '/api/trips/schedule' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const scheduled = await tripSchedulingService.programarViaje(body);
|
|
return sendJson(200, { success: true, trip: scheduled });
|
|
}
|
|
|
|
// 5.3. Panel Admin: Sugerir conductores ordenados por cercanía de residencia a las recogidas en Cali
|
|
if (pathname.startsWith('/api/trips/') && pathname.endsWith('/suggested-drivers') && req.method === 'GET') {
|
|
const parts = pathname.split('/');
|
|
const tripId = parts[3];
|
|
const suggestions = await tripSchedulingService.sugerirConductoresPorCercaniaResidencia(tripId);
|
|
return sendJson(200, { success: true, count: suggestions.length, suggestions });
|
|
}
|
|
|
|
// 6. Cancelación de Viajes con Evaluación de Penalidad
|
|
if (pathname === '/api/trips/cancel' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const result = await cancellationService.procesarCancelacion(body.tripId, body.usuarioId, body.motivo);
|
|
return sendJson(200, result);
|
|
}
|
|
|
|
// 7. Liquidación de Penalidades
|
|
if (pathname === '/api/penalties/settle' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const result = await cancellationService.liquidarPenalidad(body.usuarioId, body.monto, body.referencia);
|
|
return sendJson(200, result);
|
|
}
|
|
|
|
// 8. Administración de Usuarios (Estilo DripZone)
|
|
if (pathname === '/api/users' && req.method === 'GET') {
|
|
const users = await userAdminService.getAllUsers();
|
|
return sendJson(200, { success: true, count: users.length, users });
|
|
}
|
|
|
|
if (pathname === '/api/users/status' && req.method === 'GET') {
|
|
const query = url.searchParams.get('query') || '';
|
|
const status = await userAdminService.getUserByPhoneOrId(query);
|
|
return sendJson(200, { success: true, ...status });
|
|
}
|
|
|
|
if (pathname === '/api/users/toggle-active' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const updated = await userAdminService.toggleUserActiveStatus(body.userId, body.isActive);
|
|
return sendJson(200, { success: true, user: updated });
|
|
}
|
|
|
|
if (pathname === '/api/users/change-role' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const updated = await userAdminService.updateUserRole(body.userId, body.newRole);
|
|
return sendJson(200, { success: true, user: updated });
|
|
}
|
|
|
|
// 9. Webhook oficial de Bold para confirmación de transacciones
|
|
if (pathname === '/api/payments/bold/webhook' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
console.log('🔔 [Bold Webhook Received]:', JSON.stringify(body));
|
|
|
|
const eventData = body.data || body;
|
|
const ref = eventData.reference || '';
|
|
const status = eventData.status;
|
|
|
|
if (status === 'PAID') {
|
|
if (ref.startsWith('PENALTY_SETTLEMENT-')) {
|
|
const parts = ref.split('-');
|
|
const userId = parts[1];
|
|
const amount = eventData.amount?.total_amount || 0;
|
|
await cancellationService.liquidarPenalidad(userId, amount, eventData.id || ref);
|
|
} else if (ref.startsWith('TRIP_PASSENGER-') || ref.startsWith('MESSAGING_PACKAGE-')) {
|
|
const tripId = ref.split('-')[1];
|
|
await Trip.findByIdAndUpdate(tripId, {
|
|
'boldPayment.transactionStatus': 'PAID',
|
|
'boldPayment.paidAt': new Date(),
|
|
});
|
|
}
|
|
}
|
|
|
|
return sendJson(200, { received: true });
|
|
}
|
|
|
|
// 10. Telemetría GPS en tiempo real de conductores para el Panel Admin
|
|
if (pathname === '/api/admin/drivers-realtime' && req.method === 'GET') {
|
|
const drivers = await adminDashboardService.getUbicacionesConductoresEnVivo();
|
|
return sendJson(200, { success: true, count: drivers.length, drivers });
|
|
}
|
|
|
|
// 11. Analítica de Demanda por Zonas y Déficit de Vehículos
|
|
if (pathname === '/api/admin/demand-analytics' && req.method === 'GET') {
|
|
const demand = await adminDashboardService.getAnalisisDemandaYDeficitVehicular();
|
|
return sendJson(200, { success: true, ...demand });
|
|
}
|
|
|
|
// 12. KPIs Operativos y Financieros del Administrador
|
|
if (pathname === '/api/admin/kpis' && req.method === 'GET') {
|
|
const kpis = await adminDashboardService.getDashboardKPIs();
|
|
return sendJson(200, { success: true, kpis });
|
|
}
|
|
|
|
// 13. Actualización de posición GPS desde la App del Conductor
|
|
if (pathname === '/api/drivers/location-update' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const updateResult = await adminDashboardService.actualizarUbicacionConductor(
|
|
body.userId,
|
|
body.lat,
|
|
body.lng
|
|
);
|
|
return sendJson(200, updateResult);
|
|
}
|
|
|
|
// 14. Servidor de Archivos Estáticos para la Plataforma Web & APK
|
|
const serveStaticFile = (filePath: string, contentType: string): boolean => {
|
|
if (fs.existsSync(filePath)) {
|
|
const stat = fs.statSync(filePath);
|
|
if (stat.isFile()) {
|
|
res.writeHead(200, {
|
|
'Content-Type': contentType,
|
|
'Content-Length': stat.size,
|
|
'Cache-Control': contentType.includes('apk') ? 'no-cache' : 'public, max-age=3600'
|
|
});
|
|
fs.createReadStream(filePath).pipe(res);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Descarga directa del APK Oficial Android
|
|
if (pathname === '/PacifictransGo.apk' || pathname === '/download/apk') {
|
|
const localApk = path.join(PUBLIC_DIR, 'PacifictransGo.apk');
|
|
const fallbackApk = path.resolve(__dirname, '../../../pacifictrans-app/public/PacifictransGo.apk');
|
|
const vpsApk = '/home/carlos/apps/dripzone-app/public/PacifictransGo.apk';
|
|
|
|
const targetApk = fs.existsSync(localApk) ? localApk : (fs.existsSync(fallbackApk) ? fallbackApk : vpsApk);
|
|
if (serveStaticFile(targetApk, MIME_TYPES['.apk'])) return;
|
|
}
|
|
|
|
// Archivo estático directo (HTML, CSS, JS, Imágenes)
|
|
let relativePath = pathname === '/' ? 'index.html' : pathname.replace(/^\//, '');
|
|
let targetPath = path.join(PUBLIC_DIR, relativePath);
|
|
|
|
const ext = path.extname(targetPath).toLowerCase();
|
|
const mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
|
|
if (serveStaticFile(targetPath, mime)) {
|
|
return;
|
|
}
|
|
|
|
// Fallback SPA a index.html para rutas sin extensión
|
|
if (!ext) {
|
|
const indexPath = path.join(PUBLIC_DIR, 'index.html');
|
|
if (serveStaticFile(indexPath, MIME_TYPES['.html'])) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Ruta no encontrada
|
|
sendJson(404, { error: 'Recurso o Endpoint no encontrado' });
|
|
} catch (error: any) {
|
|
console.error('❌ [API Error]:', error);
|
|
sendJson(500, { error: error.message || 'Error interno del servidor' });
|
|
}
|
|
});
|
|
|
|
async function bootstrap() {
|
|
await connectDatabase();
|
|
server.listen(PORT, () => {
|
|
console.log(`🚀 [Servidor Transporte Cali - Buenaventura] Ejecutándose en puerto ${PORT}`);
|
|
});
|
|
}
|
|
|
|
bootstrap().catch(console.error);
|