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,
},
};
}