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 = { '.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 => { 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);