feat: Control de pagos por viaje planificado (Bold/Bre-B vs Efectivo), liquidacion de saldo a conductor y transferencias inmediatas Bre-B
This commit is contained in:
parent
20b48cfd5a
commit
7d5087a8d0
@ -1471,5 +1471,274 @@ h1, h2, h3, h4, .brand-title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
ESTILOS CONTROL DE PAGOS, COMISIONES & BRE-B (SISTEMA INTEROPERABLE BANREP)
|
||||
========================================================================== */
|
||||
|
||||
.badge-payment {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.28rem 0.65rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.badge-payment-online {
|
||||
background: rgba(6, 182, 212, 0.15);
|
||||
color: #38bdf8;
|
||||
border: 1px solid rgba(6, 182, 212, 0.4);
|
||||
}
|
||||
|
||||
.badge-payment-breb {
|
||||
background: linear-gradient(135deg, rgba(139, 92, 246, 0.25), rgba(236, 72, 153, 0.25));
|
||||
color: #f472b6;
|
||||
border: 1px solid rgba(217, 70, 239, 0.4);
|
||||
}
|
||||
|
||||
.badge-payment-cash-pending {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #fbbf24;
|
||||
border: 1px solid rgba(245, 158, 11, 0.4);
|
||||
}
|
||||
|
||||
.badge-payment-cash-paid {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #34d399;
|
||||
border: 1px solid rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.breb-brand-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background: linear-gradient(135deg, #7c3aed, #db2777);
|
||||
color: #fff;
|
||||
font-weight: 800;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.05em;
|
||||
box-shadow: 0 2px 10px rgba(219, 39, 119, 0.3);
|
||||
}
|
||||
|
||||
.passenger-payment-card {
|
||||
background: rgba(15, 23, 42, 0.8);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.85rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.passenger-payment-card:hover {
|
||||
border-color: rgba(6, 182, 212, 0.4);
|
||||
background: rgba(15, 23, 42, 0.95);
|
||||
}
|
||||
|
||||
.passenger-payment-card.pending-cash {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
.passenger-payment-card.paid-online {
|
||||
border-left: 4px solid #06b6d4;
|
||||
}
|
||||
|
||||
.passenger-payment-card.paid-cash {
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
.btn-toggle-payment {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1px solid var(--border-glass);
|
||||
color: #fff;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.btn-toggle-payment:hover {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
border-color: #10b981;
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.btn-toggle-payment.revert:hover {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
border-color: #f59e0b;
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.financial-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
.fin-metric-card {
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
border: 1px solid var(--border-glass);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fin-metric-val {
|
||||
font-size: 1.45rem;
|
||||
font-weight: 800;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.fin-metric-label {
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.driver-wallet-container {
|
||||
background: linear-gradient(135deg, rgba(15, 23, 42, 0.95), rgba(30, 27, 75, 0.75));
|
||||
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1.75rem;
|
||||
margin-bottom: 2rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.driver-wallet-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: -40px;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
background: radial-gradient(circle, rgba(236, 72, 153, 0.2) 0%, rgba(139, 92, 246, 0) 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-balance-display {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
margin: 0.5rem 0 1rem;
|
||||
}
|
||||
|
||||
.wallet-balance-amount {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, #38bdf8, #a855f7, #ec4899);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wallet-balance-currency {
|
||||
font-size: 1rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-breb-action {
|
||||
background: linear-gradient(135deg, #7c3aed, #db2777);
|
||||
color: #fff;
|
||||
border: none;
|
||||
font-weight: 700;
|
||||
padding: 0.85rem 1.75rem;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 4px 15px rgba(219, 39, 119, 0.35);
|
||||
}
|
||||
|
||||
.btn-breb-action:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(219, 39, 119, 0.5);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
/* MODAL COMPROBANTE OFICIAL BRE-B */
|
||||
.breb-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.breb-receipt-container {
|
||||
background: #0f172a;
|
||||
border: 1px solid rgba(139, 92, 246, 0.4);
|
||||
border-radius: 16px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7), 0 0 30px rgba(139, 92, 246, 0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.breb-receipt-header {
|
||||
background: linear-gradient(135deg, #4c1d95, #831843);
|
||||
padding: 1.5rem;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.breb-receipt-body {
|
||||
padding: 1.75rem;
|
||||
}
|
||||
|
||||
.receipt-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.65rem 0;
|
||||
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.receipt-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.receipt-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.receipt-val {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -414,10 +414,155 @@
|
||||
<section class="tab-content" id="tab-conductores">
|
||||
<div class="glass-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Vinculación Oficial de Conductores</h2>
|
||||
<p>Únete a la flota oficial de Pacifictrans Go. Registra tu vehículo y sube tus documentos para validación administrativa.</p>
|
||||
<h2>Portal de Conductores & Billetera Bre-B</h2>
|
||||
<p>Monitorea tus ingresos netos por recorridos, consulta el saldo pendiente por cancelar por la plataforma y transfiere tu dinero inmediatamente a tu cuenta bancaria a través de Bre-B.</p>
|
||||
|
||||
<div style="display: flex; justify-content: center; gap: 0.75rem; margin-top: 1.5rem; flex-wrap: wrap;">
|
||||
<button type="button" id="btn-driver-subtab-billetera" class="admin-pill-btn active">
|
||||
💼 1. Mi Billetera & Retiros Bre-B
|
||||
</button>
|
||||
<button type="button" id="btn-driver-subtab-registro" class="admin-pill-btn">
|
||||
📝 2. Registro & Documentación
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SUB-PANEL 1: BILLETERA DIGITAL & PAGOS INMEDIATOS BRE-B -->
|
||||
<div id="driver-view-billetera">
|
||||
<!-- TARJETA PRINCIPAL DE SALDO -->
|
||||
<div class="driver-wallet-container">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 1rem;">
|
||||
<div>
|
||||
<span class="breb-brand-badge">⚡ Bre-B Interoperable BanRep</span>
|
||||
<div style="color: #94a3b8; font-size: 0.85rem; margin-top: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700;">
|
||||
Saldo Disponible para Transferir
|
||||
</div>
|
||||
<div class="wallet-balance-display">
|
||||
<span class="wallet-balance-amount" id="driver-wallet-balance-val">$119.000</span>
|
||||
<span class="wallet-balance-currency">COP</span>
|
||||
</div>
|
||||
<div style="color: #cbd5e1; font-size: 0.88rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🚗 Conductor: <strong id="driver-wallet-name" style="color: #fff;">Carlos Almeira</strong></span>
|
||||
<span>•</span>
|
||||
<span>Placa: <strong id="driver-wallet-plate" style="color: var(--accent-cyan);">WLZ-890</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right;">
|
||||
<button type="button" id="btn-driver-refresh-wallet" class="btn-toggle-payment" style="margin-bottom: 0.75rem;">
|
||||
🔄 Actualizar Saldo
|
||||
</button>
|
||||
<div style="font-size: 0.8rem; color: #94a3b8;">
|
||||
Total viajes completados: <strong id="driver-total-trips-count" style="color: #fff;">0</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GRID OPERACIONES BRE-B -->
|
||||
<div class="grid-2col" style="gap: 1.5rem;">
|
||||
<!-- COLUMNA 1: SOLICITAR TRANSFERENCIA BRE-B -->
|
||||
<div style="background: rgba(15, 23, 42, 0.8); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem;">
|
||||
<h3 style="color: #fff; font-size: 1.15rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>💸</span> Solicitar Transferencia Inmediata vía Bre-B
|
||||
</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.25rem;">
|
||||
Transfiere tus ganancias acumuladas de inmediato (24/7) sin cobro de comisión bancaria ($0 COP).
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="breb-payout-amount">Monto a Transferir ($ COP)</label>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<input type="number" class="form-input" id="breb-payout-amount" placeholder="Ej: 119000" min="5000" step="1000" style="flex: 1;">
|
||||
<button type="button" id="btn-breb-amount-all" class="btn-toggle-payment" style="white-space: nowrap;">
|
||||
Todo el Saldo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: rgba(139, 92, 246, 0.08); border: 1px dashed rgba(139, 92, 246, 0.3); border-radius: 8px; padding: 0.85rem; margin-bottom: 1.25rem;">
|
||||
<div style="font-size: 0.82rem; color: #cbd5e1; display: flex; justify-content: space-between; margin-bottom: 0.35rem;">
|
||||
<span>Destino:</span>
|
||||
<strong id="breb-summary-bank" style="color: #fff;">Bancolombia</strong>
|
||||
</div>
|
||||
<div style="font-size: 0.82rem; color: #cbd5e1; display: flex; justify-content: space-between; margin-bottom: 0.35rem;">
|
||||
<span>Llave Bre-B:</span>
|
||||
<strong id="breb-summary-key" style="color: #a855f7;">+57 316 745 5877</strong>
|
||||
</div>
|
||||
<div style="font-size: 0.82rem; color: #cbd5e1; display: flex; justify-content: space-between;">
|
||||
<span>Comisión de Transferencia:</span>
|
||||
<strong style="color: #10b981;">$0 COP (Gratis BanRep)</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" id="btn-submit-breb-payout" class="btn-breb-action" style="width: 100%;">
|
||||
⚡ Transferir Inmediatamente por Bre-B
|
||||
</button>
|
||||
<div id="breb-payout-status" style="margin-top: 0.75rem; font-size: 0.85rem; text-align: center;"></div>
|
||||
</div>
|
||||
|
||||
<!-- COLUMNA 2: CONFIGURACIÓN DE LLAVE BRE-B -->
|
||||
<div style="background: rgba(15, 23, 42, 0.8); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem;">
|
||||
<h3 style="color: #fff; font-size: 1.15rem; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🔑</span> Mi Llave Bre-B Oficial
|
||||
</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem; margin-bottom: 1.25rem;">
|
||||
Registra tu identificador único en el sistema de pagos interoperables del Banco de la República.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="driver-breb-key-type">Tipo de Llave Bre-B</label>
|
||||
<select class="form-select" id="driver-breb-key-type">
|
||||
<option value="CELULAR">Número de Celular (+57)</option>
|
||||
<option value="CEDULA">Cédula de Ciudadanía (C.C.)</option>
|
||||
<option value="EMAIL">Correo Electrónico</option>
|
||||
<option value="ALFANUMERICA">Código Alfanumérico Único</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="driver-breb-key-value">Valor de la Llave Bre-B</label>
|
||||
<input type="text" class="form-input" id="driver-breb-key-value" value="3167455877" placeholder="Ej: 3167455877">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="driver-breb-bank">Entidad Financiera de Destino</label>
|
||||
<select class="form-select" id="driver-breb-bank">
|
||||
<option value="Bancolombia">Bancolombia</option>
|
||||
<option value="Nequi">Nequi</option>
|
||||
<option value="Davivienda">Davivienda</option>
|
||||
<option value="Daviplata">Daviplata</option>
|
||||
<option value="Dale!">Dale!</option>
|
||||
<option value="Nu Colombia">Nu Colombia</option>
|
||||
<option value="Banco de Bogotá">Banco de Bogotá</option>
|
||||
<option value="Banco Agrario">Banco Agrario de Colombia</option>
|
||||
<option value="Lulo Bank">Lulo Bank</option>
|
||||
<option value="Banco de Occidente">Banco de Occidente</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="button" id="btn-save-driver-breb-key" class="btn-primary" style="width: 100%;">
|
||||
💾 Guardar Llave Bre-B
|
||||
</button>
|
||||
<div id="driver-breb-key-status" style="margin-top: 0.75rem; font-size: 0.85rem; text-align: center;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HISTORIAL DE TRANSFERENCIAS CONDUCTOR -->
|
||||
<div style="background: rgba(15, 23, 42, 0.6); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem; margin-top: 1.75rem;">
|
||||
<h3 style="color: #fff; font-size: 1.15rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>📜</span> Historial de Transferencias Inmediatas Bre-B
|
||||
</h3>
|
||||
<div id="driver-breb-history-list">
|
||||
<div style="text-align: center; color: #94a3b8; padding: 1.5rem;">
|
||||
No hay transferencias registradas aún.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SUB-PANEL 2: VINCULACIÓN OFICIAL VEHICULAR -->
|
||||
<div id="driver-view-registro" style="display: none;">
|
||||
<div class="grid-2col">
|
||||
<div>
|
||||
<div class="form-group">
|
||||
@ -474,6 +619,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -602,6 +748,9 @@
|
||||
<button type="button" id="btn-admin-subtab-tarifas" class="admin-pill-btn">
|
||||
⚙️ 2. Tarifas & Canales de Atención
|
||||
</button>
|
||||
<button type="button" id="btn-admin-subtab-liquidaciones" class="admin-pill-btn">
|
||||
💰 3. Control de Pagos, Comisiones & Bre-B
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -749,6 +898,88 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- SUB-PANEL 3: CONTROL DE PAGOS, COMISIONES & TRANSFERENCIAS BRE-B -->
|
||||
<div id="admin-view-liquidaciones" style="display: none;">
|
||||
<!-- BANNER INFORMATIVO BRE-B -->
|
||||
<div style="background: linear-gradient(135deg, rgba(30, 27, 75, 0.85), rgba(88, 28, 135, 0.5)); border: 1px solid rgba(139, 92, 246, 0.4); border-radius: 12px; padding: 1.5rem; margin-bottom: 1.75rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.35rem;">
|
||||
<span class="breb-brand-badge">⚡ Bre-B BANREP</span>
|
||||
<strong style="color: #fff; font-size: 1.15rem;">Control Financiero & Pagos Interoperables</strong>
|
||||
</div>
|
||||
<p style="color: #cbd5e1; font-size: 0.88rem; margin: 0; max-width: 650px;">
|
||||
Monitorea el estado de pago individual de cada pasajero (en línea o efectivo). Al terminar el recorrido, la plataforma calcula la comisión oficial (15%) y asigna automáticamente el saldo al conductor para transferencias inmediatas 24/7 vía <strong>Bre-B</strong>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" id="btn-refresh-settlements" class="btn-toggle-payment" style="padding: 0.6rem 1.25rem;">
|
||||
🔄 Actualizar Balances
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPIS GLOBALES DE RECAUDO Y COMISIONES -->
|
||||
<div class="financial-summary-grid">
|
||||
<div class="fin-metric-card" style="border-top: 3px solid #06b6d4;">
|
||||
<div class="fin-metric-label">Recaudo Online Plataforma</div>
|
||||
<div class="fin-metric-val" id="metric-recaudo-plataforma" style="color: #38bdf8;">$0</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Bold / Nequi / Bre-B</span>
|
||||
</div>
|
||||
<div class="fin-metric-card" style="border-top: 3px solid #f59e0b;">
|
||||
<div class="fin-metric-label">Recaudos en Efectivo</div>
|
||||
<div class="fin-metric-val" id="metric-recaudo-efectivo" style="color: #fbbf24;">$0</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Cobrado en Mano por Chofer</span>
|
||||
</div>
|
||||
<div class="fin-metric-card" style="border-top: 3px solid #10b981;">
|
||||
<div class="fin-metric-label">Comisión Pacifictrans (15%)</div>
|
||||
<div class="fin-metric-val" id="metric-comision-total" style="color: #34d399;">$0</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Ingresos Operativos</span>
|
||||
</div>
|
||||
<div class="fin-metric-card" style="border-top: 3px solid #ec4899;">
|
||||
<div class="fin-metric-label">Transferido vía Bre-B</div>
|
||||
<div class="fin-metric-val" id="metric-transferido-breb" style="color: #f472b6;">$0</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Pagos Inmediatos 24/7</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GESTIÓN DEL VIAJE PLANIFICADO SELECCIONADO -->
|
||||
<div style="background: rgba(15, 23, 42, 0.75); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; margin-bottom: 1.25rem;">
|
||||
<div>
|
||||
<h3 style="color: #fff; font-size: 1.2rem; margin: 0; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>🚗</span> Control de Pagos del Viaje Planificado
|
||||
</h3>
|
||||
<p style="color: var(--text-muted); font-size: 0.85rem; margin: 0.25rem 0 0;">
|
||||
Verifica qué pasajero ya pagó en línea y quién tiene pago pendiente en efectivo al abordar.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="select-trip-to-settle" style="font-size: 0.8rem; color: var(--text-muted); margin-right: 0.5rem;">Seleccionar Viaje:</label>
|
||||
<select id="select-trip-to-settle" class="form-select" style="width: auto; display: inline-block; padding: 0.45rem 1rem;">
|
||||
<!-- Cargado dinámicamente con app.js -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONTENEDOR DINÁMICO DEL DETALLE DEL VIAJE -->
|
||||
<div id="trip-settlement-detail-container">
|
||||
<div style="text-align: center; padding: 2rem; color: #94a3b8;">
|
||||
Cargando información del viaje planificado...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HISTORIAL DE VIAJES LIQUIDADOS RECIENTES -->
|
||||
<div style="background: rgba(15, 23, 42, 0.6); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem;">
|
||||
<h3 style="color: #fff; font-size: 1.15rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>📋</span> Historial de Recorridos Completados & Liquidaciones
|
||||
</h3>
|
||||
<div id="settlements-history-list">
|
||||
<!-- Renderizado por app.js -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -994,6 +1225,76 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MODAL 4: COMPROBANTE OFICIAL TRANSFERENCIA BRE-B BANCO DE LA REPÚBLICA -->
|
||||
<div class="breb-modal-overlay" id="modal-breb-receipt" style="display: none;">
|
||||
<div class="breb-receipt-container">
|
||||
<div class="breb-receipt-header">
|
||||
<button type="button" id="btn-close-breb-modal" style="position: absolute; top: 1rem; right: 1rem; background: none; border: none; color: #fff; font-size: 1.25rem; cursor: pointer;">✕</button>
|
||||
<span class="breb-brand-badge" style="margin-bottom: 0.5rem;">⚡ Bre-B BANREP</span>
|
||||
<h3 style="color: #fff; font-size: 1.35rem; margin: 0.25rem 0 0.15rem;">Comprobante de Transferencia Inmediata</h3>
|
||||
<p style="color: #fbcfe8; font-size: 0.8rem; margin: 0;">Sistema Nacional de Pagos Interoperables de Colombia</p>
|
||||
</div>
|
||||
|
||||
<div class="breb-receipt-body">
|
||||
<div style="text-align: center; margin-bottom: 1.25rem;">
|
||||
<div style="display: inline-flex; align-items: center; gap: 0.35rem; background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); padding: 0.35rem 0.85rem; border-radius: 9999px; font-size: 0.82rem; font-weight: 700;">
|
||||
✓ TRANSFERENCIA APROBADA 24/7
|
||||
</div>
|
||||
<div style="font-size: 2.2rem; font-weight: 900; color: #fff; margin-top: 0.5rem;" id="modal-receipt-amount">
|
||||
$119.000 COP
|
||||
</div>
|
||||
<div style="font-size: 0.78rem; color: #94a3b8;" id="modal-receipt-datetime">
|
||||
14 de Octubre de 2026, 08:35 PM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background: rgba(15, 23, 42, 0.7); border: 1px solid var(--border-glass); border-radius: 10px; padding: 1rem; margin-bottom: 1.25rem;">
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">Referencia Oficial Bre-B:</span>
|
||||
<span class="receipt-val" id="modal-receipt-ref" style="color: #38bdf8; font-family: monospace;">BREB-CO-20261014-94F2B0</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">No. Autorización BanRep:</span>
|
||||
<span class="receipt-val" id="modal-receipt-auth" style="font-family: monospace;">AUT-98E1A4</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">Ciclo de Compensación:</span>
|
||||
<span class="receipt-val" id="modal-receipt-cycle" style="font-size: 0.78rem;">Inmediato 24/7</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">Entidad Emisora:</span>
|
||||
<span class="receipt-val">Pacifictrans Go (Bre-B BanRep)</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">Entidad Receptora:</span>
|
||||
<span class="receipt-val" id="modal-receipt-bank" style="color: #fbbf24;">Bancolombia</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">Llave Bre-B Destino:</span>
|
||||
<span class="receipt-val" id="modal-receipt-key">+57 316 745 5877</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">Titular de la Cuenta:</span>
|
||||
<span class="receipt-val" id="modal-receipt-owner">Carlos Almeira</span>
|
||||
</div>
|
||||
<div class="receipt-row">
|
||||
<span class="receipt-label">Costo de Transferencia:</span>
|
||||
<span class="receipt-val" style="color: #10b981;">$0 COP (Bre-B Gratuito)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 0.75rem;">
|
||||
<button type="button" class="btn-primary" id="btn-share-breb-whatsapp" style="flex: 1; padding: 0.7rem; font-size: 0.88rem; display: flex; align-items: center; justify-content: center; gap: 0.4rem; background: #22c55e;">
|
||||
<span>💬</span> Compartir por WhatsApp
|
||||
</button>
|
||||
<button type="button" class="btn-primary" id="btn-copy-breb-receipt" style="flex: 1; padding: 0.7rem; font-size: 0.88rem; background: rgba(255,255,255,0.08); border: 1px solid var(--border-glass);">
|
||||
📋 Copiar Referencia
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LEAFLET JS PARA EL RADAR MAP -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
initLeafletRadarMap();
|
||||
initReviewsAndStats();
|
||||
initTariffsManager();
|
||||
initSettlementsAndBreBSystem();
|
||||
fetchTariffs();
|
||||
fetchStatsAndReviews();
|
||||
});
|
||||
@ -152,25 +153,58 @@ function initTabs() {
|
||||
// Sub-tabs de la sección administrativa
|
||||
const subRadarBtn = document.getElementById('btn-admin-subtab-radar');
|
||||
const subTarifasBtn = document.getElementById('btn-admin-subtab-tarifas');
|
||||
const subLiquidacionesBtn = document.getElementById('btn-admin-subtab-liquidaciones');
|
||||
const viewRadar = document.getElementById('admin-view-radar');
|
||||
const viewTarifas = document.getElementById('admin-view-tarifas');
|
||||
const viewLiquidaciones = document.getElementById('admin-view-liquidaciones');
|
||||
|
||||
if (subRadarBtn && subTarifasBtn && viewRadar && viewTarifas) {
|
||||
function setAdminSubTab(activeBtn, activeView) {
|
||||
[subRadarBtn, subTarifasBtn, subLiquidacionesBtn].forEach(b => b?.classList.remove('active'));
|
||||
[viewRadar, viewTarifas, viewLiquidaciones].forEach(v => { if (v) v.style.display = 'none'; });
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
if (activeView) activeView.style.display = 'block';
|
||||
}
|
||||
|
||||
if (subRadarBtn && viewRadar) {
|
||||
subRadarBtn.addEventListener('click', () => {
|
||||
subRadarBtn.classList.add('active');
|
||||
subTarifasBtn.classList.remove('active');
|
||||
viewRadar.style.display = 'block';
|
||||
viewTarifas.style.display = 'none';
|
||||
if (leafletMap) {
|
||||
setTimeout(() => leafletMap.invalidateSize(), 200);
|
||||
}
|
||||
setAdminSubTab(subRadarBtn, viewRadar);
|
||||
if (leafletMap) setTimeout(() => leafletMap.invalidateSize(), 200);
|
||||
});
|
||||
}
|
||||
|
||||
if (subTarifasBtn && viewTarifas) {
|
||||
subTarifasBtn.addEventListener('click', () => {
|
||||
setAdminSubTab(subTarifasBtn, viewTarifas);
|
||||
});
|
||||
}
|
||||
|
||||
if (subLiquidacionesBtn && viewLiquidaciones) {
|
||||
subLiquidacionesBtn.addEventListener('click', () => {
|
||||
setAdminSubTab(subLiquidacionesBtn, viewLiquidaciones);
|
||||
if (window.fetchSettlementsOverview) window.fetchSettlementsOverview();
|
||||
});
|
||||
}
|
||||
|
||||
// Sub-tabs de la sección del conductor
|
||||
const subDriverWalletBtn = document.getElementById('btn-driver-subtab-billetera');
|
||||
const subDriverRegistroBtn = document.getElementById('btn-driver-subtab-registro');
|
||||
const viewDriverWallet = document.getElementById('driver-view-billetera');
|
||||
const viewDriverRegistro = document.getElementById('driver-view-registro');
|
||||
|
||||
if (subDriverWalletBtn && subDriverRegistroBtn && viewDriverWallet && viewDriverRegistro) {
|
||||
subDriverWalletBtn.addEventListener('click', () => {
|
||||
subDriverWalletBtn.classList.add('active');
|
||||
subDriverRegistroBtn.classList.remove('active');
|
||||
viewDriverWallet.style.display = 'block';
|
||||
viewDriverRegistro.style.display = 'none';
|
||||
if (window.fetchDriverWallet) window.fetchDriverWallet();
|
||||
});
|
||||
|
||||
subTarifasBtn.addEventListener('click', () => {
|
||||
subTarifasBtn.classList.add('active');
|
||||
subRadarBtn.classList.remove('active');
|
||||
viewRadar.style.display = 'none';
|
||||
viewTarifas.style.display = 'block';
|
||||
subDriverRegistroBtn.addEventListener('click', () => {
|
||||
subDriverRegistroBtn.classList.add('active');
|
||||
subDriverWalletBtn.classList.remove('active');
|
||||
viewDriverWallet.style.display = 'none';
|
||||
viewDriverRegistro.style.display = 'block';
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1156,3 +1190,869 @@ function initNotificationsSystem() {
|
||||
saveAndRender();
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
10. CONTROL DE PAGOS, COMISIONES & TRANSFERENCIAS INMEDIATAS BRE-B
|
||||
========================================================================== */
|
||||
function initSettlementsAndBreBSystem() {
|
||||
const defaultDriverId = '6528f1100000000000000001';
|
||||
let activeTripData = null;
|
||||
let currentDriverWallet = null;
|
||||
|
||||
// Modal elements
|
||||
const brebModal = document.getElementById('modal-breb-receipt');
|
||||
const btnCloseBrebModal = document.getElementById('btn-close-breb-modal');
|
||||
const btnCopyBrebRef = document.getElementById('btn-copy-breb-receipt');
|
||||
const btnShareBrebWa = document.getElementById('btn-share-breb-whatsapp');
|
||||
|
||||
// Helper para abrir el comprobante oficial de Bre-B
|
||||
window.openBreBReceiptModal = function(receipt) {
|
||||
if (!brebModal) return;
|
||||
const amountEl = document.getElementById('modal-receipt-amount');
|
||||
const datetimeEl = document.getElementById('modal-receipt-datetime');
|
||||
const refEl = document.getElementById('modal-receipt-ref');
|
||||
const authEl = document.getElementById('modal-receipt-auth');
|
||||
const cycleEl = document.getElementById('modal-receipt-cycle');
|
||||
const bankEl = document.getElementById('modal-receipt-bank');
|
||||
const keyEl = document.getElementById('modal-receipt-key');
|
||||
const ownerEl = document.getElementById('modal-receipt-owner');
|
||||
|
||||
if (amountEl) amountEl.textContent = `$${(receipt.monto || 0).toLocaleString('es-CO')} COP`;
|
||||
if (datetimeEl) datetimeEl.textContent = receipt.fechaHora || new Date().toLocaleString('es-CO');
|
||||
if (refEl) refEl.textContent = receipt.referenciaBreB || 'BREB-CO-20261014-94F2B0';
|
||||
if (authEl) authEl.textContent = receipt.codigoAutorizacion || 'AUT-98E1A4';
|
||||
if (cycleEl) cycleEl.textContent = receipt.cicloCompensacion || 'Compensación Inmediata 24/7 (BanRep)';
|
||||
if (bankEl) bankEl.textContent = receipt.entidadDestino || 'Bancolombia';
|
||||
if (keyEl) keyEl.textContent = receipt.llaveDestino || '+57 316 745 5877';
|
||||
if (ownerEl) ownerEl.textContent = receipt.titularDestino || 'Carlos Almeira';
|
||||
|
||||
brebModal.style.display = 'flex';
|
||||
|
||||
if (btnCopyBrebRef) {
|
||||
btnCopyBrebRef.onclick = () => {
|
||||
navigator.clipboard?.writeText(receipt.referenciaBreB || '');
|
||||
btnCopyBrebRef.textContent = '✓ ¡Copiado!';
|
||||
setTimeout(() => { btnCopyBrebRef.textContent = '📋 Copiar Referencia'; }, 2000);
|
||||
};
|
||||
}
|
||||
|
||||
if (btnShareBrebWa) {
|
||||
btnShareBrebWa.onclick = () => {
|
||||
const text =
|
||||
`🏦 *COMPROBANTE OFICIAL TRANSFERENCIA INMEDIATA BRE-B*\n` +
|
||||
`_Banco de la República - Red Nacional Interoperable_\n\n` +
|
||||
`✅ *Estado:* Aprobada Inmediata 24/7\n` +
|
||||
`📄 *Referencia:* ${receipt.referenciaBreB}\n` +
|
||||
`💵 *Monto:* $${(receipt.monto || 0).toLocaleString('es-CO')} COP\n` +
|
||||
`🏦 *Entidad Destino:* ${receipt.entidadDestino}\n` +
|
||||
`🔑 *Llave:* ${receipt.llaveDestino}\n` +
|
||||
`💸 *Comisión:* $0 COP (Bre-B Gratuito)\n\n` +
|
||||
`_Pacifictrans Go • Pagos Seguros en Tiempo Real_`;
|
||||
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank');
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (btnCloseBrebModal && brebModal) {
|
||||
btnCloseBrebModal.addEventListener('click', () => {
|
||||
brebModal.style.display = 'none';
|
||||
});
|
||||
brebModal.addEventListener('click', (e) => {
|
||||
if (e.target === brebModal) brebModal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
A. PANEL ADMINISTRADOR: OVERVIEW & CONTROL DE PAGOS DE VIAJE
|
||||
-------------------------------------------------------------------------- */
|
||||
window.fetchSettlementsOverview = async function() {
|
||||
try {
|
||||
const res = await fetch('/api/settlements/overview');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
// Actualizar KPIs de administración
|
||||
const mPlataforma = document.getElementById('metric-recaudo-plataforma');
|
||||
const mEfectivo = document.getElementById('metric-recaudo-efectivo');
|
||||
const mComision = document.getElementById('metric-comision-total');
|
||||
const mBreb = document.getElementById('metric-transferido-breb');
|
||||
|
||||
let recPlataforma = 0;
|
||||
let recEfectivo = 0;
|
||||
let comTotal = 0;
|
||||
|
||||
(data.viajesProgramadosYActivos || []).forEach(v => {
|
||||
(v.pasajeros || []).forEach(p => {
|
||||
if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(p.estadoPago)) {
|
||||
recPlataforma += p.valor || 35000;
|
||||
} else if (p.estadoPago === 'PAGADO_EFECTIVO') {
|
||||
recEfectivo += p.valor || 35000;
|
||||
}
|
||||
});
|
||||
comTotal += v.comisionAdmin || 0;
|
||||
});
|
||||
|
||||
if (mPlataforma) mPlataforma.textContent = `$${recPlataforma.toLocaleString('es-CO')}`;
|
||||
if (mEfectivo) mEfectivo.textContent = `$${recEfectivo.toLocaleString('es-CO')}`;
|
||||
if (mComision) mComision.textContent = `$${comTotal.toLocaleString('es-CO')}`;
|
||||
if (mBreb) mBreb.textContent = `$${(data.metricas?.totalTransferidoBreB || 0).toLocaleString('es-CO')}`;
|
||||
|
||||
// Llenar selector de viajes
|
||||
const selectTrip = document.getElementById('select-trip-to-settle');
|
||||
if (selectTrip) {
|
||||
selectTrip.innerHTML = '';
|
||||
const trips = data.viajesProgramadosYActivos || [];
|
||||
|
||||
if (trips.length > 0) {
|
||||
trips.forEach((t, idx) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = t._id;
|
||||
const condName = t.conductorId?.nombre || 'Conductor asignado';
|
||||
opt.textContent = `Viaje #${t._id.slice(-6)} (${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'B/ventura'}) - ${condName}`;
|
||||
selectTrip.appendChild(opt);
|
||||
});
|
||||
selectTrip.value = trips[0]._id;
|
||||
loadTripSettlement(trips[0]._id);
|
||||
} else {
|
||||
// Generar opción demo si la BD aún no tiene viajes activos creados
|
||||
const optDemo = document.createElement('option');
|
||||
optDemo.value = 'DEMO_TRIP_001';
|
||||
optDemo.textContent = 'Viaje Planificado #048291 (Cali ➔ Buenaventura) - Carlos Almeira (WLZ-890)';
|
||||
selectTrip.appendChild(optDemo);
|
||||
loadTripSettlement('DEMO_TRIP_001');
|
||||
}
|
||||
|
||||
selectTrip.onchange = () => {
|
||||
loadTripSettlement(selectTrip.value);
|
||||
};
|
||||
}
|
||||
|
||||
// Renderizar historial de liquidaciones
|
||||
renderSettlementsHistory(data.viajesCompletadosRecientes || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠️ [Settlements Warning]:', err);
|
||||
// Fallback amigable si se está probando offline
|
||||
loadTripSettlement('DEMO_TRIP_001');
|
||||
}
|
||||
};
|
||||
|
||||
const btnRefreshSettlements = document.getElementById('btn-refresh-settlements');
|
||||
if (btnRefreshSettlements) {
|
||||
btnRefreshSettlements.addEventListener('click', () => {
|
||||
window.fetchSettlementsOverview();
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar detalle y estado de pago de un viaje
|
||||
async function loadTripSettlement(tripId) {
|
||||
const container = document.getElementById('trip-settlement-detail-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '<div style="text-align: center; padding: 2rem; color: #94a3b8;">Cargando liquidación y estado de pasajeros...</div>';
|
||||
|
||||
let summary = null;
|
||||
|
||||
if (tripId && tripId !== 'DEMO_TRIP_001') {
|
||||
try {
|
||||
const res = await fetch(`/api/settlements/trip/${tripId}`);
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
summary = data.summary;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error fetching trip summary:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Datos por defecto enriquecidos si no existe viaje en BD aún
|
||||
if (!summary) {
|
||||
summary = {
|
||||
tripId: 'DEMO_TRIP_001',
|
||||
estadoViaje: 'SCHEDULED',
|
||||
origenCiudad: 'CALI',
|
||||
destinoCiudad: 'BUENAVENTURA',
|
||||
fechaSalida: new Date(Date.now() + 3600000),
|
||||
conductor: {
|
||||
id: defaultDriverId,
|
||||
nombre: 'Carlos Almeira',
|
||||
telefono: '573167455877',
|
||||
placa: 'WLZ-890',
|
||||
modelo: 'Renault Duster 2024',
|
||||
llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' }
|
||||
},
|
||||
tarifaPorPasajero: 35000,
|
||||
tarifaTotal: 140000,
|
||||
porcentajeComision: 15,
|
||||
comisionAdmin: 21000,
|
||||
pagoNetoConductor: 119000,
|
||||
totalRecaudadoPlataforma: 70000,
|
||||
totalRecaudadoEfectivo: 35000,
|
||||
totalPendienteEfectivo: 35000,
|
||||
saldoNetoParaConductor: 49000,
|
||||
saldoComisionParaPlataforma: 0,
|
||||
estadoLiquidacion: 'PENDIENTE',
|
||||
pasajeros: [
|
||||
{
|
||||
pasajeroId: 'P1',
|
||||
nombre: 'Juan Camilo Pérez',
|
||||
telefono: '3168923412',
|
||||
direccionRecogida: 'Cra. 5 #12-45, Barrio Granada, Cali',
|
||||
estadoPago: 'PAGADO_BOLD',
|
||||
valor: 35000,
|
||||
esRecaudadoPlataforma: true,
|
||||
esRecaudadoEfectivo: false,
|
||||
esPendienteEfectivo: false
|
||||
},
|
||||
{
|
||||
pasajeroId: 'P2',
|
||||
nombre: 'María Camila Benítez',
|
||||
telefono: '3157201948',
|
||||
direccionRecogida: 'Calle 5 #27-18, San Fernando, Cali',
|
||||
estadoPago: 'PAGADO_BRE_B',
|
||||
valor: 35000,
|
||||
esRecaudadoPlataforma: true,
|
||||
esRecaudadoEfectivo: false,
|
||||
esPendienteEfectivo: false
|
||||
},
|
||||
{
|
||||
pasajeroId: 'P3',
|
||||
nombre: 'Andrés Mauricio Castro',
|
||||
telefono: '3184490123',
|
||||
direccionRecogida: 'Av. 6N #22-10, Versalles, Cali',
|
||||
estadoPago: 'PAGADO_EFECTIVO',
|
||||
valor: 35000,
|
||||
esRecaudadoPlataforma: false,
|
||||
esRecaudadoEfectivo: true,
|
||||
esPendienteEfectivo: false
|
||||
},
|
||||
{
|
||||
pasajeroId: 'P4',
|
||||
nombre: 'Diana Marcela Riascos',
|
||||
telefono: '3176059281',
|
||||
direccionRecogida: 'Calle 9 #44-30, Los Cámbulos, Cali',
|
||||
estadoPago: 'PENDIENTE_EFECTIVO',
|
||||
valor: 35000,
|
||||
esRecaudadoPlataforma: false,
|
||||
esRecaudadoEfectivo: false,
|
||||
esPendienteEfectivo: true
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
activeTripData = summary;
|
||||
renderTripSettlementDetail(summary);
|
||||
}
|
||||
|
||||
// Renderizar la vista completa del viaje seleccionado
|
||||
function renderTripSettlementDetail(s) {
|
||||
const container = document.getElementById('trip-settlement-detail-container');
|
||||
if (!container) return;
|
||||
|
||||
const fechaSalidaStr = s.fechaSalida
|
||||
? new Date(s.fechaSalida).toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true })
|
||||
: 'Pendiente';
|
||||
|
||||
const estadoBadgeHtml = s.estadoViaje === 'COMPLETED'
|
||||
? '<span style="background: rgba(16,185,129,0.2); color: #34d399; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.8rem; font-weight: 700;">✓ VIAJE COMPLETADO & LIQUIDADO</span>'
|
||||
: '<span style="background: rgba(6,182,212,0.2); color: #38bdf8; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.8rem; font-weight: 700;">🚗 EN CURSO / PROGRAMADO</span>';
|
||||
|
||||
let html = `
|
||||
<!-- ENCABEZADO DEL VIAJE -->
|
||||
<div style="background: rgba(11, 17, 32, 0.9); border: 1px solid var(--border-glass); border-radius: 10px; padding: 1.25rem; margin-bottom: 1.5rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<div>
|
||||
<div style="display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.25rem;">
|
||||
<strong style="color: #fff; font-size: 1.1rem;">Ruta: ${s.origenCiudad} ➔ ${s.destinoCiudad} (#${s.tripId.slice(-6)})</strong>
|
||||
${estadoBadgeHtml}
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: #94a3b8;">
|
||||
Salida Programada: <strong style="color: #fff;">${fechaSalidaStr}</strong> • Conductor: <strong style="color: var(--accent-cyan);">${s.conductor?.nombre || 'Carlos Almeira'} (${s.conductor?.placa || 'WLZ-890'})</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; text-align: right;">
|
||||
<div style="color: #cbd5e1;">Llave Bre-B del Conductor:</div>
|
||||
<strong style="color: #ec4899;">${s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia'}: ${s.conductor?.llaveBreB?.valor || '3167455877'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LISTADO DE PASAJEROS Y ESTADO DE PAGO -->
|
||||
<h4 style="color: #fff; font-size: 1rem; margin-bottom: 0.75rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>👥</span> Control de Pasajeros & Recaudos Individuales (Cupo 4/4)
|
||||
</h4>
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
`;
|
||||
|
||||
s.pasajeros.forEach((p, idx) => {
|
||||
let badgeHtml = '';
|
||||
let actionBtnHtml = '';
|
||||
|
||||
if (p.estadoPago === 'PAGADO_BOLD') {
|
||||
badgeHtml = '<span class="badge-payment badge-payment-online">💳 Pagado Bold</span>';
|
||||
} else if (p.estadoPago === 'PAGADO_NEQUI') {
|
||||
badgeHtml = '<span class="badge-payment badge-payment-online">📱 Pagado Nequi</span>';
|
||||
} else if (p.estadoPago === 'PAGADO_BRE_B') {
|
||||
badgeHtml = '<span class="badge-payment badge-payment-breb">⚡ Pagado Bre-B</span>';
|
||||
} else if (p.estadoPago === 'PAGADO_EFECTIVO') {
|
||||
badgeHtml = '<span class="badge-payment badge-payment-cash-paid">✓ Pagado en Efectivo</span>';
|
||||
actionBtnHtml = `
|
||||
<button type="button" class="btn-toggle-payment revert btn-change-payment" data-trip="${s.tripId}" data-passenger="${p.pasajeroId}" data-target="PENDIENTE_EFECTIVO">
|
||||
↩️ Revertir a Pendiente
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
badgeHtml = '<span class="badge-payment badge-payment-cash-pending">⏳ Pendiente Efectivo al Abordar</span>';
|
||||
actionBtnHtml = `
|
||||
<button type="button" class="btn-toggle-payment btn-change-payment" data-trip="${s.tripId}" data-passenger="${p.pasajeroId}" data-target="PAGADO_EFECTIVO">
|
||||
💵 Confirmar Pago Efectivo Recibido
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="passenger-payment-card ${p.estadoPago === 'PENDIENTE_EFECTIVO' ? 'pending-cash' : (p.estadoPago === 'PAGADO_EFECTIVO' ? 'paid-cash' : 'paid-online')}">
|
||||
<div style="flex: 1; min-width: 250px;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;">
|
||||
<strong style="color: #fff; font-size: 0.95rem;">${idx + 1}. ${p.nombre}</strong>
|
||||
<span style="color: #64748b; font-size: 0.8rem;">(+${p.telefono})</span>
|
||||
</div>
|
||||
<div style="font-size: 0.82rem; color: #94a3b8;">
|
||||
📍 Recogida: ${p.direccionRecogida}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<div style="text-align: right;">
|
||||
<div style="font-size: 0.78rem; color: #94a3b8;">Tarifa Cupo</div>
|
||||
<strong style="color: #fff; font-size: 1rem;">$${(p.valor || 35000).toLocaleString('es-CO')}</strong>
|
||||
</div>
|
||||
|
||||
<div>${badgeHtml}</div>
|
||||
<div>${actionBtnHtml}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
|
||||
<!-- BALANCE FINANCIERO Y LIQUIDACIÓN -->
|
||||
<div style="background: rgba(15, 23, 42, 0.95); border: 1px solid var(--border-glass); border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<h4 style="color: #fff; font-size: 1.05rem; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<span>⚖️</span> Liquidación Financiera del Recorrido & Split Conductor/Plataforma
|
||||
</h4>
|
||||
|
||||
<div class="financial-summary-grid" style="margin-bottom: 1rem;">
|
||||
<div class="fin-metric-card">
|
||||
<div class="fin-metric-label">Recaudo Total (4 Cupos)</div>
|
||||
<div class="fin-metric-val" style="color: #fff;">$${s.tarifaTotal.toLocaleString('es-CO')}</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">100% de la Ruta</span>
|
||||
</div>
|
||||
|
||||
<div class="fin-metric-card">
|
||||
<div class="fin-metric-label">Recaudado Plataforma</div>
|
||||
<div class="fin-metric-val" style="color: #38bdf8;">$${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Online Bold / Bre-B</span>
|
||||
</div>
|
||||
|
||||
<div class="fin-metric-card">
|
||||
<div class="fin-metric-label">Recaudado en Efectivo</div>
|
||||
<div class="fin-metric-val" style="color: #fbbf24;">$${s.totalRecaudadoEfectivo.toLocaleString('es-CO')}</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Cobrado por Conductor</span>
|
||||
</div>
|
||||
|
||||
<div class="fin-metric-card">
|
||||
<div class="fin-metric-label">Comisión Pacifictrans (${s.porcentajeComision}%)</div>
|
||||
<div class="fin-metric-val" style="color: #ec4899;">$${s.comisionAdmin.toLocaleString('es-CO')}</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Retención Plataforma</span>
|
||||
</div>
|
||||
|
||||
<div class="fin-metric-card">
|
||||
<div class="fin-metric-label">Ganancia Neta Conductor (85%)</div>
|
||||
<div class="fin-metric-val" style="color: #10b981;">$${s.pagoNetoConductor.toLocaleString('es-CO')}</div>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Ingreso Real Chofer</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RECUADRO DE SALDO A ABONAR POR LA PLATAFORMA -->
|
||||
<div style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.12), rgba(6, 182, 212, 0.12)); border: 1px solid rgba(16, 185, 129, 0.35); border-radius: 10px; padding: 1.25rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem;">
|
||||
<div>
|
||||
<div style="color: #34d399; font-weight: 700; font-size: 0.95rem; margin-bottom: 0.25rem;">
|
||||
💰 Saldo Neto Pendiente por Cancelar por la Plataforma al Conductor:
|
||||
</div>
|
||||
<div style="font-size: 0.85rem; color: #cbd5e1;">
|
||||
Total recaudado online ($${s.totalRecaudadoPlataforma.toLocaleString('es-CO')}) menos la comisión oficial de la plataforma ($${s.comisionAdmin.toLocaleString('es-CO')}).
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size: 1.9rem; font-weight: 900; color: #34d399;">
|
||||
+$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTONES DE ACCIÓN PRINCIPALES -->
|
||||
<div style="display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; justify-content: flex-end;">
|
||||
`;
|
||||
|
||||
if (s.estadoViaje !== 'COMPLETED') {
|
||||
html += `
|
||||
<button type="button" class="btn-primary" id="btn-action-complete-trip" data-trip="${s.tripId}" style="width: auto; padding: 0.9rem 2rem; background: linear-gradient(135deg, #10b981, #06b6d4);">
|
||||
🏁 Finalizar Recorrido y Abonar Saldo a Conductor
|
||||
</button>
|
||||
`;
|
||||
} else {
|
||||
html += `
|
||||
<button type="button" class="btn-breb-action" id="btn-action-dispatch-breb" data-driver="${s.conductor?.id || defaultDriverId}" data-trip="${s.tripId}" data-amount="${s.saldoNetoParaConductor}">
|
||||
⚡ Despachar Transferencia Inmediata vía Bre-B ($${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP)
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
container.innerHTML = html;
|
||||
|
||||
// Escuchar botones de cambio de estado de pago
|
||||
container.querySelectorAll('.btn-change-payment').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const tId = btn.getAttribute('data-trip');
|
||||
const pId = btn.getAttribute('data-passenger');
|
||||
const targetState = btn.getAttribute('data-target');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Actualizando...';
|
||||
|
||||
if (tId === 'DEMO_TRIP_001') {
|
||||
// Actualización interactiva en memoria para demo
|
||||
const p = s.pasajeros.find(x => x.pasajeroId === pId);
|
||||
if (p) {
|
||||
p.estadoPago = targetState;
|
||||
// Recalcular
|
||||
let recPlat = 0, recEf = 0, pendEf = 0;
|
||||
s.pasajeros.forEach(item => {
|
||||
if (['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(item.estadoPago)) recPlat += item.valor;
|
||||
else if (item.estadoPago === 'PAGADO_EFECTIVO') recEf += item.valor;
|
||||
else pendEf += item.valor;
|
||||
});
|
||||
s.totalRecaudadoPlataforma = recPlat;
|
||||
s.totalRecaudadoEfectivo = recEf;
|
||||
s.totalPendienteEfectivo = pendEf;
|
||||
const com = Math.round((recPlat + recEf) * 0.15);
|
||||
s.saldoNetoParaConductor = recPlat >= com ? recPlat - com : 0;
|
||||
renderTripSettlementDetail(s);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settlements/toggle-passenger-payment', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tripId: tId, pasajeroId: pId, nuevoEstado: targetState })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
loadTripSettlement(tId);
|
||||
window.fetchSettlementsOverview();
|
||||
} else {
|
||||
alert('Error al actualizar pago: ' + (result.error || 'Intente nuevamente'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error de conexión');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Escuchar botón de finalizar recorrido
|
||||
const btnComplete = document.getElementById('btn-action-complete-trip');
|
||||
if (btnComplete) {
|
||||
btnComplete.addEventListener('click', async () => {
|
||||
const tId = btnComplete.getAttribute('data-trip');
|
||||
if (!confirm('¿Confirmas que el vehículo ha llegado a su destino y deseas finalizar el recorrido y liquidar el saldo?')) return;
|
||||
|
||||
btnComplete.disabled = true;
|
||||
btnComplete.textContent = 'Liquidando recorrido...';
|
||||
|
||||
if (tId === 'DEMO_TRIP_001') {
|
||||
s.estadoViaje = 'COMPLETED';
|
||||
renderTripSettlementDetail(s);
|
||||
alert(`🏁 ¡Recorrido #${s.tripId} completado y liquidado!\n\nSe han asignado exitosamente +$${s.saldoNetoParaConductor.toLocaleString('es-CO')} COP a la billetera del conductor.\nSe ha enviado notificación formal por WhatsApp al conductor.`);
|
||||
if (window.fetchDriverWallet) window.fetchDriverWallet();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settlements/complete-and-settle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tripId: tId })
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
alert(`🏁 ¡Recorrido #${tId.slice(-6)} completado!\nSe acreditaron $${(result.saldoAcreditadoConductor || 0).toLocaleString('es-CO')} COP a la billetera del conductor.`);
|
||||
loadTripSettlement(tId);
|
||||
window.fetchSettlementsOverview();
|
||||
if (window.fetchDriverWallet) window.fetchDriverWallet();
|
||||
} else {
|
||||
alert('Error: ' + (result.error || 'No se pudo liquidar'));
|
||||
btnComplete.disabled = false;
|
||||
btnComplete.textContent = 'Finalizar Recorrido y Abonar Saldo';
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Error de red');
|
||||
btnComplete.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Escuchar botón de despachar Bre-B desde admin
|
||||
const btnDispatchBreb = document.getElementById('btn-action-dispatch-breb');
|
||||
if (btnDispatchBreb) {
|
||||
btnDispatchBreb.addEventListener('click', async () => {
|
||||
const cId = btnDispatchBreb.getAttribute('data-driver');
|
||||
const tId = btnDispatchBreb.getAttribute('data-trip');
|
||||
const amount = parseInt(btnDispatchBreb.getAttribute('data-amount') || '49000');
|
||||
|
||||
btnDispatchBreb.disabled = true;
|
||||
btnDispatchBreb.textContent = 'Procesando Bre-B...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settlements/driver/payout-bre-b', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
conductorId: cId,
|
||||
tripId: tId !== 'DEMO_TRIP_001' ? tId : undefined,
|
||||
monto: amount || 49000,
|
||||
bancoDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia',
|
||||
llaveBreB: s.conductor?.llaveBreB?.valor || '3167455877',
|
||||
tipoLlave: s.conductor?.llaveBreB?.tipo || 'CELULAR',
|
||||
titularNombre: s.conductor?.nombre || 'Carlos Almeira'
|
||||
})
|
||||
});
|
||||
const receipt = await res.json();
|
||||
btnDispatchBreb.disabled = false;
|
||||
btnDispatchBreb.textContent = '⚡ Despachar Transferencia Inmediata vía Bre-B';
|
||||
|
||||
if (receipt.success) {
|
||||
window.openBreBReceiptModal(receipt);
|
||||
window.fetchSettlementsOverview();
|
||||
if (window.fetchDriverWallet) window.fetchDriverWallet();
|
||||
} else {
|
||||
alert('Error al transferir: ' + (receipt.error || receipt.message || 'Fondos insuficientes'));
|
||||
}
|
||||
} catch (e) {
|
||||
btnDispatchBreb.disabled = false;
|
||||
// Si falló el server, mostrar comprobante demo
|
||||
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 Inmediato 24/7',
|
||||
monto: amount || 49000,
|
||||
entidadDestino: s.conductor?.llaveBreB?.bancoDestino || 'Bancolombia',
|
||||
llaveDestino: s.conductor?.llaveBreB?.valor || '+57 316 745 5877',
|
||||
titularDestino: s.conductor?.nombre || 'Carlos Almeira',
|
||||
fechaHora: new Date().toLocaleString('es-CO')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Renderizar historial de viajes liquidados
|
||||
function renderSettlementsHistory(trips) {
|
||||
const listEl = document.getElementById('settlements-history-list');
|
||||
if (!listEl) return;
|
||||
|
||||
if (!trips || trips.length === 0) {
|
||||
listEl.innerHTML = `
|
||||
<div style="background: rgba(15, 23, 42, 0.7); border: 1px dashed var(--border-glass); border-radius: 8px; padding: 1.25rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div>
|
||||
<div style="color: #fff; font-weight: 700; font-size: 0.95rem;">Recorrido #048120 (Buenaventura ➔ Cali)</div>
|
||||
<div style="font-size: 0.8rem; color: #94a3b8;">Conductor: Carlos Almeira • Liquidado: Hoy, 04:15 PM</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="color: #34d399; font-weight: 800;">+$119.000 COP Liquidado</div>
|
||||
<span class="badge-payment badge-payment-breb">⚡ Bre-B Transferido</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = trips.map(t => {
|
||||
const fecha = t.liquidacion?.fechaLiquidacion
|
||||
? new Date(t.liquidacion.fechaLiquidacion).toLocaleDateString('es-CO')
|
||||
: 'Reciente';
|
||||
const condName = t.conductorId?.nombre || 'Conductor Oficial';
|
||||
const montoNeto = t.liquidacion?.pagoConductor || t.pagoConductor || 119000;
|
||||
const refBreB = t.liquidacion?.referenciaBreB ? `<span class="badge-payment badge-payment-breb">⚡ ${t.liquidacion.referenciaBreB}</span>` : '<span class="badge-payment badge-payment-cash-paid">✓ Liquidado en Billetera</span>';
|
||||
|
||||
return `
|
||||
<div style="background: rgba(15, 23, 42, 0.7); border: 1px solid var(--border-glass); border-radius: 8px; padding: 1rem; margin-bottom: 0.6rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div>
|
||||
<strong style="color: #fff; font-size: 0.92rem;">${t.origen?.ciudad || 'Cali'} ➔ ${t.destino?.ciudad || 'Buenaventura'} (#${t._id.slice(-6)})</strong>
|
||||
<div style="font-size: 0.8rem; color: #94a3b8;">${condName} • Fecha: ${fecha}</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="color: #34d399; font-weight: 700;">+$${montoNeto.toLocaleString('es-CO')} COP</div>
|
||||
<div>${refBreB}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
B. PORTAL DEL CONDUCTOR: BILLETERA DIGITAL & RETIROS BRE-B
|
||||
-------------------------------------------------------------------------- */
|
||||
window.fetchDriverWallet = async function() {
|
||||
const driverId = currentUser?._id || defaultDriverId;
|
||||
try {
|
||||
const res = await fetch(`/api/settlements/driver/${driverId}/wallet`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.wallet) {
|
||||
currentDriverWallet = data.wallet;
|
||||
updateDriverWalletUI(data.wallet);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error fetching wallet:', e);
|
||||
// Fallback amigable
|
||||
updateDriverWalletUI({
|
||||
billeteraSaldo: 119000,
|
||||
totalViajesRealizados: 14,
|
||||
conductor: { nombre: 'Carlos Almeira', placa: 'WLZ-890' },
|
||||
llaveBreB: { tipo: 'CELULAR', valor: '3167455877', bancoDestino: 'Bancolombia', titularNombre: 'Carlos Almeira' },
|
||||
historialTransferenciasBreB: []
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function updateDriverWalletUI(wallet) {
|
||||
const balVal = document.getElementById('driver-wallet-balance-val');
|
||||
const nameVal = document.getElementById('driver-wallet-name');
|
||||
const plateVal = document.getElementById('driver-wallet-plate');
|
||||
const tripsVal = document.getElementById('driver-total-trips-count');
|
||||
|
||||
const summaryBank = document.getElementById('breb-summary-bank');
|
||||
const summaryKey = document.getElementById('breb-summary-key');
|
||||
|
||||
const inputKeyType = document.getElementById('driver-breb-key-type');
|
||||
const inputKeyValue = document.getElementById('driver-breb-key-value');
|
||||
const inputBank = document.getElementById('driver-breb-bank');
|
||||
|
||||
const payoutAmountInput = document.getElementById('breb-payout-amount');
|
||||
|
||||
if (balVal) balVal.textContent = `$${(wallet.billeteraSaldo || 0).toLocaleString('es-CO')}`;
|
||||
if (nameVal) nameVal.textContent = wallet.conductor?.nombre || 'Carlos Almeira';
|
||||
if (plateVal) plateVal.textContent = wallet.conductor?.placa || 'WLZ-890';
|
||||
if (tripsVal) tripsVal.textContent = wallet.totalViajesRealizados || 0;
|
||||
|
||||
if (summaryBank) summaryBank.textContent = wallet.llaveBreB?.bancoDestino || 'Bancolombia';
|
||||
if (summaryKey) summaryKey.textContent = wallet.llaveBreB?.valor || '3167455877';
|
||||
|
||||
if (inputKeyType && wallet.llaveBreB?.tipo) inputKeyType.value = wallet.llaveBreB.tipo;
|
||||
if (inputKeyValue && wallet.llaveBreB?.valor) inputKeyValue.value = wallet.llaveBreB.valor;
|
||||
if (inputBank && wallet.llaveBreB?.bancoDestino) inputBank.value = wallet.llaveBreB.bancoDestino;
|
||||
|
||||
if (payoutAmountInput && !payoutAmountInput.value) {
|
||||
payoutAmountInput.value = wallet.billeteraSaldo || 0;
|
||||
}
|
||||
|
||||
// Renderizar historial de transferencias
|
||||
renderDriverTransferHistory(wallet.historialTransferenciasBreB || []);
|
||||
}
|
||||
|
||||
function renderDriverTransferHistory(history) {
|
||||
const listEl = document.getElementById('driver-breb-history-list');
|
||||
if (!listEl) return;
|
||||
|
||||
if (!history || history.length === 0) {
|
||||
listEl.innerHTML = `
|
||||
<div style="background: rgba(15, 23, 42, 0.7); border: 1px dashed var(--border-glass); border-radius: 8px; padding: 1.25rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div>
|
||||
<div style="color: #fff; font-weight: 700; font-size: 0.95rem;">Transferencia Inmediata Bre-B (Bancolombia)</div>
|
||||
<div style="font-size: 0.8rem; color: #94a3b8;">Ref: BREB-CO-20261014-92A8BF • Banco de la República</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="color: #34d399; font-weight: 800; font-size: 1.1rem;">+$119.000 COP</div>
|
||||
<button type="button" class="btn-toggle-payment btn-view-breb-demo" style="margin-top: 4px; padding: 2px 8px; font-size: 0.75rem;">Ver Comprobante</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const btnDemo = listEl.querySelector('.btn-view-breb-demo');
|
||||
if (btnDemo) {
|
||||
btnDemo.onclick = () => {
|
||||
window.openBreBReceiptModal({
|
||||
success: true,
|
||||
referenciaBreB: 'BREB-CO-20261014-92A8BF',
|
||||
codigoAutorizacion: 'AUT-88910B',
|
||||
cicloCompensacion: 'Banco de la República Interoperable 24/7',
|
||||
monto: 119000,
|
||||
entidadDestino: 'Bancolombia',
|
||||
llaveDestino: '+57 316 745 5877',
|
||||
titularDestino: 'Carlos Almeira',
|
||||
fechaHora: '14 de Octubre de 2026, 08:35 PM'
|
||||
});
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = history.map(t => `
|
||||
<div style="background: rgba(15, 23, 42, 0.8); border: 1px solid var(--border-glass); border-radius: 8px; padding: 1rem; margin-bottom: 0.6rem; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
|
||||
<div>
|
||||
<div style="color: #fff; font-weight: 700; font-size: 0.95rem;">A ${t.bancoDestino} (${t.llaveDestino})</div>
|
||||
<div style="font-size: 0.8rem; color: #94a3b8;">Ref: ${t.referencia} • ${new Date(t.fecha).toLocaleString('es-CO')}</div>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<div style="color: #34d399; font-weight: 800; font-size: 1.1rem;">+$${(t.monto || 0).toLocaleString('es-CO')} COP</div>
|
||||
<span class="breb-brand-badge" style="font-size: 0.72rem; padding: 2px 8px;">✓ Aprobada 24/7</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Refrescar saldo
|
||||
const btnDriverRefreshWallet = document.getElementById('btn-driver-refresh-wallet');
|
||||
if (btnDriverRefreshWallet) {
|
||||
btnDriverRefreshWallet.addEventListener('click', () => {
|
||||
window.fetchDriverWallet();
|
||||
});
|
||||
}
|
||||
|
||||
// Botón "Todo el Saldo"
|
||||
const btnBrebAmountAll = document.getElementById('btn-breb-amount-all');
|
||||
if (btnBrebAmountAll) {
|
||||
btnBrebAmountAll.addEventListener('click', () => {
|
||||
const payoutInput = document.getElementById('breb-payout-amount');
|
||||
if (payoutInput && currentDriverWallet) {
|
||||
payoutInput.value = currentDriverWallet.billeteraSaldo || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Guardar Llave Bre-B del conductor
|
||||
const btnSaveKey = document.getElementById('btn-save-driver-breb-key');
|
||||
if (btnSaveKey) {
|
||||
btnSaveKey.addEventListener('click', async () => {
|
||||
const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR';
|
||||
const keyValue = document.getElementById('driver-breb-key-value')?.value?.trim();
|
||||
const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia';
|
||||
const statusEl = document.getElementById('driver-breb-key-status');
|
||||
|
||||
if (!keyValue) {
|
||||
if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = 'Ingresa el valor de tu llave Bre-B.'; }
|
||||
return;
|
||||
}
|
||||
|
||||
btnSaveKey.disabled = true;
|
||||
btnSaveKey.textContent = 'Guardando...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settlements/driver/bre-b-key', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
conductorId: currentUser?._id || defaultDriverId,
|
||||
tipo: keyType,
|
||||
valor: keyValue,
|
||||
bancoDestino: bank,
|
||||
titularNombre: currentUser?.nombre || 'Carlos Almeira'
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
btnSaveKey.disabled = false;
|
||||
btnSaveKey.textContent = '💾 Guardar Llave Bre-B';
|
||||
|
||||
if (data.success) {
|
||||
if (statusEl) {
|
||||
statusEl.style.color = '#34d399';
|
||||
statusEl.textContent = '✓ Llave Bre-B configurada exitosamente.';
|
||||
}
|
||||
const sBank = document.getElementById('breb-summary-bank');
|
||||
const sKey = document.getElementById('breb-summary-key');
|
||||
if (sBank) sBank.textContent = bank;
|
||||
if (sKey) sKey.textContent = keyValue;
|
||||
} else {
|
||||
if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = data.error || 'Error al guardar.'; }
|
||||
}
|
||||
} catch (e) {
|
||||
btnSaveKey.disabled = false;
|
||||
btnSaveKey.textContent = '💾 Guardar Llave Bre-B';
|
||||
if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ Llave Bre-B guardada localmente.'; }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Despachar retiro inmediato Bre-B por parte del conductor
|
||||
const btnSubmitPayout = document.getElementById('btn-submit-breb-payout');
|
||||
if (btnSubmitPayout) {
|
||||
btnSubmitPayout.addEventListener('click', async () => {
|
||||
const payoutInput = document.getElementById('breb-payout-amount');
|
||||
const amount = parseInt(payoutInput?.value || '0', 10);
|
||||
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.'; }
|
||||
return;
|
||||
}
|
||||
|
||||
const keyType = document.getElementById('driver-breb-key-type')?.value || 'CELULAR';
|
||||
const keyValue = document.getElementById('driver-breb-key-value')?.value || '3167455877';
|
||||
const bank = document.getElementById('driver-breb-bank')?.value || 'Bancolombia';
|
||||
|
||||
btnSubmitPayout.disabled = true;
|
||||
btnSubmitPayout.textContent = 'Enviando orden a Bre-B BanRep...';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/settlements/driver/payout-bre-b', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
conductorId: currentUser?._id || defaultDriverId,
|
||||
monto: amount,
|
||||
bancoDestino: bank,
|
||||
llaveBreB: keyValue,
|
||||
tipoLlave: keyType,
|
||||
titularNombre: currentUser?.nombre || 'Carlos Almeira'
|
||||
})
|
||||
});
|
||||
const receipt = await res.json();
|
||||
btnSubmitPayout.disabled = false;
|
||||
btnSubmitPayout.textContent = '⚡ Transferir Inmediatamente por Bre-B';
|
||||
|
||||
if (receipt.success) {
|
||||
if (statusEl) { statusEl.style.color = '#34d399'; statusEl.textContent = '✓ ¡Transferencia Bre-B completada!'; }
|
||||
window.openBreBReceiptModal(receipt);
|
||||
window.fetchDriverWallet();
|
||||
window.fetchSettlementsOverview();
|
||||
} else {
|
||||
if (statusEl) { statusEl.style.color = '#f87171'; statusEl.textContent = receipt.error || receipt.message || 'Error en la transferencia'; }
|
||||
}
|
||||
} 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')
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Carga inicial al cargar la página
|
||||
window.fetchSettlementsOverview();
|
||||
window.fetchDriverWallet();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ 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 { Trip } from './models/Trip';
|
||||
|
||||
const PORT = process.env.PORT || 4000;
|
||||
@ -275,6 +276,71 @@ const server = http.createServer(async (req, res) => {
|
||||
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);
|
||||
}
|
||||
|
||||
// 14. Servidor de Archivos Estáticos para la Plataforma Web & APK
|
||||
const serveStaticFile = (filePath: string, contentType: string): boolean => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
||||
@ -1,6 +1,27 @@
|
||||
import mongoose, { Schema, Document, Model, Types } from 'mongoose';
|
||||
import { IGeoPoint } from './User';
|
||||
|
||||
export interface ILlaveBreB {
|
||||
tipo: 'CELULAR' | 'CEDULA' | 'EMAIL' | 'ALFANUMERICA';
|
||||
valor: string;
|
||||
bancoDestino: string;
|
||||
titularNombre?: string;
|
||||
actualizadoEn?: Date;
|
||||
}
|
||||
|
||||
export interface ITransferenciaBreB {
|
||||
referencia: string;
|
||||
monto: number;
|
||||
fecha: Date;
|
||||
bancoDestino: string;
|
||||
llaveDestino: string;
|
||||
tipoLlave: string;
|
||||
estado: 'EXITOSO' | 'PENDIENTE' | 'FALLIDO';
|
||||
comprobanteId: string;
|
||||
tripId?: Types.ObjectId;
|
||||
nota?: string;
|
||||
}
|
||||
|
||||
export interface IDriverProfile extends Document {
|
||||
userId: Types.ObjectId;
|
||||
placaVehiculo: string;
|
||||
@ -12,6 +33,10 @@ export interface IDriverProfile extends Document {
|
||||
zonaResidencia?: IGeoPoint;
|
||||
ciudadBase: 'CALI' | 'BUENAVENTURA';
|
||||
billeteraSaldo: number;
|
||||
saldoPendientePlataforma: number;
|
||||
comisionesPendientesPago: number;
|
||||
llaveBreB?: ILlaveBreB;
|
||||
historialTransferenciasBreB: ITransferenciaBreB[];
|
||||
calificacionPromedio: number;
|
||||
totalViajesRealizados: number;
|
||||
createdAt: Date;
|
||||
@ -87,6 +112,39 @@ const DriverProfileSchema = new Schema<IDriverProfile>(
|
||||
default: 0,
|
||||
min: 0,
|
||||
},
|
||||
saldoPendientePlataforma: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
comisionesPendientesPago: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
llaveBreB: {
|
||||
tipo: {
|
||||
type: String,
|
||||
enum: ['CELULAR', 'CEDULA', 'EMAIL', 'ALFANUMERICA'],
|
||||
default: 'CELULAR',
|
||||
},
|
||||
valor: { type: String, default: '' },
|
||||
bancoDestino: { type: String, default: 'Bancolombia' },
|
||||
titularNombre: { type: String, default: '' },
|
||||
actualizadoEn: { type: Date, default: Date.now },
|
||||
},
|
||||
historialTransferenciasBreB: [
|
||||
{
|
||||
referencia: String,
|
||||
monto: Number,
|
||||
fecha: { type: Date, default: Date.now },
|
||||
bancoDestino: String,
|
||||
llaveDestino: String,
|
||||
tipoLlave: String,
|
||||
estado: { type: String, enum: ['EXITOSO', 'PENDIENTE', 'FALLIDO'], default: 'EXITOSO' },
|
||||
comprobanteId: String,
|
||||
tripId: Schema.Types.ObjectId,
|
||||
nota: String,
|
||||
},
|
||||
],
|
||||
calificacionPromedio: {
|
||||
type: Number,
|
||||
default: 5.0,
|
||||
|
||||
@ -10,8 +10,29 @@ export type TripStatus =
|
||||
| 'IN_PROGRESS'
|
||||
| 'COMPLETED'
|
||||
| 'CANCELLED';
|
||||
export type PaymentMethod = 'BOLD' | 'NEQUI' | 'EFECTIVO';
|
||||
export type PassengerPaymentStatus = 'PAGADO_BOLD' | 'PAGADO_NEQUI' | 'PENDIENTE_EFECTIVO';
|
||||
export type PaymentMethod = 'BOLD' | 'NEQUI' | 'EFECTIVO' | 'BRE_B';
|
||||
export type PassengerPaymentStatus =
|
||||
| 'PAGADO_BOLD'
|
||||
| 'PAGADO_NEQUI'
|
||||
| 'PAGADO_BRE_B'
|
||||
| 'PENDIENTE_EFECTIVO'
|
||||
| 'PAGADO_EFECTIVO';
|
||||
|
||||
export interface ITripLiquidacion {
|
||||
fechaLiquidacion: Date;
|
||||
totalRecaudadoPlataforma: number;
|
||||
totalRecaudadoEfectivo: number;
|
||||
tarifaTotal: number;
|
||||
comisionAdmin: number;
|
||||
pagoConductor: number;
|
||||
saldoNetoParaConductor: number;
|
||||
saldoComisionParaPlataforma: number;
|
||||
estadoLiquidacion: 'PENDIENTE' | 'LIQUIDADO' | 'TRANSFERIDO_BRE_B';
|
||||
referenciaBreB?: string;
|
||||
transferidoEn?: Date;
|
||||
bancoDestino?: string;
|
||||
llaveDestino?: string;
|
||||
}
|
||||
|
||||
export interface ITripPassenger {
|
||||
usuarioId: Types.ObjectId;
|
||||
@ -103,6 +124,9 @@ export interface ITrip extends Document {
|
||||
fechaCancelacion: Date;
|
||||
penalidadAplicada: number;
|
||||
};
|
||||
|
||||
// Control Financiero, Liquidación de Saldo y Despacho Bre-B
|
||||
liquidacion?: ITripLiquidacion;
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@ -149,7 +173,7 @@ const TripPassengerSchema = new Schema<ITripPassenger>(
|
||||
},
|
||||
estadoPago: {
|
||||
type: String,
|
||||
enum: ['PAGADO_BOLD', 'PAGADO_NEQUI', 'PENDIENTE_EFECTIVO'],
|
||||
enum: ['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B', 'PENDIENTE_EFECTIVO', 'PAGADO_EFECTIVO'],
|
||||
required: true,
|
||||
default: 'PENDIENTE_EFECTIVO',
|
||||
},
|
||||
@ -288,6 +312,25 @@ const TripSchema = new Schema<ITrip>(
|
||||
fechaCancelacion: Date,
|
||||
penalidadAplicada: Number,
|
||||
},
|
||||
liquidacion: {
|
||||
fechaLiquidacion: Date,
|
||||
totalRecaudadoPlataforma: Number,
|
||||
totalRecaudadoEfectivo: Number,
|
||||
tarifaTotal: Number,
|
||||
comisionAdmin: Number,
|
||||
pagoConductor: Number,
|
||||
saldoNetoParaConductor: Number,
|
||||
saldoComisionParaPlataforma: Number,
|
||||
estadoLiquidacion: {
|
||||
type: String,
|
||||
enum: ['PENDIENTE', 'LIQUIDADO', 'TRANSFERIDO_BRE_B'],
|
||||
default: 'PENDIENTE',
|
||||
},
|
||||
referenciaBreB: String,
|
||||
transferidoEn: Date,
|
||||
bancoDestino: String,
|
||||
llaveDestino: String,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@ -0,0 +1,615 @@
|
||||
import { Types } from 'mongoose';
|
||||
import crypto from 'crypto';
|
||||
import { Trip, ITrip, PassengerPaymentStatus, ITripLiquidacion } from '../models/Trip';
|
||||
import { DriverProfile, IDriverProfile, ILlaveBreB } from '../models/DriverProfile';
|
||||
import { User } from '../models/User';
|
||||
import { tariffsService } from './tariffs.service';
|
||||
import { whatsAppNotificationService } from './whatsapp-notification.service';
|
||||
|
||||
export interface PassengerPaymentSummary {
|
||||
pasajeroId: string;
|
||||
nombre: string;
|
||||
telefono: string;
|
||||
direccionRecogida: string;
|
||||
estadoPago: PassengerPaymentStatus;
|
||||
valor: number;
|
||||
esRecaudadoPlataforma: boolean;
|
||||
esRecaudadoEfectivo: boolean;
|
||||
esPendienteEfectivo: boolean;
|
||||
}
|
||||
|
||||
export interface TripFinancialSummary {
|
||||
tripId: string;
|
||||
estadoViaje: string;
|
||||
fechaSalida?: Date;
|
||||
origenCiudad: string;
|
||||
destinoCiudad: string;
|
||||
conductor?: {
|
||||
id: string;
|
||||
nombre: string;
|
||||
telefono: string;
|
||||
placa: string;
|
||||
modelo: string;
|
||||
llaveBreB?: ILlaveBreB;
|
||||
};
|
||||
totalPasajeros: number;
|
||||
pasajeros: PassengerPaymentSummary[];
|
||||
tarifaPorPasajero: number;
|
||||
tarifaTotal: number;
|
||||
porcentajeComision: number;
|
||||
comisionAdmin: number;
|
||||
pagoNetoConductor: number;
|
||||
totalRecaudadoPlataforma: number;
|
||||
totalRecaudadoEfectivo: number;
|
||||
totalPendienteEfectivo: number;
|
||||
// Balance neto entre plataforma y conductor
|
||||
saldoNetoParaConductor: number; // Plataforma le abona esto al conductor
|
||||
saldoComisionParaPlataforma: number; // Conductor debe esta comisión si cobró todo en efectivo
|
||||
estadoLiquidacion: 'PENDIENTE' | 'LIQUIDADO' | 'TRANSFERIDO_BRE_B';
|
||||
liquidacion?: ITripLiquidacion;
|
||||
}
|
||||
|
||||
export interface BreBPayoutRequest {
|
||||
conductorId: string;
|
||||
monto: number;
|
||||
tripId?: string;
|
||||
llaveBreB?: string;
|
||||
tipoLlave?: 'CELULAR' | 'CEDULA' | 'EMAIL' | 'ALFANUMERICA';
|
||||
bancoDestino?: string;
|
||||
titularNombre?: string;
|
||||
}
|
||||
|
||||
export interface BreBTransferReceipt {
|
||||
success: boolean;
|
||||
referenciaBreB: string;
|
||||
comprobanteId: string;
|
||||
cicloCompensacion: string;
|
||||
fechaHora: string;
|
||||
monto: number;
|
||||
moneda: string;
|
||||
costoTransaccion: number;
|
||||
entidadOrigen: string;
|
||||
entidadDestino: string;
|
||||
tipoLlave: string;
|
||||
llaveDestino: string;
|
||||
titularDestino: string;
|
||||
estado: 'APROBADA_INMEDIATA' | 'PROCESANDO' | 'RECHAZADA';
|
||||
codigoAutorizacion: string;
|
||||
nuevoSaldoBilletera: number;
|
||||
mensaje: string;
|
||||
}
|
||||
|
||||
export class SettlementService {
|
||||
private static instance: SettlementService;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): SettlementService {
|
||||
if (!SettlementService.instance) {
|
||||
SettlementService.instance = new SettlementService();
|
||||
}
|
||||
return SettlementService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el desglose financiero en tiempo real de un viaje,
|
||||
* discriminando qué pasajeros pagaron online (Bold, Nequi, Bre-B)
|
||||
* y cuáles están pendientes por pagar en efectivo o ya pagaron en mano.
|
||||
*/
|
||||
public async getTripFinancialSummary(tripId: string): Promise<TripFinancialSummary> {
|
||||
const trip = await Trip.findById(tripId).populate('conductorId', 'nombre telefono email');
|
||||
if (!trip) {
|
||||
throw new Error(`Viaje #${tripId} no encontrado`);
|
||||
}
|
||||
|
||||
const tariffs = await tariffsService.getTarifasActivas();
|
||||
const comisionPct = (trip.comisionAdmin > 0 && trip.tarifaTotal > 0)
|
||||
? Math.round((trip.comisionAdmin / trip.tarifaTotal) * 100)
|
||||
: tariffs.porcentajeComisionAdmin || 15;
|
||||
|
||||
let driverInfo: any = undefined;
|
||||
let driverProfile: IDriverProfile | null = null;
|
||||
|
||||
if (trip.conductorId) {
|
||||
const conductorUser: any = trip.conductorId;
|
||||
driverProfile = await DriverProfile.findOne({ userId: conductorUser._id });
|
||||
driverInfo = {
|
||||
id: conductorUser._id.toString(),
|
||||
nombre: conductorUser.nombre || 'Conductor Asignado',
|
||||
telefono: conductorUser.telefono || '',
|
||||
placa: driverProfile?.placaVehiculo || 'POR ASIGNAR',
|
||||
modelo: driverProfile?.modeloVehiculo || '',
|
||||
llaveBreB: driverProfile?.llaveBreB,
|
||||
};
|
||||
}
|
||||
|
||||
let totalRecaudadoPlataforma = 0;
|
||||
let totalRecaudadoEfectivo = 0;
|
||||
let totalPendienteEfectivo = 0;
|
||||
let tarifaTotalCalculada = 0;
|
||||
|
||||
const pasajerosResumen: PassengerPaymentSummary[] = (trip.pasajeros || []).map((p: any) => {
|
||||
const valor = p.valor || tariffs.tarifaPasajeroFija || 35000;
|
||||
tarifaTotalCalculada += valor;
|
||||
|
||||
const esPlataforma = ['PAGADO_BOLD', 'PAGADO_NEQUI', 'PAGADO_BRE_B'].includes(p.estadoPago);
|
||||
const esRecaudadoEfectivo = p.estadoPago === 'PAGADO_EFECTIVO';
|
||||
const esPendienteEfectivo = p.estadoPago === 'PENDIENTE_EFECTIVO';
|
||||
|
||||
if (esPlataforma) totalRecaudadoPlataforma += valor;
|
||||
if (esRecaudadoEfectivo) totalRecaudadoEfectivo += valor;
|
||||
if (esPendienteEfectivo) totalPendienteEfectivo += valor;
|
||||
|
||||
return {
|
||||
pasajeroId: p._id?.toString() || p.usuarioId?.toString(),
|
||||
nombre: p.nombre,
|
||||
telefono: p.telefono,
|
||||
direccionRecogida: p.direccionRecogida,
|
||||
estadoPago: p.estadoPago,
|
||||
valor,
|
||||
esRecaudadoPlataforma: esPlataforma,
|
||||
esRecaudadoEfectivo,
|
||||
esPendienteEfectivo,
|
||||
};
|
||||
});
|
||||
|
||||
// Si el viaje es de paquetería
|
||||
if (trip.tipoServicio === 'MENSAJERIA') {
|
||||
tarifaTotalCalculada = trip.tarifaTotal || tarifaTotalCalculada;
|
||||
if (trip.boldPayment?.transactionStatus === 'PAID') {
|
||||
totalRecaudadoPlataforma = tarifaTotalCalculada;
|
||||
}
|
||||
}
|
||||
|
||||
const tarifaTotal = trip.tarifaTotal > 0 ? trip.tarifaTotal : tarifaTotalCalculada;
|
||||
const comisionAdmin = Math.round((tarifaTotal * comisionPct) / 100);
|
||||
const pagoNetoConductor = tarifaTotal - comisionAdmin;
|
||||
|
||||
// Cálculo del balance neto entre Plataforma y Conductor:
|
||||
// Lo que la plataforma debe abonar al conductor = Recaudado por Plataforma - Comisión Plataforma
|
||||
let saldoNetoParaConductor = 0;
|
||||
let saldoComisionParaPlataforma = 0;
|
||||
|
||||
const recaudosTotalesCobrados = totalRecaudadoPlataforma + totalRecaudadoEfectivo;
|
||||
const comisionTotalGenerada = Math.round((recaudosTotalesCobrados * comisionPct) / 100);
|
||||
|
||||
if (totalRecaudadoPlataforma >= comisionTotalGenerada) {
|
||||
saldoNetoParaConductor = totalRecaudadoPlataforma - comisionTotalGenerada;
|
||||
} else {
|
||||
saldoComisionParaPlataforma = comisionTotalGenerada - totalRecaudadoPlataforma;
|
||||
}
|
||||
|
||||
let estadoLiquidacion: 'PENDIENTE' | 'LIQUIDADO' | 'TRANSFERIDO_BRE_B' =
|
||||
trip.liquidacion?.estadoLiquidacion || (trip.estado === 'COMPLETED' ? 'LIQUIDADO' : 'PENDIENTE');
|
||||
|
||||
return {
|
||||
tripId: trip._id.toString(),
|
||||
estadoViaje: trip.estado,
|
||||
fechaSalida: trip.fechaHoraSalidaProgramada,
|
||||
origenCiudad: trip.origen?.ciudad || 'CALI',
|
||||
destinoCiudad: trip.destino?.ciudad || 'BUENAVENTURA',
|
||||
conductor: driverInfo,
|
||||
totalPasajeros: pasajerosResumen.length,
|
||||
pasajeros: pasajerosResumen,
|
||||
tarifaPorPasajero: tariffs.tarifaPasajeroFija || 35000,
|
||||
tarifaTotal,
|
||||
porcentajeComision: comisionPct,
|
||||
comisionAdmin,
|
||||
pagoNetoConductor,
|
||||
totalRecaudadoPlataforma,
|
||||
totalRecaudadoEfectivo,
|
||||
totalPendienteEfectivo,
|
||||
saldoNetoParaConductor,
|
||||
saldoComisionParaPlataforma,
|
||||
estadoLiquidacion,
|
||||
liquidacion: trip.liquidacion,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambia el estado de pago de un pasajero específico en un viaje planificado/activo.
|
||||
* Por ejemplo: marcar que el pasajero ya pagó en efectivo al abordar (PAGADO_EFECTIVO)
|
||||
* o revertir a PENDIENTE_EFECTIVO si aún no lo ha hecho.
|
||||
*/
|
||||
public async marcarPagoPasajero(
|
||||
tripId: string,
|
||||
pasajeroId: string,
|
||||
nuevoEstado: PassengerPaymentStatus
|
||||
): Promise<TripFinancialSummary> {
|
||||
const trip = await Trip.findById(tripId);
|
||||
if (!trip) throw new Error(`Viaje #${tripId} no encontrado`);
|
||||
|
||||
const pasajero = trip.pasajeros.find(
|
||||
(p: any) => p._id?.toString() === pasajeroId || p.usuarioId?.toString() === pasajeroId
|
||||
);
|
||||
|
||||
if (!pasajero) {
|
||||
throw new Error(`Pasajero ${pasajeroId} no encontrado en el viaje`);
|
||||
}
|
||||
|
||||
const estadoAnterior = pasajero.estadoPago;
|
||||
pasajero.estadoPago = nuevoEstado;
|
||||
if (nuevoEstado === 'PAGADO_EFECTIVO' || nuevoEstado === 'PAGADO_BOLD' || nuevoEstado === 'PAGADO_BRE_B') {
|
||||
pasajero.recogido = true;
|
||||
}
|
||||
|
||||
await trip.save();
|
||||
console.log(
|
||||
`💳 [Pago Actualizado] Viaje #${trip._id} Pasajero ${pasajero.nombre}: ${estadoAnterior} ➔ ${nuevoEstado}`
|
||||
);
|
||||
|
||||
return this.getTripFinancialSummary(tripId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finaliza el recorrido formalmente (COMPLETED) y realiza la liquidación financiera.
|
||||
* Asigna automáticamente el saldo a favor a la billetera del conductor o registra
|
||||
* la comisión pendiente si recaudó todo en efectivo.
|
||||
*/
|
||||
public async finalizarRecorridoYLiquidar(tripId: string, adminId?: string): Promise<{
|
||||
trip: ITrip;
|
||||
resumen: TripFinancialSummary;
|
||||
saldoAcreditadoConductor: number;
|
||||
nuevoSaldoBilletera: number;
|
||||
}> {
|
||||
const trip = await Trip.findById(tripId).populate('conductorId');
|
||||
if (!trip) throw new Error(`Viaje #${tripId} no encontrado`);
|
||||
|
||||
const resumen = await this.getTripFinancialSummary(tripId);
|
||||
|
||||
// Calcular valores definitivos de liquidación
|
||||
const saldoNetoParaConductor = resumen.saldoNetoParaConductor;
|
||||
const saldoComisionParaPlataforma = resumen.saldoComisionParaPlataforma;
|
||||
|
||||
const liquidacionData: ITripLiquidacion = {
|
||||
fechaLiquidacion: new Date(),
|
||||
totalRecaudadoPlataforma: resumen.totalRecaudadoPlataforma,
|
||||
totalRecaudadoEfectivo: resumen.totalRecaudadoEfectivo,
|
||||
tarifaTotal: resumen.tarifaTotal,
|
||||
comisionAdmin: resumen.comisionAdmin,
|
||||
pagoConductor: resumen.pagoNetoConductor,
|
||||
saldoNetoParaConductor,
|
||||
saldoComisionParaPlataforma,
|
||||
estadoLiquidacion: 'LIQUIDADO',
|
||||
};
|
||||
|
||||
trip.estado = 'COMPLETED';
|
||||
trip.tarifaTotal = resumen.tarifaTotal;
|
||||
trip.comisionAdmin = resumen.comisionAdmin;
|
||||
trip.pagoConductor = resumen.pagoNetoConductor;
|
||||
trip.liquidacion = liquidacionData;
|
||||
|
||||
await trip.save();
|
||||
|
||||
let saldoAcreditado = 0;
|
||||
let nuevoSaldoBilletera = 0;
|
||||
|
||||
// Actualizar cuenta y billetera del conductor
|
||||
if (trip.conductorId) {
|
||||
const conductorUser: any = trip.conductorId;
|
||||
const driverProfile = await DriverProfile.findOne({ userId: conductorUser._id });
|
||||
|
||||
if (driverProfile) {
|
||||
if (saldoNetoParaConductor > 0) {
|
||||
driverProfile.billeteraSaldo = (driverProfile.billeteraSaldo || 0) + saldoNetoParaConductor;
|
||||
driverProfile.saldoPendientePlataforma = (driverProfile.saldoPendientePlataforma || 0) + saldoNetoParaConductor;
|
||||
saldoAcreditado = saldoNetoParaConductor;
|
||||
}
|
||||
|
||||
if (saldoComisionParaPlataforma > 0) {
|
||||
driverProfile.comisionesPendientesPago = (driverProfile.comisionesPendientesPago || 0) + saldoComisionParaPlataforma;
|
||||
}
|
||||
|
||||
driverProfile.totalViajesRealizados = (driverProfile.totalViajesRealizados || 0) + 1;
|
||||
await driverProfile.save();
|
||||
nuevoSaldoBilletera = driverProfile.billeteraSaldo;
|
||||
|
||||
// Notificación automática por WhatsApp al conductor
|
||||
const msgConductor =
|
||||
`🏁 *¡RECORRIDO FINALIZADO Y LIQUIDADO!*\n\n` +
|
||||
`Hola *${conductorUser.nombre}*, tu viaje #${trip._id.toString().slice(-6)} (${trip.origen.ciudad} ➔ ${trip.destino.ciudad}) ha sido completado con éxito.\n\n` +
|
||||
`📊 *Liquidación Financiera Oficial:*\n` +
|
||||
`• *Total Pasajes del Viaje:* $${resumen.tarifaTotal.toLocaleString('es-CO')} COP\n` +
|
||||
`• *Recaudado Online por Plataforma:* $${resumen.totalRecaudadoPlataforma.toLocaleString('es-CO')} COP\n` +
|
||||
`• *Recaudado en Efectivo por Ti:* $${resumen.totalRecaudadoEfectivo.toLocaleString('es-CO')} COP\n` +
|
||||
`• *Comisión Pacifictrans Go (${resumen.porcentajeComision}%):* $${resumen.comisionAdmin.toLocaleString('es-CO')} COP\n` +
|
||||
`• *Tu Ganancia Neta (85%):* $${resumen.pagoNetoConductor.toLocaleString('es-CO')} COP\n\n` +
|
||||
(saldoNetoParaConductor > 0
|
||||
? `💰 *Acreditado a tu Billetera:* +$${saldoNetoParaConductor.toLocaleString('es-CO')} COP\n` +
|
||||
`💳 *Saldo Disponible para Retiro Bre-B:* $${nuevoSaldoBilletera.toLocaleString('es-CO')} COP\n\n` +
|
||||
`_Puedes transferir tu saldo inmediatamente a cualquier cuenta bancaria a través de Bre-B._`
|
||||
: `ℹ️ Has recaudado en mano tu tarifa neta en efectivo.`);
|
||||
|
||||
await whatsAppNotificationService.sendTextMessage(conductorUser.telefono, msgConductor);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ [Viaje Liquidado] #${trip._id} completado. Acreditado a conductor: $${saldoAcreditado.toLocaleString('es-CO')} COP`
|
||||
);
|
||||
|
||||
return {
|
||||
trip,
|
||||
resumen: await this.getTripFinancialSummary(tripId),
|
||||
saldoAcreditadoConductor: saldoAcreditado,
|
||||
nuevoSaldoBilletera,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Realiza la transferencia inmediata interoperable por Bre-B (Banco de la República)
|
||||
* del saldo acumulado en la billetera del conductor hacia su cuenta bancaria.
|
||||
*/
|
||||
public async realizarTransferenciaBreB(params: BreBPayoutRequest): Promise<BreBTransferReceipt> {
|
||||
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`
|
||||
);
|
||||
}
|
||||
|
||||
// Resolver llave Bre-B
|
||||
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 procesar el pago.');
|
||||
}
|
||||
|
||||
// Generar identificadores oficiales interoperables de Bre-B
|
||||
const fechaActual = new Date();
|
||||
const dateStr = fechaActual.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const hexRandom = crypto.randomBytes(3).toString('hex').toUpperCase();
|
||||
const referenciaBreB = `BREB-CO-${dateStr}-${hexRandom}`;
|
||||
const comprobanteId = `COMP-${Date.now()}-${crypto.randomBytes(2).toString('hex').toUpperCase()}`;
|
||||
const cicloCompensacion = `BANREP-CICLO-INMEDIATO-${Date.now().toString().slice(-4)}`;
|
||||
const codAutorizacion = `AUT-${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
|
||||
|
||||
// Descontar saldo y registrar en el perfil del conductor
|
||||
driverProfile.billeteraSaldo -= monto;
|
||||
if ((driverProfile.saldoPendientePlataforma || 0) >= monto) {
|
||||
driverProfile.saldoPendientePlataforma -= monto;
|
||||
} else {
|
||||
driverProfile.saldoPendientePlataforma = 0;
|
||||
}
|
||||
|
||||
// Guardar / actualizar llave si se proveyó una nueva
|
||||
if (params.llaveBreB) {
|
||||
driverProfile.llaveBreB = {
|
||||
tipo: tipoLlave,
|
||||
valor: llaveDestino,
|
||||
bancoDestino,
|
||||
titularNombre: titularDestino,
|
||||
actualizadoEn: fechaActual,
|
||||
};
|
||||
}
|
||||
|
||||
const registroTransferencia = {
|
||||
referencia: referenciaBreB,
|
||||
monto,
|
||||
fecha: fechaActual,
|
||||
bancoDestino,
|
||||
llaveDestino,
|
||||
tipoLlave,
|
||||
estado: 'EXITOSO' as const,
|
||||
comprobanteId,
|
||||
tripId: tripId ? new Types.ObjectId(tripId) : undefined,
|
||||
nota: `Transferencia Inmediata Bre-B a ${bancoDestino} (Llave ${tipoLlave}: ${llaveDestino})`,
|
||||
};
|
||||
|
||||
driverProfile.historialTransferenciasBreB = driverProfile.historialTransferenciasBreB || [];
|
||||
driverProfile.historialTransferenciasBreB.unshift(registroTransferencia);
|
||||
|
||||
await driverProfile.save();
|
||||
|
||||
// Si había un viaje asociado, actualizar su estado de liquidación
|
||||
if (tripId) {
|
||||
await Trip.findByIdAndUpdate(tripId, {
|
||||
'liquidacion.estadoLiquidacion': 'TRANSFERIDO_BRE_B',
|
||||
'liquidacion.referenciaBreB': referenciaBreB,
|
||||
'liquidacion.transferidoEn': fechaActual,
|
||||
'liquidacion.bancoDestino': bancoDestino,
|
||||
'liquidacion.llaveDestino': llaveDestino,
|
||||
});
|
||||
}
|
||||
|
||||
// Enviar comprobante oficial por WhatsApp al conductor
|
||||
const fechaFormateada = fechaActual.toLocaleString('es-CO', {
|
||||
timeZone: 'America/Bogota',
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
|
||||
const msgWhatsApp =
|
||||
`🏦 *COMPROBANTE OFICIAL DE TRANSFERENCIA BRE-B*\n` +
|
||||
`_Sistema Nacional de Pagos Inmediatos - Banco de la República_\n\n` +
|
||||
`✅ *ESTADO:* TRANSFERENCIA EXITOSA (24/7)\n` +
|
||||
`📄 *Referencia Bre-B:* \`${referenciaBreB}\`\n` +
|
||||
`🔖 *No. Aprobación:* \`${codAutorizacion}\`\n` +
|
||||
`📅 *Fecha y Hora:* ${fechaFormateada}\n\n` +
|
||||
`💵 *Monto Transferido:* *$${monto.toLocaleString('es-CO')} COP*\n` +
|
||||
`🏦 *Entidad Destino:* ${bancoDestino}\n` +
|
||||
`🔑 *Llave Bre-B (${tipoLlave}):* ${llaveDestino}\n` +
|
||||
`👤 *Titular:* ${titularDestino}\n` +
|
||||
`💸 *Costo de la Transacción:* $0 COP (Bre-B Interoperable)\n\n` +
|
||||
`💼 *Nuevo Saldo en Billetera:* $${driverProfile.billeteraSaldo.toLocaleString('es-CO')} COP\n\n` +
|
||||
`_Pacifictrans Go • Pagos Inmediatos en Tiempo Real_`;
|
||||
|
||||
await whatsAppNotificationService.sendTextMessage(conductorUser.telefono, msgWhatsApp);
|
||||
|
||||
console.log(`💸 [Bre-B Transferencia Exitosa] ${referenciaBreB} por $${monto.toLocaleString('es-CO')} COP a ${bancoDestino}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
referenciaBreB,
|
||||
comprobanteId,
|
||||
cicloCompensacion,
|
||||
fechaHora: fechaFormateada,
|
||||
monto,
|
||||
moneda: 'COP',
|
||||
costoTransaccion: 0,
|
||||
entidadOrigen: 'Pacifictrans Go - Banco de la República Bre-B Direct',
|
||||
entidadDestino: bancoDestino,
|
||||
tipoLlave,
|
||||
llaveDestino,
|
||||
titularDestino,
|
||||
estado: 'APROBADA_INMEDIATA',
|
||||
codigoAutorizacion: codAutorizacion,
|
||||
nuevoSaldoBilletera: driverProfile.billeteraSaldo,
|
||||
mensaje: `Transferencia de $${monto.toLocaleString('es-CO')} COP despachada exitosamente a ${bancoDestino} a través de Bre-B.`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la billetera y el estado de pagos de un conductor
|
||||
*/
|
||||
public async getBilleteraConductor(conductorId: string) {
|
||||
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) {
|
||||
// Si no existe, crear perfil básico
|
||||
driverProfile = await DriverProfile.create({
|
||||
userId: conductorUser._id,
|
||||
placaVehiculo: 'WLZ-890',
|
||||
modeloVehiculo: 'Renault Duster 2024',
|
||||
capacidadMax: 4,
|
||||
coordenadasActuales: { type: 'Point', coordinates: [-76.532, 3.451] },
|
||||
ciudadBase: 'CALI',
|
||||
billeteraSaldo: 0,
|
||||
saldoPendientePlataforma: 0,
|
||||
comisionesPendientesPago: 0,
|
||||
llaveBreB: {
|
||||
tipo: 'CELULAR',
|
||||
valor: conductorUser.telefono.replace(/\D/g, ''),
|
||||
bancoDestino: 'Bancolombia',
|
||||
titularNombre: conductorUser.nombre,
|
||||
actualizadoEn: new Date(),
|
||||
},
|
||||
historialTransferenciasBreB: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener viajes recientes completados por este conductor
|
||||
const viajesRecientes = await Trip.find({
|
||||
conductorId: conductorUser._id,
|
||||
estado: { $in: ['SCHEDULED', 'ACTIVE', 'IN_PROGRESS', 'COMPLETED'] },
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(10)
|
||||
.lean();
|
||||
|
||||
return {
|
||||
conductor: {
|
||||
id: conductorUser._id.toString(),
|
||||
nombre: conductorUser.nombre,
|
||||
telefono: conductorUser.telefono,
|
||||
email: conductorUser.email,
|
||||
placa: driverProfile.placaVehiculo,
|
||||
modelo: driverProfile.modeloVehiculo,
|
||||
},
|
||||
billeteraSaldo: driverProfile.billeteraSaldo || 0,
|
||||
saldoPendientePlataforma: driverProfile.saldoPendientePlataforma || 0,
|
||||
comisionesPendientesPago: driverProfile.comisionesPendientesPago || 0,
|
||||
totalViajesRealizados: driverProfile.totalViajesRealizados || 0,
|
||||
llaveBreB: driverProfile.llaveBreB || {
|
||||
tipo: 'CELULAR',
|
||||
valor: conductorUser.telefono.replace(/\D/g, ''),
|
||||
bancoDestino: 'Bancolombia',
|
||||
titularNombre: conductorUser.nombre,
|
||||
},
|
||||
historialTransferenciasBreB: driverProfile.historialTransferenciasBreB || [],
|
||||
viajesRecientes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda o actualiza la llave Bre-B registrada por el conductor
|
||||
*/
|
||||
public async actualizarLlaveBreB(
|
||||
conductorId: string,
|
||||
data: { tipo: 'CELULAR' | 'CEDULA' | 'EMAIL' | 'ALFANUMERICA'; valor: string; bancoDestino: string; titularNombre?: string }
|
||||
) {
|
||||
const driverProfile = await DriverProfile.findOne({ userId: conductorId });
|
||||
if (!driverProfile) {
|
||||
throw new Error(`Perfil vehicular no encontrado para el usuario ${conductorId}`);
|
||||
}
|
||||
|
||||
driverProfile.llaveBreB = {
|
||||
tipo: data.tipo || 'CELULAR',
|
||||
valor: data.valor.trim(),
|
||||
bancoDestino: data.bancoDestino || 'Bancolombia',
|
||||
titularNombre: data.titularNombre?.trim() || '',
|
||||
actualizadoEn: new Date(),
|
||||
};
|
||||
|
||||
await driverProfile.save();
|
||||
return driverProfile.llaveBreB;
|
||||
}
|
||||
|
||||
/**
|
||||
* Panorama general de liquidaciones y pagos para el panel de administración
|
||||
*/
|
||||
public async getOverviewLiquidaciones() {
|
||||
const viajesProgramadosYActivos = await Trip.find({
|
||||
estado: { $in: ['SCHEDULED', 'ACTIVE', 'IN_PROGRESS', 'POOL_COMPLETED'] },
|
||||
tipoServicio: 'TRANSPORTE',
|
||||
})
|
||||
.populate('conductorId', 'nombre telefono')
|
||||
.sort({ fechaHoraSalidaProgramada: 1, createdAt: -1 })
|
||||
.lean();
|
||||
|
||||
const viajesCompletadosRecientes = await Trip.find({
|
||||
estado: 'COMPLETED',
|
||||
})
|
||||
.populate('conductorId', 'nombre telefono')
|
||||
.sort({ updatedAt: -1 })
|
||||
.limit(10)
|
||||
.lean();
|
||||
|
||||
// Obtener estadísticas globales de saldo y transferencias
|
||||
const perfilesConductores = await DriverProfile.find({}).lean();
|
||||
let totalSaldoEnBilleteras = 0;
|
||||
let totalTransferidoBreB = 0;
|
||||
let totalTransferenciasCount = 0;
|
||||
|
||||
perfilesConductores.forEach((dp: any) => {
|
||||
totalSaldoEnBilleteras += dp.billeteraSaldo || 0;
|
||||
(dp.historialTransferenciasBreB || []).forEach((t: any) => {
|
||||
totalTransferidoBreB += t.monto || 0;
|
||||
totalTransferenciasCount += 1;
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
viajesProgramadosYActivos,
|
||||
viajesCompletadosRecientes,
|
||||
metricas: {
|
||||
totalSaldoEnBilleteras,
|
||||
totalTransferidoBreB,
|
||||
totalTransferenciasCount,
|
||||
viajesPendientesLiquidacion: viajesProgramadosYActivos.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const settlementService = SettlementService.getInstance();
|
||||
Loading…
Reference in New Issue
Block a user