616 lines
28 KiB
TypeScript
616 lines
28 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 { tariffsService } from './services/tariffs.service';
|
|
import { authService } from './services/auth.service';
|
|
import { statsReviewsService } from './services/stats-reviews.service';
|
|
import { settlementService } from './services/settlement.service';
|
|
import { whatsAppNotificationService } from './services/whatsapp-notification.service';
|
|
import { couponService } from './services/coupon.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: 'Pacific Trans GO API - Transporte & Mensajería Cali - Buenaventura' });
|
|
}
|
|
|
|
// 1.1. Autenticación: Enviar OTP de seguridad por WhatsApp & Email
|
|
if (pathname === '/api/auth/send-otp' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.identifier) {
|
|
return sendJson(400, { success: false, error: 'Se requiere número de WhatsApp o identificador' });
|
|
}
|
|
const result = await authService.sendOtp(body.identifier, body.email);
|
|
return sendJson(200, result);
|
|
}
|
|
|
|
// 1.2. Autenticación: Validar OTP de 6 dígitos e iniciar sesión / registro
|
|
if (pathname === '/api/auth/verify-otp' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.identifier || !body.code) {
|
|
return sendJson(400, { success: false, error: 'Identificador y código OTP requeridos' });
|
|
}
|
|
const result = await authService.verifyOtp(body);
|
|
return sendJson(result.success ? 200 : 400, result);
|
|
}
|
|
|
|
// 1.3. Autenticación: Obtener perfil del usuario actual (sesión activa)
|
|
if (pathname === '/api/auth/me' && req.method === 'GET') {
|
|
const authHeader = req.headers['authorization'] || '';
|
|
const token = authHeader.replace(/^Bearer\s+/i, '') || url.searchParams.get('token') || '';
|
|
const user = await authService.getUserFromToken(token);
|
|
if (!user) {
|
|
return sendJson(401, { success: false, error: 'Sesión no válida o expirada (lapso de 24 horas superado)' });
|
|
}
|
|
return sendJson(200, { success: true, user });
|
|
}
|
|
|
|
// Helper de seguridad por roles y permisos en rutas API
|
|
const getToken = (): string => {
|
|
const authHeader = (req.headers['authorization'] as string) || '';
|
|
return authHeader.replace(/^Bearer\s+/i, '').trim() || url.searchParams.get('token') || (req.headers['x-session-token'] as string) || '';
|
|
};
|
|
|
|
const requireRole = async (allowedRoles: ('ADMIN' | 'CONDUCTOR' | 'PASAJERO' | 'OPERADOR')[]) => {
|
|
const token = getToken();
|
|
if (!token) {
|
|
return { allowed: false, error: 'Autenticación requerida: Inicia sesión con código OTP de seguridad.' };
|
|
}
|
|
return authService.verifyRole(token, allowedRoles);
|
|
};
|
|
|
|
// 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 (Protegido por Rol)
|
|
if (pathname === '/api/trips/pending' && req.method === 'GET') {
|
|
const auth = await requireRole(['ADMIN', 'OPERADOR']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
const pending = await adminApprovalService.getTripsPendientesAprobacion();
|
|
return sendJson(200, { success: true, count: pending.length, trips: pending });
|
|
}
|
|
|
|
// 5. Panel Admin: Aprobar y Activar Viaje (Protegido por Rol)
|
|
if (pathname === '/api/trips/approve' && req.method === 'POST') {
|
|
const auth = await requireRole(['ADMIN', 'OPERADOR']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
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 (Protegido)
|
|
if (pathname === '/api/trips/ready-to-schedule' && req.method === 'GET') {
|
|
const auth = await requireRole(['ADMIN', 'OPERADOR']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
const trips = await tripSchedulingService.getViajesListosParaProgramar();
|
|
return sendJson(200, { success: true, count: trips.length, trips });
|
|
}
|
|
|
|
// 5.2. Panel Admin: Programar formalmente horario de salida (Protegido)
|
|
if (pathname === '/api/trips/schedule' && req.method === 'POST') {
|
|
const auth = await requireRole(['ADMIN', 'OPERADOR']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
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 y Perfil Personal
|
|
// 8.0. Actualización de Información Personal (No permite cambiar número de celular registrado)
|
|
if ((pathname === '/api/users/profile' || pathname === '/api/auth/profile') && req.method === 'GET') {
|
|
const auth = await requireRole(['PASAJERO', 'CONDUCTOR', 'ADMIN', 'OPERADOR']);
|
|
if (!auth.allowed) return sendJson(401, { success: false, error: auth.error });
|
|
return sendJson(200, { success: true, user: auth.user });
|
|
}
|
|
|
|
if ((pathname === '/api/users/profile' || pathname === '/api/auth/profile') && (req.method === 'PATCH' || req.method === 'PUT' || req.method === 'POST')) {
|
|
const auth = await requireRole(['PASAJERO', 'CONDUCTOR', 'ADMIN', 'OPERADOR']);
|
|
if (!auth.allowed) return sendJson(401, { success: false, error: auth.error });
|
|
const body = await getJsonBody();
|
|
try {
|
|
const updated = await userAdminService.updateUserProfile(auth.user.id || auth.user._id, body);
|
|
return sendJson(200, { success: true, message: 'Información personal actualizada correctamente', user: updated });
|
|
} catch (err: any) {
|
|
return sendJson(400, { success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
// 8.1. Gestión Centralizada de Administradores por el Superadministrador
|
|
if (pathname === '/api/admin/administrators' && req.method === 'GET') {
|
|
const auth = await requireRole(['ADMIN']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
const administrators = await userAdminService.getAdministrators();
|
|
return sendJson(200, { success: true, count: administrators.length, administrators });
|
|
}
|
|
|
|
if (pathname === '/api/admin/administrators' && req.method === 'POST') {
|
|
const auth = await requireRole(['ADMIN']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
const body = await getJsonBody();
|
|
try {
|
|
const administrator = await userAdminService.createAdministrator(body);
|
|
return sendJson(201, { success: true, administrator });
|
|
} catch (err: any) {
|
|
return sendJson(400, { success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
if ((pathname === '/api/admin/administrators/update' || (pathname.startsWith('/api/admin/administrators/') && !pathname.endsWith('/toggle-active'))) && (req.method === 'PUT' || req.method === 'POST')) {
|
|
const auth = await requireRole(['ADMIN']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
const body = await getJsonBody();
|
|
const adminId = pathname.split('/')[4] || body.id || body.adminId;
|
|
try {
|
|
const administrator = await userAdminService.updateAdministrator(adminId, body, auth.user.id);
|
|
return sendJson(200, { success: true, administrator });
|
|
} catch (err: any) {
|
|
return sendJson(400, { success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
if ((pathname === '/api/admin/administrators/toggle-active' || (pathname.startsWith('/api/admin/administrators/') && pathname.endsWith('/toggle-active'))) && (req.method === 'POST' || req.method === 'PATCH')) {
|
|
const auth = await requireRole(['ADMIN']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
const body = await getJsonBody();
|
|
const adminId = body.id || body.adminId || pathname.split('/')[4];
|
|
try {
|
|
const administrator = await userAdminService.toggleAdministratorActive(adminId, body.isActive);
|
|
return sendJson(200, { success: true, administrator });
|
|
} catch (err: any) {
|
|
return sendJson(400, { success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
if ((pathname === '/api/admin/administrators/delete' && req.method === 'POST') || (pathname.startsWith('/api/admin/administrators/') && req.method === 'DELETE')) {
|
|
const auth = await requireRole(['ADMIN']);
|
|
if (!auth.allowed) return sendJson(403, { success: false, error: auth.error });
|
|
const body = await getJsonBody();
|
|
const adminId = pathname.split('/')[4] || body.id || body.adminId;
|
|
try {
|
|
await userAdminService.deleteAdministrator(adminId, auth.user.id);
|
|
return sendJson(200, { success: true, message: 'Administrador eliminado exitosamente' });
|
|
} catch (err: any) {
|
|
return sendJson(400, { success: false, error: err.message });
|
|
}
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
// 12.1. Administrador: Consultar grupos de WhatsApp para despacho oficial de Pacific Trans GO
|
|
if (pathname === '/api/admin/whatsapp/groups' && req.method === 'GET') {
|
|
const pacificJid = await whatsAppNotificationService.getPacificTransNotificationTarget();
|
|
return sendJson(200, {
|
|
success: true,
|
|
pacificTrans: {
|
|
groupName: 'Pacific Trans Notificaciones',
|
|
resolvedJid: pacificJid,
|
|
isGroup: pacificJid.endsWith('@g.us'),
|
|
},
|
|
});
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 13.1. Obtener Configuración de Tarifas
|
|
if (pathname === '/api/tariffs' && req.method === 'GET') {
|
|
const config = await tariffsService.getTarifasActivas();
|
|
return sendJson(200, { success: true, config });
|
|
}
|
|
|
|
// 13.2. Actualizar Tarifas Configurables desde la Plataforma
|
|
if (pathname === '/api/tariffs' && (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH')) {
|
|
const body = await getJsonBody();
|
|
const updated = await tariffsService.updateTarifas(body);
|
|
return sendJson(200, { success: true, message: 'Tarifas actualizadas correctamente en MongoDB', config: updated });
|
|
}
|
|
|
|
// 13.2.1. Gestión de Bonos y Cupones de Descuento (Viajes y Encomiendas)
|
|
if (pathname === '/api/coupons' && req.method === 'GET') {
|
|
const coupons = await couponService.getAllCoupons();
|
|
return sendJson(200, { success: true, count: coupons.length, coupons });
|
|
}
|
|
|
|
if (pathname === '/api/coupons' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
try {
|
|
const coupon = await couponService.createCoupon(body);
|
|
return sendJson(201, { success: true, coupon });
|
|
} catch (err: any) {
|
|
return sendJson(400, { success: false, error: err.message || 'Error al crear cupón' });
|
|
}
|
|
}
|
|
|
|
if ((pathname === '/api/coupons/toggle' || (pathname.startsWith('/api/coupons/') && pathname.endsWith('/toggle'))) && (req.method === 'POST' || req.method === 'PATCH')) {
|
|
const body = await getJsonBody();
|
|
const id = body.id || body.couponId || pathname.split('/')[3];
|
|
if (!id) return sendJson(400, { success: false, error: 'ID de cupón requerido' });
|
|
const coupon = await couponService.toggleCouponStatus(id, body.estaActivo);
|
|
return sendJson(200, { success: true, coupon });
|
|
}
|
|
|
|
if ((pathname === '/api/coupons/delete' && req.method === 'POST') || (pathname.startsWith('/api/coupons/') && req.method === 'DELETE')) {
|
|
const body = await getJsonBody();
|
|
const id = pathname.split('/')[3] || body.id || body.couponId;
|
|
if (!id) return sendJson(400, { success: false, error: 'ID de cupón requerido' });
|
|
const success = await couponService.deleteCoupon(id);
|
|
return sendJson(200, { success });
|
|
}
|
|
|
|
if (pathname === '/api/coupons/validate' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
const validation = await couponService.validateCoupon(
|
|
body.codigo || body.code,
|
|
body.serviceType || body.tipoServicio || 'VIAJES',
|
|
parseFloat(body.subtotal || 0)
|
|
);
|
|
return sendJson(200, validation);
|
|
}
|
|
|
|
// 13.3. Estadísticas de Viajes y Sistema de Calificaciones/Comentarios
|
|
if (pathname === '/api/stats-reviews' && req.method === 'GET') {
|
|
const data = await statsReviewsService.getStatsAndReviews();
|
|
return sendJson(200, { success: true, ...data });
|
|
}
|
|
|
|
if (pathname === '/api/stats-reviews' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.autorNombre || !body.puntuacion || !body.comentario) {
|
|
return sendJson(400, { success: false, error: 'Nombre, puntuación y comentario requeridos' });
|
|
}
|
|
const review = await statsReviewsService.addReview(body);
|
|
return sendJson(200, { success: true, review });
|
|
}
|
|
|
|
// 13.4. Control de Pagos, Comisiones, Liquidación y Transferencias Bre-B
|
|
// 13.4.1. Panorama general de liquidaciones
|
|
if (pathname === '/api/settlements/overview' && req.method === 'GET') {
|
|
const overview = await settlementService.getOverviewLiquidaciones();
|
|
return sendJson(200, { success: true, ...overview });
|
|
}
|
|
|
|
// 13.4.2. Desglose financiero y estado de pago de pasajeros de un viaje
|
|
if (pathname.startsWith('/api/settlements/trip/') && req.method === 'GET') {
|
|
const parts = pathname.split('/');
|
|
const tripId = parts[4];
|
|
if (!tripId) return sendJson(400, { success: false, error: 'tripId requerido' });
|
|
const summary = await settlementService.getTripFinancialSummary(tripId);
|
|
return sendJson(200, { success: true, summary });
|
|
}
|
|
|
|
// 13.4.3. Marcar estado de pago de un pasajero (ej. pago recibido en efectivo o revertir)
|
|
if (pathname === '/api/settlements/toggle-passenger-payment' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.tripId || !body.pasajeroId || !body.nuevoEstado) {
|
|
return sendJson(400, { success: false, error: 'tripId, pasajeroId y nuevoEstado requeridos' });
|
|
}
|
|
const summary = await settlementService.marcarPagoPasajero(body.tripId, body.pasajeroId, body.nuevoEstado);
|
|
return sendJson(200, { success: true, summary });
|
|
}
|
|
|
|
// 13.4.4. Finalizar viaje formalmente (COMPLETED) y liquidar saldo al conductor
|
|
if (pathname === '/api/settlements/complete-and-settle' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.tripId) {
|
|
return sendJson(400, { success: false, error: 'tripId requerido' });
|
|
}
|
|
const result = await settlementService.finalizarRecorridoYLiquidar(body.tripId, body.adminId);
|
|
return sendJson(200, { success: true, ...result });
|
|
}
|
|
|
|
// 13.4.5. Obtener billetera y estado financiero de un conductor
|
|
if (pathname.startsWith('/api/settlements/driver/') && pathname.endsWith('/wallet') && req.method === 'GET') {
|
|
const parts = pathname.split('/');
|
|
const driverId = parts[4];
|
|
if (!driverId) return sendJson(400, { success: false, error: 'driverId requerido' });
|
|
const wallet = await settlementService.getBilleteraConductor(driverId);
|
|
return sendJson(200, { success: true, wallet });
|
|
}
|
|
|
|
// 13.4.6. Configurar o actualizar Llave Bre-B del conductor
|
|
if (pathname === '/api/settlements/driver/bre-b-key' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.conductorId || !body.valor || !body.bancoDestino) {
|
|
return sendJson(400, { success: false, error: 'conductorId, valor y bancoDestino requeridos' });
|
|
}
|
|
const llave = await settlementService.actualizarLlaveBreB(body.conductorId, body);
|
|
return sendJson(200, { success: true, llave });
|
|
}
|
|
|
|
// 13.4.7. Realizar transferencia inmediata por Bre-B (Banco de la República)
|
|
if (pathname === '/api/settlements/driver/payout-bre-b' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.conductorId || !body.monto) {
|
|
return sendJson(400, { success: false, error: 'conductorId y monto requeridos' });
|
|
}
|
|
const receipt = await settlementService.realizarTransferenciaBreB(body);
|
|
return sendJson(200, receipt);
|
|
}
|
|
|
|
// 13.4.8. Conductor crea una Solicitud de Transferencia Bre-B
|
|
if (pathname === '/api/settlements/payout-requests/create' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.conductorId || !body.monto) {
|
|
return sendJson(400, { success: false, error: 'conductorId y monto requeridos' });
|
|
}
|
|
const solicitud = await settlementService.crearSolicitudTransferencia(body);
|
|
return sendJson(200, { success: true, solicitud });
|
|
}
|
|
|
|
// 13.4.9. Consultar solicitudes de transferencia (Bandeja Admin y Conductor)
|
|
if (pathname === '/api/settlements/payout-requests' && req.method === 'GET') {
|
|
const estado = url.searchParams.get('estado') || undefined;
|
|
const conductorId = url.searchParams.get('conductorId') || undefined;
|
|
const solicitudes = await settlementService.obtenerSolicitudesTransferencia({ estado, conductorId });
|
|
return sendJson(200, { success: true, count: solicitudes.length, solicitudes });
|
|
}
|
|
|
|
// 13.4.10. Administrador aprueba y ejecuta la transferencia por Bre-B
|
|
if (pathname === '/api/settlements/payout-requests/approve-and-transfer' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.solicitudId) {
|
|
return sendJson(400, { success: false, error: 'solicitudId requerido' });
|
|
}
|
|
const result = await settlementService.aprobarYTransferirBreB(body.solicitudId, body.adminId, body.notas);
|
|
return sendJson(200, { success: true, ...result });
|
|
}
|
|
|
|
// 13.4.11. Administrador rechaza solicitud de transferencia
|
|
if (pathname === '/api/settlements/payout-requests/reject' && req.method === 'POST') {
|
|
const body = await getJsonBody();
|
|
if (!body.solicitudId) {
|
|
return sendJson(400, { success: false, error: 'solicitudId requerido' });
|
|
}
|
|
const solicitud = await settlementService.rechazarSolicitudTransferencia(body.solicitudId, body.adminId, body.motivo);
|
|
return sendJson(200, { success: true, solicitud });
|
|
}
|
|
|
|
// 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() {
|
|
// 1. Iniciar servidor HTTP de inmediato para responder 200 OK a Traefik y visitantes
|
|
server.listen(PORT, () => {
|
|
console.log(`🚀 [Pacific Trans GO] Plataforma Web y API escuchando en puerto ${PORT}`);
|
|
});
|
|
|
|
// 2. Conectar a MongoDB de forma no bloqueante con reintento automático
|
|
const tryConnect = async () => {
|
|
try {
|
|
await connectDatabase();
|
|
console.log('✅ [MongoDB] Conectado exitosamente en segundo plano.');
|
|
await couponService.ensureSeedCoupons();
|
|
await userAdminService.ensureSuperAdminExists();
|
|
} catch (err: any) {
|
|
console.warn(`⚠️ [MongoDB Warning] Conexión no disponible (${err.message || 'ENOTFOUND'}). Reintentando en 10s...`);
|
|
setTimeout(tryConnect, 10000);
|
|
}
|
|
};
|
|
|
|
tryConnect();
|
|
}
|
|
|
|
bootstrap().catch(console.error);
|