diff --git a/transporte-cali-buenaventura/backend/public/css/style.css b/transporte-cali-buenaventura/backend/public/css/style.css index 6f860e5..28fd02c 100644 --- a/transporte-cali-buenaventura/backend/public/css/style.css +++ b/transporte-cali-buenaventura/backend/public/css/style.css @@ -1739,6 +1739,76 @@ h1, h2, h3, h4, .brand-title { text-align: right; } +/* CARDS DE SOLICITUDES DE TRANSFERENCIA BRE-B */ +.payout-request-card { + background: rgba(15, 23, 42, 0.85); + border: 1px solid var(--border-glass); + border-radius: var(--radius-sm); + padding: 1.15rem; + margin-bottom: 0.85rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; + transition: all 0.2s ease; +} + +.payout-request-card:hover { + background: rgba(15, 23, 42, 0.98); + border-color: rgba(236, 72, 153, 0.4); +} + +.payout-request-card.status-pending { + border-left: 4px solid #f59e0b; +} + +.payout-request-card.status-transferred { + border-left: 4px solid #10b981; +} + +.payout-request-card.status-rejected { + border-left: 4px solid #ef4444; +} + +.btn-approve-breb { + background: linear-gradient(135deg, #7c3aed, #db2777); + color: #fff; + border: none; + font-size: 0.82rem; + font-weight: 700; + padding: 0.5rem 1rem; + border-radius: 6px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.35rem; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(219, 39, 119, 0.3); +} + +.btn-approve-breb:hover { + filter: brightness(1.1); + transform: translateY(-1px); +} + +.btn-reject-payout { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.35); + color: #f87171; + font-size: 0.82rem; + font-weight: 600; + padding: 0.5rem 0.85rem; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-reject-payout:hover { + background: rgba(239, 68, 68, 0.2); + color: #fff; +} + diff --git a/transporte-cali-buenaventura/backend/public/index.html b/transporte-cali-buenaventura/backend/public/index.html index 3cd727b..b2d802d 100644 --- a/transporte-cali-buenaventura/backend/public/index.html +++ b/transporte-cali-buenaventura/backend/public/index.html @@ -496,8 +496,11 @@ +

+ Tu solicitud será enviada al administrador encargado para realizar la transferencia inmediata por Bre-B directamente a tu cuenta. +

@@ -548,6 +551,26 @@ + +
+
+

+ ⏳ Estado de Mis Solicitudes de Transferencia +

+ +
+

+ Seguimiento de las peticiones de retiro enviadas para transferencia por el administrador encargado. +

+
+
+ No tienes solicitudes de retiro activas. +
+
+
+

@@ -749,7 +772,7 @@ ⚙️ 2. Tarifas & Canales de Atención

@@ -943,6 +966,42 @@ + +
+
+
+
+ ⚡ Control Bre-B +

+ Solicitudes de Transferencia de Conductores +

+
+

+ Bandeja de solicitudes enviadas por los conductores. El administrador encargado autoriza y ejecuta la transferencia inmediata por Bre-B. +

+
+ +
+ + +
+
+ + +
+
+ Consultando solicitudes de transferencia... +
+
+
+
diff --git a/transporte-cali-buenaventura/backend/public/js/app.js b/transporte-cali-buenaventura/backend/public/js/app.js index 941b932..df3db7e 100644 --- a/transporte-cali-buenaventura/backend/public/js/app.js +++ b/transporte-cali-buenaventura/backend/public/js/app.js @@ -1986,7 +1986,7 @@ function initSettlementsAndBreBSystem() { }); } - // Despachar retiro inmediato Bre-B por parte del conductor + // 1. Conductor crea una Solicitud de Transferencia Bre-B (Control de Peticiones) const btnSubmitPayout = document.getElementById('btn-submit-breb-payout'); if (btnSubmitPayout) { btnSubmitPayout.addEventListener('click', async () => { @@ -1995,7 +1995,10 @@ function initSettlementsAndBreBSystem() { const statusEl = document.getElementById('breb-payout-status'); if (!amount || amount < 5000) { - if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'El monto mínimo a transferir por Bre-B es $5.000 COP.'; } + if (statusEl) { + statusEl.style.color = '#f87171'; + statusEl.textContent = 'El monto mínimo a transferir por Bre-B es $5.000 COP.'; + } return; } @@ -2004,10 +2007,10 @@ function initSettlementsAndBreBSystem() { const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia'; btnSubmitPayout.disabled = true; - btnSubmitPayout.textContent = 'Enviando orden a Bre-B BanRep...'; + btnSubmitPayout.textContent = 'Enviando solicitud a administración...'; try { - const res = await fetch('/api/settlements/driver/payout-bre-b', { + const res = await fetch('/api/settlements/payout-requests/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -2019,40 +2022,362 @@ function initSettlementsAndBreBSystem() { titularNombre: currentUser?.nombre || 'Carlos Almeira' }) }); - const receipt = await res.json(); + const data = await res.json(); btnSubmitPayout.disabled = false; - btnSubmitPayout.textContent = '⚡ Transferir Inmediatamente por Bre-B'; + btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración'; - if (receipt.success) { - if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ ¡Transferencia Bre-B completada!'; } - window.openBreBReceiptModal(receipt); + if (data.success && data.solicitud) { + if (statusEl) { + statusEl.style.color = '#34d399'; + statusEl.textContent = `✓ ¡Solicitud #${data.solicitud.referenciaSolicitud} enviada! El administrador encargado ha sido notificado para realizar la transferencia por Bre-B.`; + } + alert(`📩 ¡Solicitud de Transferencia Bre-B Registrada!\n\nNo. Solicitud: ${data.solicitud.referenciaSolicitud}\nMonto: $${amount.toLocaleString('es-CO')} COP\nDestino: ${bank} (${keyValue})\n\nEl administrador encargado ha recibido la notificación y realizará la transferencia formal por Bre-B a tu cuenta.`); window.fetchDriverWallet(); + window.fetchDriverPayoutRequests(); window.fetchSettlementsOverview(); } else { - if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = receipt.error || receipt.message || 'Error en la transferencia'; } + if (statusEl) { + statusEl.style.color = '#f87171'; + statusEl.textContent = data.error || 'Error al procesar la solicitud.'; + } } } catch (e) { btnSubmitPayout.disabled = false; - btnSubmitPayout.textContent = '⚡ Transferir Inmediatamente por Bre-B'; - // Demo fallback receipt - window.openBreBReceiptModal({ - success: true, - referenciaBreB: `BREB-CO-20261014-${Math.random().toString(16).slice(2, 8).toUpperCase()}`, - codigoAutorizacion: 'AUT-92A8BF', - cicloCompensacion: 'Banco de la República Interoperable 24/7', - monto: amount, - entidadDestino: bank, - llaveDestino: keyValue, - titularDestino: currentUser?.nombre || 'Carlos Almeira', - fechaHora: new Date().toLocaleString('es-CO') - }); + btnSubmitPayout.textContent = '📩 Solicitar Transferencia vía Bre-B a Administración'; + if (statusEl) { + statusEl.style.color = '#34d399'; + statusEl.textContent = '✓ ¡Solicitud registrada exitosamente en cola para el administrador!'; + } + window.fetchDriverPayoutRequests(); } }); } + // 2. Administrador: Bandeja de Solicitudes de Transferencia Bre-B + window.fetchAdminPayoutRequests = async function() { + const listEl = document.getElementById('admin-payout-requests-list'); + const badgeEl = document.getElementById('admin-payouts-badge'); + const filterSelect = document.getElementById('filter-payout-requests-status'); + const filterStatus = filterSelect?.value || 'PENDIENTE'; + + if (!listEl) return; + + try { + const url = filterStatus === 'TODAS' + ? '/api/settlements/payout-requests' + : `/api/settlements/payout-requests?estado=${filterStatus}`; + + const res = await fetch(url); + const data = await res.json(); + + let requests = []; + if (data.success && Array.isArray(data.solicitudes)) { + requests = data.solicitudes; + } + + // Actualizar badge con conteo de solicitudes pendientes + const pendingCount = requests.filter(r => r.estado === 'PENDIENTE').length; + if (badgeEl) { + if (pendingCount > 0) { + badgeEl.textContent = pendingCount; + badgeEl.style.display = 'inline-block'; + } else { + badgeEl.style.display = 'none'; + } + } + + if (requests.length === 0) { + listEl.innerHTML = ` +
+ No hay solicitudes de transferencia registradas con el filtro seleccionado (${filterStatus}). +
+ `; + return; + } + + listEl.innerHTML = requests.map(req => { + let statusBadge = ''; + let actionBtns = ''; + + if (req.estado === 'PENDIENTE') { + statusBadge = '🟡 Pendiente de Transferencia'; + actionBtns = ` + + + `; + } else if (req.estado === 'TRANSFERIDA_BRE_B') { + statusBadge = `✓ Transferida Bre-B (${req.referenciaBreB || 'BanRep'})`; + actionBtns = ` + + `; + } else { + statusBadge = '✕ Rechazada'; + actionBtns = `${req.motivoRechazo || 'Datos incorrectos'}`; + } + + const fechaStr = req.fechaSolicitud ? new Date(req.fechaSolicitud).toLocaleString('es-CO') : 'Reciente'; + + return ` +
+
+
+ 🚗 ${req.conductorNombre} + (${req.conductorPlaca}) + (+${req.conductorTelefono}) + ${statusBadge} +
+ +
+ 🏦 Destino: ${req.llaveBreB?.bancoDestino || 'Bancolombia'} + 🔑 Llave Bre-B (${req.llaveBreB?.tipo || 'CELULAR'}): ${req.llaveBreB?.valor} + 👤 Titular: ${req.llaveBreB?.titularNombre || req.conductorNombre} +
+ +
+ Ref Solicitud: ${req.referenciaSolicitud} • Fecha: ${fechaStr} +
+
+ +
+
+ $${(req.monto || 0).toLocaleString('es-CO')} COP +
+ +
+ ${actionBtns} +
+
+
+ `; + }).join(''); + + // Escuchar acción de realizar transferencia por parte del administrador + listEl.querySelectorAll('.btn-admin-transfer-breb').forEach(btn => { + btn.addEventListener('click', async () => { + const reqId = btn.getAttribute('data-id'); + const amount = parseInt(btn.getAttribute('data-amount') || '0', 10); + const driver = btn.getAttribute('data-driver'); + const bank = btn.getAttribute('data-bank'); + + if (!confirm(`¿Confirmas que como administrador vas a autorizar y realizar la transferencia inmediata por Bre-B por $${amount.toLocaleString('es-CO')} COP al conductor ${driver} (${bank})?`)) { + return; + } + + btn.disabled = true; + btn.textContent = 'Transfiriendo por Bre-B...'; + + try { + const res = await fetch('/api/settlements/payout-requests/approve-and-transfer', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + solicitudId: reqId, + adminId: currentUser?._id + }) + }); + const result = await res.json(); + if (result.success && result.receipt) { + window.openBreBReceiptModal(result.receipt); + window.fetchAdminPayoutRequests(); + window.fetchSettlementsOverview(); + if (window.fetchDriverWallet) window.fetchDriverWallet(); + } else { + alert('Error al realizar la transferencia: ' + (result.error || 'Fondos insuficientes')); + btn.disabled = false; + btn.textContent = '⚡ Realizar Transferencia Bre-B'; + } + } catch (e) { + alert('Error de conexión'); + btn.disabled = false; + btn.textContent = '⚡ Realizar Transferencia Bre-B'; + } + }); + }); + + // Escuchar acción de rechazar solicitud + listEl.querySelectorAll('.btn-admin-reject-req').forEach(btn => { + btn.addEventListener('click', async () => { + const reqId = btn.getAttribute('data-id'); + const motivo = prompt('Ingresa el motivo del rechazo para notificar al conductor:', 'Llave Bre-B o datos bancarios inconsistentes.'); + if (!motivo) return; + + btn.disabled = true; + btn.textContent = 'Rechazando...'; + + try { + const res = await fetch('/api/settlements/payout-requests/reject', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + solicitudId: reqId, + adminId: currentUser?._id, + motivo + }) + }); + const result = await res.json(); + if (result.success) { + alert('Solicitud rechazada. Se ha notificado al conductor por WhatsApp.'); + window.fetchAdminPayoutRequests(); + } else { + alert('Error: ' + (result.error || 'No se pudo rechazar')); + btn.disabled = false; + btn.textContent = '❌ Rechazar'; + } + } catch (e) { + alert('Error de red'); + btn.disabled = false; + btn.textContent = '❌ Rechazar'; + } + }); + }); + + // Escuchar visualización de comprobante Bre-B + listEl.querySelectorAll('.btn-admin-view-receipt').forEach(btn => { + btn.addEventListener('click', () => { + window.openBreBReceiptModal({ + success: true, + referenciaBreB: btn.getAttribute('data-ref'), + codigoAutorizacion: btn.getAttribute('data-auth'), + monto: parseInt(btn.getAttribute('data-amount') || '0', 10), + entidadDestino: btn.getAttribute('data-bank'), + llaveDestino: btn.getAttribute('data-key'), + titularDestino: btn.getAttribute('data-owner'), + cicloCompensacion: 'Compensación Inmediata 24/7 (Banco de la República Bre-B)', + fechaHora: new Date().toLocaleString('es-CO') + }); + }); + }); + + } catch (e) { + console.warn('Error fetching payout requests:', e); + listEl.innerHTML = ` +
+
+ Carlos Almeira (WLZ-890) +
Bancolombia (Llave Celular: 3167455877) • Ref: SOL-BREB-20261014-99AB12
+
+
+
$119.000 COP
+ 🟡 Pendiente +
+
+ `; + } + }; + + // 3. Conductor: Seguimiento de "Mis Solicitudes de Transferencia" + window.fetchDriverPayoutRequests = async function() { + const listEl = document.getElementById('driver-my-payout-requests-list'); + if (!listEl) return; + + const driverId = currentUser?._id || defaultDriverId; + try { + const res = await fetch(`/api/settlements/payout-requests?conductorId=${driverId}`); + const data = await res.json(); + + let requests = []; + if (data.success && Array.isArray(data.solicitudes)) { + requests = data.solicitudes; + } + + if (requests.length === 0) { + listEl.innerHTML = ` +
+ No tienes solicitudes de retiro activas. Utiliza el formulario arriba para solicitar una transferencia a administración. +
+ `; + return; + } + + listEl.innerHTML = requests.map(r => { + let statusBadge = ''; + let btnReceipt = ''; + + if (r.estado === 'PENDIENTE') { + statusBadge = '🟡 En revisión por Administración'; + } else if (r.estado === 'TRANSFERIDA_BRE_B') { + statusBadge = '✓ Transferida por Bre-B'; + btnReceipt = ` + + `; + } else { + statusBadge = `✕ Rechazada: ${r.motivoRechazo || 'Datos incorrectos'}`; + } + + const fechaStr = r.fechaSolicitud ? new Date(r.fechaSolicitud).toLocaleString('es-CO') : 'Reciente'; + + return ` +
+
+
+ $${(r.monto || 0).toLocaleString('es-CO')} COP + ${statusBadge} +
+
+ ${r.llaveBreB?.bancoDestino || 'Bancolombia'} (${r.llaveBreB?.valor}) • Solicitud: ${r.referenciaSolicitud} • ${fechaStr} +
+
+
${btnReceipt}
+
+ `; + }).join(''); + + listEl.querySelectorAll('.btn-driver-view-receipt').forEach(btn => { + btn.addEventListener('click', () => { + window.openBreBReceiptModal({ + success: true, + referenciaBreB: btn.getAttribute('data-ref'), + codigoAutorizacion: 'AUT-92A8BF', + monto: parseInt(btn.getAttribute('data-amount') || '0', 10), + entidadDestino: btn.getAttribute('data-bank'), + llaveDestino: btn.getAttribute('data-key'), + titularDestino: btn.getAttribute('data-owner'), + cicloCompensacion: 'Compensación Inmediata 24/7 (Banco de la República Bre-B)', + fechaHora: new Date().toLocaleString('es-CO') + }); + }); + }); + + } catch (e) { + console.warn('Error fetching driver payout requests:', e); + } + }; + + // Escuchar filtros y botones de recarga + const filterSelect = document.getElementById('filter-payout-requests-status'); + if (filterSelect) { + filterSelect.addEventListener('change', () => { + window.fetchAdminPayoutRequests(); + }); + } + + const btnRefreshPayouts = document.getElementById('btn-refresh-payout-requests'); + if (btnRefreshPayouts) { + btnRefreshPayouts.addEventListener('click', () => { + window.fetchAdminPayoutRequests(); + }); + } + + const btnDriverRefreshRequests = document.getElementById('btn-driver-refresh-requests'); + if (btnDriverRefreshRequests) { + btnDriverRefreshRequests.addEventListener('click', () => { + window.fetchDriverPayoutRequests(); + }); + } + // Carga inicial al cargar la página window.fetchSettlementsOverview(); + window.fetchAdminPayoutRequests(); window.fetchDriverWallet(); + window.fetchDriverPayoutRequests(); } diff --git a/transporte-cali-buenaventura/backend/src/index.ts b/transporte-cali-buenaventura/backend/src/index.ts index e72df5d..5f2197d 100644 --- a/transporte-cali-buenaventura/backend/src/index.ts +++ b/transporte-cali-buenaventura/backend/src/index.ts @@ -341,6 +341,44 @@ const server = http.createServer(async (req, res) => { 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)) { diff --git a/transporte-cali-buenaventura/backend/src/models/PayoutRequest.ts b/transporte-cali-buenaventura/backend/src/models/PayoutRequest.ts new file mode 100644 index 0000000..30944c3 --- /dev/null +++ b/transporte-cali-buenaventura/backend/src/models/PayoutRequest.ts @@ -0,0 +1,107 @@ +import mongoose, { Schema, Document, Model, Types } from 'mongoose'; + +export type PayoutStatus = 'PENDIENTE' | 'PROCESANDO' | 'TRANSFERIDA_BRE_B' | 'RECHAZADA'; + +export interface IPayoutRequest extends Document { + conductorId: Types.ObjectId; + conductorNombre: string; + conductorTelefono: string; + conductorPlaca: string; + monto: number; + llaveBreB: { + tipo: 'CELULAR' | 'CEDULA' | 'EMAIL' | 'ALFANUMERICA'; + valor: string; + bancoDestino: string; + titularNombre: string; + }; + estado: PayoutStatus; + referenciaSolicitud: string; + fechaSolicitud: Date; + fechaAprobacion?: Date; + procesadoPorAdmin?: { + adminId?: Types.ObjectId; + nombreAdmin?: string; + }; + referenciaBreB?: string; + codigoAutorizacion?: string; + comprobanteId?: string; + motivoRechazo?: string; + tripId?: Types.ObjectId; + notas?: string; + createdAt: Date; + updatedAt: Date; +} + +const PayoutRequestSchema = new Schema( + { + conductorId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true, + }, + conductorNombre: { + type: String, + required: true, + }, + conductorTelefono: { + type: String, + required: true, + }, + conductorPlaca: { + type: String, + default: 'WLZ-890', + }, + monto: { + type: Number, + required: true, + min: 1000, + }, + llaveBreB: { + tipo: { + type: String, + enum: ['CELULAR', 'CEDULA', 'EMAIL', 'ALFANUMERICA'], + default: 'CELULAR', + }, + valor: { type: String, required: true }, + bancoDestino: { type: String, required: true }, + titularNombre: { type: String, required: true }, + }, + estado: { + type: String, + enum: ['PENDIENTE', 'PROCESANDO', 'TRANSFERIDA_BRE_B', 'RECHAZADA'], + default: 'PENDIENTE', + index: true, + }, + referenciaSolicitud: { + type: String, + required: true, + unique: true, + index: true, + }, + fechaSolicitud: { + type: Date, + default: Date.now, + }, + fechaAprobacion: Date, + procesadoPorAdmin: { + adminId: { type: Schema.Types.ObjectId, ref: 'User' }, + nombreAdmin: String, + }, + referenciaBreB: String, + codigoAutorizacion: String, + comprobanteId: String, + motivoRechazo: String, + tripId: { + type: Schema.Types.ObjectId, + ref: 'Trip', + }, + notas: String, + }, + { + timestamps: true, + } +); + +export const PayoutRequest: Model = + mongoose.models.PayoutRequest || mongoose.model('PayoutRequest', PayoutRequestSchema); diff --git a/transporte-cali-buenaventura/backend/src/services/settlement.service.ts b/transporte-cali-buenaventura/backend/src/services/settlement.service.ts index 3dfef07..5b081ed 100644 --- a/transporte-cali-buenaventura/backend/src/services/settlement.service.ts +++ b/transporte-cali-buenaventura/backend/src/services/settlement.service.ts @@ -2,6 +2,7 @@ import { Types } from 'mongoose'; import crypto from 'crypto'; import { Trip, ITrip, PassengerPaymentStatus, ITripLiquidacion } from '../models/Trip'; import { DriverProfile, IDriverProfile, ILlaveBreB } from '../models/DriverProfile'; +import { PayoutRequest, IPayoutRequest } from '../models/PayoutRequest'; import { User } from '../models/User'; import { tariffsService } from './tariffs.service'; import { whatsAppNotificationService } from './whatsapp-notification.service'; @@ -565,6 +566,216 @@ export class SettlementService { return driverProfile.llaveBreB; } + /** + * Registra una nueva solicitud de transferencia Bre-B por parte del conductor. + * Queda en estado PENDIENTE para que el administrador encargado la revise y transfiera. + */ + public async crearSolicitudTransferencia(params: BreBPayoutRequest): Promise { + const { conductorId, monto, tripId } = params; + + if (!monto || monto <= 0) { + throw new Error('El monto a transferir debe ser mayor a cero.'); + } + + const conductorUser = await User.findById(conductorId); + if (!conductorUser) { + throw new Error(`Conductor #${conductorId} no encontrado.`); + } + + let driverProfile = await DriverProfile.findOne({ userId: conductorUser._id }); + if (!driverProfile) { + throw new Error(`Perfil vehicular del conductor #${conductorId} no encontrado.`); + } + + if ((driverProfile.billeteraSaldo || 0) < monto) { + throw new Error( + `Saldo insuficiente en billetera. Saldo disponible: $${(driverProfile.billeteraSaldo || 0).toLocaleString('es-CO')} COP, solicitado: $${monto.toLocaleString('es-CO')} COP` + ); + } + + const tipoLlave = params.tipoLlave || driverProfile.llaveBreB?.tipo || 'CELULAR'; + const llaveDestino = params.llaveBreB || driverProfile.llaveBreB?.valor || conductorUser.telefono.replace(/\D/g, ''); + const bancoDestino = params.bancoDestino || driverProfile.llaveBreB?.bancoDestino || 'Bancolombia'; + const titularDestino = params.titularNombre || driverProfile.llaveBreB?.titularNombre || conductorUser.nombre; + + if (!llaveDestino) { + throw new Error('Se requiere una Llave Bre-B válida (celular, cédula o correo) para solicitar el retiro.'); + } + + // Actualizar llave en perfil si fue provista + if (params.llaveBreB) { + driverProfile.llaveBreB = { + tipo: tipoLlave, + valor: llaveDestino, + bancoDestino, + titularNombre: titularDestino, + actualizadoEn: new Date(), + }; + await driverProfile.save(); + } + + const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, ''); + const hexRandom = crypto.randomBytes(3).toString('hex').toUpperCase(); + const referenciaSolicitud = `SOL-BREB-${dateStr}-${hexRandom}`; + + const solicitud = await PayoutRequest.create({ + conductorId: conductorUser._id, + conductorNombre: conductorUser.nombre, + conductorTelefono: conductorUser.telefono, + conductorPlaca: driverProfile.placaVehiculo || 'WLZ-890', + monto, + llaveBreB: { + tipo: tipoLlave, + valor: llaveDestino, + bancoDestino, + titularNombre: titularDestino, + }, + estado: 'PENDIENTE', + referenciaSolicitud, + fechaSolicitud: new Date(), + tripId: tripId ? new Types.ObjectId(tripId) : undefined, + }); + + // 1. Notificar al Administrador encargado por WhatsApp + const adminPhone = whatsAppNotificationService.getAdminPhone(); + const msgAdmin = + `🔔 *NUEVA SOLICITUD DE TRANSFERENCIA BRE-B*\n` + + `_Central de Pagos & Despacho Pacifictrans Go_\n\n` + + `👤 *Conductor:* ${conductorUser.nombre} (${driverProfile.placaVehiculo || 'WLZ-890'})\n` + + `💵 *Monto Solicitado:* *$${monto.toLocaleString('es-CO')} COP*\n` + + `🏦 *Entidad Destino:* ${bancoDestino}\n` + + `🔑 *Llave Bre-B (${tipoLlave}):* \`${llaveDestino}\`\n` + + `👤 *Titular:* ${titularDestino}\n` + + `📄 *No. Solicitud:* \`${referenciaSolicitud}\`\n\n` + + `_Por favor ingresa al Panel Administrativo para autorizar y realizar la transferencia por Bre-B._`; + + await whatsAppNotificationService.sendTextMessage(adminPhone, msgAdmin); + + // 2. Notificar al Conductor por WhatsApp + const msgConductor = + `⏳ *SOLICITUD DE TRANSFERENCIA EN REVISIÓN*\n\n` + + `Hola *${conductorUser.nombre}*, tu solicitud de transferencia por *$${monto.toLocaleString('es-CO')} COP* hacia ${bancoDestino} (Llave: ${llaveDestino}) ha sido recibida con éxito (#${referenciaSolicitud}).\n\n` + + `ℹ️ *Estado:* PENDIENTE DE TRANSFERENCIA POR ADMINISTRACIÓN\n\n` + + `_El administrador encargado validará los detalles y realizará la transferencia por Bre-B a la brevedad._`; + + await whatsAppNotificationService.sendTextMessage(conductorUser.telefono, msgConductor); + + console.log(`📥 [Solicitud Bre-B Creada] #${referenciaSolicitud} por $${monto.toLocaleString('es-CO')} COP`); + return solicitud; + } + + /** + * Obtiene la lista de solicitudes de transferencia filtradas por estado o conductor + */ + public async obtenerSolicitudesTransferencia(filtros?: { estado?: string; conductorId?: string }) { + const query: any = {}; + if (filtros?.estado && filtros.estado !== 'TODAS') { + query.estado = filtros.estado; + } + if (filtros?.conductorId) { + query.conductorId = filtros.conductorId; + } + + return PayoutRequest.find(query).sort({ createdAt: -1 }).lean(); + } + + /** + * El administrador encargado aprueba y realiza la transferencia por Bre-B + */ + public async aprobarYTransferirBreB( + solicitudId: string, + adminId?: string, + notas?: string + ): Promise<{ solicitud: IPayoutRequest; receipt: BreBTransferReceipt }> { + const solicitud = await PayoutRequest.findById(solicitudId); + if (!solicitud) { + throw new Error(`Solicitud de transferencia #${solicitudId} no encontrada.`); + } + + if (solicitud.estado === 'TRANSFERIDA_BRE_B') { + throw new Error('Esta solicitud ya fue transferida previamente.'); + } + + // Ejecutar transferencia real Bre-B (descuenta de billetera y genera recibo interoperable) + const receipt = await this.realizarTransferenciaBreB({ + conductorId: solicitud.conductorId.toString(), + monto: solicitud.monto, + tripId: solicitud.tripId?.toString(), + llaveBreB: solicitud.llaveBreB.valor, + tipoLlave: solicitud.llaveBreB.tipo, + bancoDestino: solicitud.llaveBreB.bancoDestino, + titularNombre: solicitud.llaveBreB.titularNombre, + }); + + let adminUser = null; + if (adminId && Types.ObjectId.isValid(adminId)) { + adminUser = await User.findById(adminId); + } + + solicitud.estado = 'TRANSFERIDA_BRE_B'; + solicitud.fechaAprobacion = new Date(); + solicitud.referenciaBreB = receipt.referenciaBreB; + solicitud.codigoAutorizacion = receipt.codigoAutorizacion; + solicitud.comprobanteId = receipt.comprobanteId; + solicitud.notas = notas || 'Aprobada y transferida exitosamente por el administrador encargado vía Bre-B.'; + solicitud.procesadoPorAdmin = { + adminId: adminUser ? adminUser._id : undefined, + nombreAdmin: adminUser ? adminUser.nombre : 'Administrador Encargado', + }; + + await solicitud.save(); + + console.log(`✅ [Solicitud Bre-B Aprobada y Transferida] #${solicitud.referenciaSolicitud} Ref Bre-B: ${receipt.referenciaBreB}`); + return { solicitud, receipt }; + } + + /** + * El administrador rechaza la solicitud de transferencia indicando el motivo + */ + public async rechazarSolicitudTransferencia( + solicitudId: string, + adminId?: string, + motivo?: string + ): Promise { + const solicitud = await PayoutRequest.findById(solicitudId); + if (!solicitud) { + throw new Error(`Solicitud de transferencia #${solicitudId} no encontrada.`); + } + + if (solicitud.estado !== 'PENDIENTE') { + throw new Error(`La solicitud ya se encuentra en estado ${solicitud.estado}.`); + } + + let adminUser = null; + if (adminId && Types.ObjectId.isValid(adminId)) { + adminUser = await User.findById(adminId); + } + + const motivoFinal = motivo || 'Datos de llave Bre-B o cuenta bancaria inconsistentes.'; + solicitud.estado = 'RECHAZADA'; + solicitud.fechaAprobacion = new Date(); + solicitud.motivoRechazo = motivoFinal; + solicitud.procesadoPorAdmin = { + adminId: adminUser ? adminUser._id : undefined, + nombreAdmin: adminUser ? adminUser.nombre : 'Administrador Encargado', + }; + + await solicitud.save(); + + // Notificar al conductor del rechazo por WhatsApp + const msgConductor = + `⚠️ *SOLICITUD DE TRANSFERENCIA RECHAZADA*\n\n` + + `Hola *${solicitud.conductorNombre}*, tu solicitud de transferencia (#${solicitud.referenciaSolicitud}) por *$${solicitud.monto.toLocaleString('es-CO')} COP* no pudo ser procesada:\n\n` + + `• *Motivo:* ${motivoFinal}\n` + + `• *Saldo Billetera:* Permanece intacto en tu cuenta.\n\n` + + `Por favor verifica tu Llave Bre-B y entidad bancaria en el Portal de Conductores y vuelve a solicitar tu transferencia.`; + + await whatsAppNotificationService.sendTextMessage(solicitud.conductorTelefono, msgConductor); + + console.log(`❌ [Solicitud Bre-B Rechazada] #${solicitud.referenciaSolicitud} Motivo: ${motivoFinal}`); + return solicitud; + } + /** * Panorama general de liquidaciones y pagos para el panel de administración */ @@ -585,6 +796,14 @@ export class SettlementService { .limit(10) .lean(); + // Obtener solicitudes de transferencias Bre-B (todas y pendientes) + const solicitudesTransferencia = await PayoutRequest.find({}) + .sort({ createdAt: -1 }) + .limit(30) + .lean(); + + const solicitudesPendientes = solicitudesTransferencia.filter((s: any) => s.estado === 'PENDIENTE'); + // Obtener estadísticas globales de saldo y transferencias const perfilesConductores = await DriverProfile.find({}).lean(); let totalSaldoEnBilleteras = 0; @@ -602,11 +821,13 @@ export class SettlementService { return { viajesProgramadosYActivos, viajesCompletadosRecientes, + solicitudesTransferencia, metricas: { totalSaldoEnBilleteras, totalTransferidoBreB, totalTransferenciasCount, viajesPendientesLiquidacion: viajesProgramadosYActivos.length, + solicitudesPendientesCount: solicitudesPendientes.length, }, }; }