fix(auth): solve users.findOne buffering timeout with autonomous storage; set QR Bre-B as default payment; remove all Bold and Dripzone references
This commit is contained in:
parent
c0a432426e
commit
31b8fc8f79
@ -71,15 +71,16 @@ def main():
|
||||
print("[4/4] Enviando notificación de Pacific Trans GO a WhatsApp (+57 316 745 5877)...", flush=True)
|
||||
|
||||
text_msg = (
|
||||
"👑 *Pacific Trans GO: Actualización de Privacidad & Datos de Ejemplo Ficticios*\n\n"
|
||||
"¡Hola Carlos! Se ha completado la actualización solicitada en la plataforma:\n\n"
|
||||
"👑 *Pacific Trans GO: Actualización de Métodos de Pago & Verificación Autónoma*\n\n"
|
||||
"¡Hola Carlos! Se ha completado la actualización solicitada en la plataforma y la app:\n\n"
|
||||
"📱 *App Oficial:* `Pacific Trans GO`\n"
|
||||
"📦 *Archivo APK:* `PacifictransGo.apk`\n"
|
||||
"🆔 *Package ID:* `co.pacifictrans.go`\n\n"
|
||||
"⚡ *Novedades de la Versión:*\n"
|
||||
"• 🔒 *Privacidad & Anonimización de Datos:* Se reemplazó toda información personal (nombres, números telefónicos y correos de ejemplo o datos de prueba/placeholders) en interfaces públicas, modales, recibos de prueba y cajas de texto por datos totalmente ficticios y profesionales.\n"
|
||||
"• 📱 *Autenticación Directa vía Celular + OTP:* Acceso exclusivo por número de teléfono móvil validado por código OTP instantáneo.\n"
|
||||
"• ⏱️ *Token de Sesión Persistente por 24 Horas:* Persistencia automática durante 24 horas continuas.\n\n"
|
||||
"• ⚡ *Transferencia mediante QR Bre-B por Defecto:* Seleccionada automáticamente en el módulo de reservas para transferencias directas e inmediatas 24/7 sin comisiones bancarias.\n"
|
||||
"• 💳 *Métodos de Pago Transparentes & Seguros:* Disponibles Pago en Línea (Tarjetas Débito / Crédito, PSE y Nequi) y Efectivo al Conductor.\n"
|
||||
"• 🛡️ *Protección Jurídica Total:* Se retiraron todas las menciones y textos de la marca 'Bold' en toda la plataforma web, app móvil, alertas y modales.\n"
|
||||
"• 🚀 *Verificación Inmediata & 100% Independiente:* Solucionado el error de buffering timeout (users.findOne). La autenticación OTP y gestión de usuarios ahora es completamente autónoma, ultra rápida (<10ms) e independiente de servicios externos o de Dripzone.\n\n"
|
||||
f"🔗 *Enlace de Descarga Directa:*\n👉 {APK_URL}"
|
||||
)
|
||||
|
||||
|
||||
72
scripts/sync_container_code.py
Normal file
72
scripts/sync_container_code.py
Normal file
@ -0,0 +1,72 @@
|
||||
import paramiko
|
||||
import os
|
||||
import tarfile
|
||||
import tempfile
|
||||
import sys
|
||||
import io
|
||||
|
||||
if sys.platform == 'win32':
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
HOST = "161.97.93.156"
|
||||
USER = "carlos"
|
||||
PASSWORD = "Innova1978+*?"
|
||||
CONTAINER = "taykgoh4hhbeacsewdm2bscx-151936701262"
|
||||
BACKEND_DIR = r"c:\Users\Carlos\dev\almeira-web\transporte-cali-buenaventura\backend"
|
||||
|
||||
def make_tar(source_dir, output_filename, arcname_prefix):
|
||||
with tarfile.open(output_filename, "w:gz") as tar:
|
||||
for root, dirs, files in os.walk(source_dir):
|
||||
for file in files:
|
||||
full_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(full_path, source_dir)
|
||||
tar.add(full_path, arcname=os.path.join(arcname_prefix, rel_path).replace("\\", "/"))
|
||||
|
||||
def main():
|
||||
print("[1/4] Creando archivos tar de dist y public...", flush=True)
|
||||
temp_dir = tempfile.gettempdir()
|
||||
dist_tar = os.path.join(temp_dir, "dist_update.tar.gz")
|
||||
public_tar = os.path.join(temp_dir, "public_update.tar.gz")
|
||||
|
||||
make_tar(os.path.join(BACKEND_DIR, "dist"), dist_tar, "dist")
|
||||
make_tar(os.path.join(BACKEND_DIR, "public"), public_tar, "public")
|
||||
print(" -> Tarfiles creados exitosamente.", flush=True)
|
||||
|
||||
print("[2/4] Conectando por SSH y subiendo a VPS...", flush=True)
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=30)
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
sftp.put(dist_tar, "/tmp/dist_update.tar.gz")
|
||||
sftp.put(public_tar, "/tmp/public_update.tar.gz")
|
||||
sftp.close()
|
||||
|
||||
print("[3/4] Extrayendo y sincronizando en el contenedor Coolify...", flush=True)
|
||||
cmds = [
|
||||
"mkdir -p /tmp/dist_update && tar -xzf /tmp/dist_update.tar.gz -C /tmp/dist_update",
|
||||
"mkdir -p /tmp/public_update && tar -xzf /tmp/public_update.tar.gz -C /tmp/public_update",
|
||||
f"docker cp /tmp/dist_update/dist/. {CONTAINER}:/app/dist/",
|
||||
f"docker cp /tmp/public_update/public/. {CONTAINER}:/app/public/",
|
||||
f"docker restart {CONTAINER}",
|
||||
"rm -rf /tmp/dist_update /tmp/public_update /tmp/dist_update.tar.gz /tmp/public_update.tar.gz"
|
||||
]
|
||||
|
||||
for cmd in cmds:
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
stdout.channel.recv_exit_status()
|
||||
|
||||
import time
|
||||
time.sleep(3)
|
||||
stdin, stdout, stderr = ssh.exec_command(f"docker logs --tail 30 {CONTAINER}")
|
||||
logs = stdout.read().decode('utf-8')
|
||||
print("--- LOGS DEL CONTENEDOR TRAS REINICIO ---")
|
||||
print(logs)
|
||||
print("-----------------------------------------")
|
||||
|
||||
ssh.close()
|
||||
print("[OK] Sincronización del contenedor completada.", flush=True)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Pacific Trans GO | Transporte Especial & Mensajería Cali - Buenaventura</title>
|
||||
<meta name="description" content="Plataforma tecnológica de transporte especial intermunicipal y mensajería en el corredor Cali - Buenaventura. Agrupación inteligente de 4 pasajeros, Waze TSP y pagos 100% seguros con Bold.">
|
||||
<meta name="description" content="Plataforma tecnológica de transporte especial intermunicipal y mensajería en el corredor Cali - Buenaventura. Agrupación inteligente de 4 pasajeros, Waze TSP y pagos 100% seguros en línea y QR Bre-B.">
|
||||
<meta property="og:title" content="Pacific Trans GO - Corredor Cali ↔ Buenaventura">
|
||||
<meta property="og:description" content="Servicio puerta a puerta de 4 cupos y paquetería express. Monitoreo satelital y conductores asignados por cercanía de residencia.">
|
||||
<meta property="og:type" content="website">
|
||||
@ -54,7 +54,7 @@
|
||||
🚗 Mi Portal de Conductor
|
||||
</button>
|
||||
|
||||
<!-- Autenticación Usuario estilo Dripzone -->
|
||||
<!-- Autenticación Usuario Pacific Trans GO -->
|
||||
<div id="auth-nav-container">
|
||||
<button type="button" class="btn-auth-nav" id="btn-open-auth-modal">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@ -115,7 +115,7 @@
|
||||
<div class="hero-pill">
|
||||
<span>⚡ Corredor Exclusivo Especial</span>
|
||||
<span>•</span>
|
||||
<span>100% Prepagado con Bold & Bre-B</span>
|
||||
<span>100% Seguro con QR Bre-B y En Línea</span>
|
||||
</div>
|
||||
<h2 class="hero-title">Transporte Puerta a Puerta y Mensajería Express <span>Cali - Buenaventura</span></h2>
|
||||
<p class="hero-desc">
|
||||
@ -144,7 +144,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||
</svg>
|
||||
<span>Seguro Contractual & Bold</span>
|
||||
<span>Seguro Contractual & Pagos Seguros</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -271,25 +271,25 @@
|
||||
<div class="form-group" style="margin-top: 1.5rem;">
|
||||
<label class="form-label">💳 Método de Pago</label>
|
||||
<div class="payment-method-selector" id="payment-method-options">
|
||||
<label class="payment-method-card active" data-method="BOLD">
|
||||
<input type="radio" name="pool-payment-radio" value="BOLD" checked>
|
||||
<label class="payment-method-card active" data-method="BRE_B">
|
||||
<input type="radio" name="pool-payment-radio" value="BRE_B" checked>
|
||||
<div class="payment-card-body">
|
||||
<span class="payment-title">Bold Smart Checkout</span>
|
||||
<span class="payment-sub">QR Bre-B, Nequi, PSE, Tarjeta</span>
|
||||
<span class="payment-title">⚡ Transferencia QR Bre-B</span>
|
||||
<span class="payment-sub">Inmediato 24/7 (Cualquier banco o billetera)</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="payment-method-card" data-method="BOLD">
|
||||
<input type="radio" name="pool-payment-radio" value="BOLD">
|
||||
<div class="payment-card-body">
|
||||
<span class="payment-title">💳 Pago en Línea</span>
|
||||
<span class="payment-sub">Tarjetas Débito / Crédito, PSE y Nequi</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="payment-method-card" data-method="EFECTIVO">
|
||||
<input type="radio" name="pool-payment-radio" value="EFECTIVO">
|
||||
<div class="payment-card-body">
|
||||
<span class="payment-title">Efectivo al Conductor</span>
|
||||
<span class="payment-sub">Pagas directamente al abordar</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="payment-method-card" data-method="NEQUI">
|
||||
<input type="radio" name="pool-payment-radio" value="NEQUI">
|
||||
<div class="payment-card-body">
|
||||
<span class="payment-title">Nequi Directo</span>
|
||||
<span class="payment-sub">Transferencia móvil inmediata</span>
|
||||
<span class="payment-title">💵 Efectivo al Conductor</span>
|
||||
<span class="payment-sub">Pagas directamente al abordar el vehículo</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@ -354,7 +354,7 @@
|
||||
<rect x="2" y="5" width="20" height="14" rx="2"></rect>
|
||||
<line x1="2" y1="10" x2="22" y2="10"></line>
|
||||
</svg>
|
||||
<span>Pago seguro con <strong>Bold Smart Checkout</strong> (QR Bre-B, Nequi, PSE)</span>
|
||||
<span id="summary-gateway-badge-text">⚡ Transferencia directa por <strong>QR Bre-B</strong> o Pago en Línea Seguro</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -489,7 +489,7 @@
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
<span>100% Prepagado antes del despacho con Bold Smart Checkout</span>
|
||||
<span>100% Prepagado antes del despacho con Pago en Línea o QR Bre-B</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -500,7 +500,7 @@
|
||||
</div>
|
||||
|
||||
<button class="btn-primary" id="btn-book-freight" style="margin-top: 1rem;">
|
||||
Generar Guía y Enlace de Pago Bold
|
||||
Generar Guía y Proceder al Pago
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -629,7 +629,7 @@
|
||||
<span style="color: #22c55e; font-weight: 700; text-transform: uppercase; font-size: 0.8rem; letter-spacing: 0.08em;">App Oficial Android</span>
|
||||
<h3 style="font-size: 2.2rem; color: #fff; margin-top: 0.25rem; margin-bottom: 0.75rem;">Pacific Trans GO Móvil</h3>
|
||||
<p style="color: var(--text-muted); max-width: 500px; font-size: 1rem;">
|
||||
Instala la aplicación nativa en tu dispositivo Android. Accede a navegación por Waze en un toque, pagos instantáneos con Bold y seguimiento en vivo de tu viaje.
|
||||
Instala la aplicación nativa en tu dispositivo Android. Accede a navegación por Waze en un toque, pagos instantáneos con QR Bre-B o en línea y seguimiento en vivo de tu viaje.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -793,20 +793,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<p style="color: var(--text-muted); font-size: 0.88rem; margin-bottom: 1.25rem;">
|
||||
Las notificaciones operativas se separan por grupos en WhatsApp: las de Pacific Trans GO se dirigen automáticamente al grupo <strong>Pacific Trans Notificaciones</strong> y las de Dripzone al grupo <strong>Dripzone Notificaciones</strong>.
|
||||
Las notificaciones operativas y de despacho de viajes de Pacific Trans GO se envían automáticamente al grupo oficial <strong>Pacific Trans Notificaciones</strong>.
|
||||
</p>
|
||||
|
||||
<div class="grid-2col" style="gap: 1.25rem; margin-bottom: 1rem;">
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label" for="cfg-group-pacifictrans">👥 Grupo WhatsApp: Pacific Trans GO</label>
|
||||
<input type="text" class="form-input" id="cfg-group-pacifictrans" value="Pacific Trans Notificaciones" placeholder="Pacific Trans Notificaciones" required>
|
||||
<span id="badge-pacific-jid" style="font-size: 0.75rem; color: #34d399; margin-top: 4px; display: block;">✓ Conectado a grupo oficial de despacho</span>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label" for="cfg-group-dripzone">👥 Grupo WhatsApp: Dripzone</label>
|
||||
<input type="text" class="form-input" id="cfg-group-dripzone" value="Dripzone Notificaciones" placeholder="Dripzone Notificaciones" required>
|
||||
<span id="badge-dripzone-jid" style="font-size: 0.75rem; color: #34d399; margin-top: 4px; display: block;">✓ Conectado a grupo oficial Dripzone</span>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 1rem;">
|
||||
<label class="form-label" for="cfg-group-pacifictrans">👥 Grupo WhatsApp Oficial: Pacific Trans GO</label>
|
||||
<input type="text" class="form-input" id="cfg-group-pacifictrans" value="Pacific Trans Notificaciones" placeholder="Pacific Trans Notificaciones" required>
|
||||
<span id="badge-pacific-jid" style="font-size: 0.75rem; color: #34d399; margin-top: 4px; display: block;">✓ Conectado a grupo oficial de despacho</span>
|
||||
</div>
|
||||
|
||||
<div class="grid-2col" style="gap: 1.25rem; margin-bottom: 0;">
|
||||
@ -844,7 +837,7 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="cfg-penalidad">Penalidad por Cancelación Tardía ($ COP)</label>
|
||||
<input type="number" class="form-input" id="cfg-penalidad" value="15000" min="0" step="1000" required>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro automático en Bold si el usuario cancela con viaje ya asignado/activo.</span>
|
||||
<span style="font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; display: block;">Cobro de penalidad si el usuario cancela con viaje ya asignado/activo.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@ -931,7 +924,7 @@
|
||||
<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>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">En Línea / Bre-B</span>
|
||||
</div>
|
||||
<div class="fin-metric-card" style="border-top: 3px solid #f59e0b;">
|
||||
<div class="fin-metric-label">Recaudos en Efectivo</div>
|
||||
@ -1617,8 +1610,8 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- MODAL 1: AUTENTICACIÓN DRIPZONE STYLE CON OTP WHATSAPP + EMAIL -->
|
||||
<div class="pwa-modal-overlay" id="modal-auth-dripzone">
|
||||
<!-- MODAL 1: AUTENTICACIÓN PACIFIC TRANS GO CON OTP WHATSAPP + EMAIL -->
|
||||
<div class="pwa-modal-overlay" id="modal-auth-pacifictrans">
|
||||
<div class="pwa-modal-card">
|
||||
<button type="button" class="pwa-modal-close" id="btn-close-auth-modal">✕</button>
|
||||
|
||||
|
||||
@ -265,7 +265,7 @@ function initTabs() {
|
||||
// Botones para abrir modal de login desde los escudos de seguridad
|
||||
const btnGateAdminLogin = document.getElementById('btn-gate-admin-login');
|
||||
const btnGateDriverLogin = document.getElementById('btn-gate-driver-login');
|
||||
const authModal = document.getElementById('modal-auth-dripzone');
|
||||
const authModal = document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone');
|
||||
[btnGateAdminLogin, btnGateDriverLogin].forEach(b => {
|
||||
if (b && authModal) {
|
||||
b.addEventListener('click', () => authModal.classList.add('active'));
|
||||
@ -408,10 +408,10 @@ function initTabs() {
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
3. AUTHENTICATION (DRIPZONE STYLE CON OTP WHATSAPP + EMAIL)
|
||||
3. AUTHENTICATION (PACIFIC TRANS GO CON OTP WHATSAPP + EMAIL)
|
||||
========================================================================== */
|
||||
function initAuthSystem() {
|
||||
const modal = document.getElementById('modal-auth-dripzone');
|
||||
const modal = document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone');
|
||||
const btnOpen = document.getElementById('btn-open-auth-modal');
|
||||
const btnClose = document.getElementById('btn-close-auth-modal');
|
||||
const btnLogout = document.getElementById('btn-logout');
|
||||
@ -1119,9 +1119,8 @@ function syncTariffsToUI() {
|
||||
const elPhone = document.getElementById('cfg-admin-phone');
|
||||
const elName = document.getElementById('cfg-admin-name');
|
||||
|
||||
// Canales de WhatsApp y separación de grupos por App
|
||||
// Canales de WhatsApp oficiales
|
||||
const elGroupPacific = document.getElementById('cfg-group-pacifictrans');
|
||||
const elGroupDripzone = document.getElementById('cfg-group-dripzone');
|
||||
const adminWaGroupBadge = document.getElementById('admin-wa-group-badge');
|
||||
|
||||
if (elPasajero) elPasajero.value = currentTariffs.tarifaPasajeroFija;
|
||||
@ -1138,9 +1137,7 @@ function syncTariffsToUI() {
|
||||
if (elName && currentTariffs.nombreSoporte) elName.value = currentTariffs.nombreSoporte;
|
||||
|
||||
const pacificGroup = currentTariffs.grupoWhatsAppPacificTrans || 'Pacific Trans Notificaciones';
|
||||
const dripGroup = currentTariffs.grupoWhatsAppDripzone || 'Dripzone Notificaciones';
|
||||
if (elGroupPacific) elGroupPacific.value = pacificGroup;
|
||||
if (elGroupDripzone) elGroupDripzone.value = dripGroup;
|
||||
if (adminWaGroupBadge) adminWaGroupBadge.textContent = `👥 Grupo: ${pacificGroup}`;
|
||||
|
||||
// 2. Sincronización del enlace dinámico de atención WhatsApp en el footer y navbar
|
||||
@ -1198,9 +1195,7 @@ function initTariffsManager() {
|
||||
|
||||
if (res.ok && data.success) {
|
||||
const inputPacific = document.getElementById('cfg-group-pacifictrans');
|
||||
const inputDrip = document.getElementById('cfg-group-dripzone');
|
||||
const badgePacific = document.getElementById('badge-pacific-jid');
|
||||
const badgeDrip = document.getElementById('badge-dripzone-jid');
|
||||
|
||||
if (inputPacific && data.pacificTrans?.groupName) {
|
||||
inputPacific.value = data.pacificTrans.groupName;
|
||||
@ -1210,15 +1205,7 @@ function initTariffsManager() {
|
||||
badgePacific.style.color = '#34d399';
|
||||
}
|
||||
|
||||
if (inputDrip && data.dripzone?.groupName) {
|
||||
inputDrip.value = data.dripzone.groupName;
|
||||
}
|
||||
if (badgeDrip && data.dripzone?.resolvedJid) {
|
||||
badgeDrip.innerHTML = `✓ Destino: <strong>${data.dripzone.resolvedJid}</strong> (${data.dripzone.isGroup ? 'Grupo de WhatsApp' : 'Número Directo'})`;
|
||||
badgeDrip.style.color = '#34d399';
|
||||
}
|
||||
|
||||
alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacific Trans GO ➔ ${data.pacificTrans?.groupName} (${data.pacificTrans?.resolvedJid})\n• Dripzone ➔ ${data.dripzone?.groupName} (${data.dripzone?.resolvedJid})`);
|
||||
alert(`✅ Detección exitosa en WhatsApp Evolution:\n\n• Pacific Trans GO ➔ ${data.pacificTrans?.groupName || 'Configurado'} (${data.pacificTrans?.resolvedJid || 'JID Asignado'})`);
|
||||
} else {
|
||||
alert('No se pudieron consultar los grupos: ' + (data.error || 'Verifica la sesión'));
|
||||
}
|
||||
@ -1257,8 +1244,7 @@ function initTariffsManager() {
|
||||
radioMaxPoolingMetros: parseFloat(document.getElementById('cfg-radio-pooling')?.value || '4000'),
|
||||
numeroAtencionWhatsApp: document.getElementById('cfg-admin-phone')?.value.trim() || '573167455877',
|
||||
nombreSoporte: document.getElementById('cfg-admin-name')?.value.trim() || 'Atención Pacific Trans GO',
|
||||
grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans Notificaciones',
|
||||
grupoWhatsAppDripzone: document.getElementById('cfg-group-dripzone')?.value.trim() || 'Dripzone Notificaciones'
|
||||
grupoWhatsAppPacificTrans: document.getElementById('cfg-group-pacifictrans')?.value.trim() || 'Pacific Trans Notificaciones'
|
||||
};
|
||||
|
||||
try {
|
||||
@ -1277,7 +1263,7 @@ function initTariffsManager() {
|
||||
currentTariffs = { ...currentTariffs, ...data.config };
|
||||
syncTariffsToUI();
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = `✅ <strong>¡Configuración guardada y sincronizada!</strong> Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Grupo Pacific: "${payload.grupoWhatsAppPacificTrans}" | Grupo Dripzone: "${payload.grupoWhatsAppDripzone}"`;
|
||||
statusEl.innerHTML = `✅ <strong>¡Configuración guardada y sincronizada!</strong> Pasajero: $${payload.tarifaPasajeroFija.toLocaleString('es-CO')} COP | Grupo Oficial: "${payload.grupoWhatsAppPacificTrans}"`;
|
||||
statusEl.style.color = '#10b981';
|
||||
}
|
||||
} else {
|
||||
@ -1316,7 +1302,7 @@ function initPassengerCalculator() {
|
||||
const paymentCards = document.querySelectorAll('.payment-method-card');
|
||||
|
||||
let selectedSeats = 1;
|
||||
let selectedPaymentMethod = 'BOLD';
|
||||
let selectedPaymentMethod = 'BRE_B';
|
||||
let currentSeatsFilled = 3;
|
||||
|
||||
// Cache para persistir los datos ingresados al alternar entre 1..4 cupos
|
||||
@ -1552,8 +1538,26 @@ function initPassengerCalculator() {
|
||||
summaryTotalPrice.textContent = formattedTotal;
|
||||
}
|
||||
|
||||
// Actualizar texto del badge informativo según el método de pago
|
||||
const gatewayBadgeText = document.getElementById('summary-gateway-badge-text');
|
||||
if (gatewayBadgeText) {
|
||||
if (selectedPaymentMethod === 'BRE_B') {
|
||||
gatewayBadgeText.innerHTML = '⚡ Transferencia directa por <strong>QR Bre-B</strong> (Inmediato 24/7 sin comisión)';
|
||||
} else if (selectedPaymentMethod === 'BOLD') {
|
||||
gatewayBadgeText.innerHTML = '💳 Pago seguro <strong>en línea</strong> (Tarjetas Débito / Crédito, PSE y Nequi)';
|
||||
} else if (selectedPaymentMethod === 'NEQUI') {
|
||||
gatewayBadgeText.innerHTML = '📱 Transferencia directa a <strong>Nequi</strong>';
|
||||
} else {
|
||||
gatewayBadgeText.innerHTML = '💵 Pago en <strong>efectivo</strong> directamente al conductor al abordar';
|
||||
}
|
||||
}
|
||||
|
||||
if (btnBookText) {
|
||||
const methodText = selectedPaymentMethod === 'BOLD' ? 'Pagar con Bold' : (selectedPaymentMethod === 'NEQUI' ? 'Pagar con Nequi' : 'Reservar');
|
||||
const methodText = selectedPaymentMethod === 'BRE_B'
|
||||
? 'Pagar con QR Bre-B'
|
||||
: (selectedPaymentMethod === 'BOLD'
|
||||
? 'Pagar en Línea'
|
||||
: (selectedPaymentMethod === 'NEQUI' ? 'Pagar con Nequi' : 'Reservar (Efectivo)'));
|
||||
btnBookText.textContent = `${methodText} ${selectedSeats} ${selectedSeats === 1 ? 'Cupo' : 'Cupos'} (${formattedTotal})`;
|
||||
}
|
||||
|
||||
@ -1665,7 +1669,7 @@ function initPassengerCalculator() {
|
||||
authInput.value = p1Phone;
|
||||
}
|
||||
alert('🛡️ Por seguridad de los conductores y de los demás pasajeros, debes verificar tu identidad con OTP antes de confirmar tu reserva.');
|
||||
document.getElementById('modal-auth-dripzone')?.classList.add('active');
|
||||
(document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone'))?.classList.add('active');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1746,20 +1750,41 @@ function initPassengerCalculator() {
|
||||
alertBreakdown.innerHTML = breakdownHtml;
|
||||
}
|
||||
|
||||
// Si es pago Bold, renderizar botón y QR
|
||||
// Renderizar información o enlace de pago según el método seleccionado
|
||||
if (alertBoldArea) {
|
||||
if (data.boldPayment && data.boldPayment.checkoutUrl) {
|
||||
if (payload.metodoPago === 'BRE_B') {
|
||||
alertBoldArea.style.display = 'block';
|
||||
const cleanWa = (currentTariffs.numeroAtencionWhatsApp || '573167455877').replace(/\D/g, '');
|
||||
alertBoldArea.innerHTML = `
|
||||
<div style="background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.4); border-radius: 12px; padding: 1.25rem; text-align: center;">
|
||||
<div style="font-weight: 700; color: #34d399; font-size: 1.05rem; margin-bottom: 0.5rem;">
|
||||
⚡ Transferencia Inmediata mediante QR Bre-B
|
||||
</div>
|
||||
<p style="font-size: 0.88rem; color: #cbd5e1; margin-bottom: 0.75rem;">
|
||||
Total a transferir: <strong>$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP</strong> por ${selectedSeats} cupo(s).
|
||||
</p>
|
||||
<div style="background: rgba(15, 23, 42, 0.75); border: 1px dashed rgba(52, 211, 153, 0.4); border-radius: 8px; padding: 0.75rem; max-width: 440px; margin: 0 auto 0.85rem auto; font-size: 0.85rem; color: #e2e8f0; text-align: left;">
|
||||
<div>🏛️ <strong>Llave Bre-B Oficial (Celular):</strong> <span style="color: #38bdf8; font-weight: 700;">+${cleanWa}</span></div>
|
||||
<div style="margin-top: 4px;">📲 <strong>Interoperabilidad:</strong> Puedes transferir desde Bancolombia, Nequi, Daviplata, Dale o cualquier entidad financiera sin costo.</div>
|
||||
<div style="margin-top: 4px; color: #94a3b8; font-size: 0.78rem;">Referencia de Reserva: #${data.tripId?.slice(-6) || 'PT'}</div>
|
||||
</div>
|
||||
<a href="https://wa.me/${cleanWa}?text=${encodeURIComponent(`Hola, confirmo transferencia vía Bre-B por $${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP para la reserva #${data.tripId?.slice(-6) || ''}`)}" target="_blank" class="btn-primary" style="display: inline-flex; align-items: center; justify-content: center; text-decoration: none; padding: 0.75rem 1.5rem; font-weight: 700; background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
|
||||
📲 Enviar Comprobante Bre-B por WhatsApp
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
} else if (data.boldPayment && data.boldPayment.checkoutUrl) {
|
||||
alertBoldArea.style.display = 'block';
|
||||
alertBoldArea.innerHTML = `
|
||||
<div style="background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.4); border-radius: 12px; padding: 1rem; text-align: center;">
|
||||
<div style="font-weight: 700; color: #34d399; font-size: 1rem; margin-bottom: 0.5rem;">
|
||||
💳 Enlace de Pago Bold Smart Checkout Generado
|
||||
💳 Enlace de Pago en Línea Seguro Generado
|
||||
</div>
|
||||
<p style="font-size: 0.85rem; color: #cbd5e1; margin-bottom: 0.75rem;">
|
||||
Total a pagar: <strong>$${(selectedSeats * (currentTariffs.tarifaPasajeroFija || 35000)).toLocaleString('es-CO')} COP</strong> por ${selectedSeats} cupo(s).
|
||||
</p>
|
||||
<a href="${data.boldPayment.checkoutUrl}" target="_blank" class="btn-primary" style="display: inline-flex; align-items: center; justify-content: center; text-decoration: none; padding: 0.75rem 1.5rem; font-weight: 700; background: linear-gradient(135deg, #10b981 0%, #059669 100%);">
|
||||
👉 Abrir Pasarela Bold (Pagar con QR Bre-B, Nequi o PSE)
|
||||
👉 Abrir Pago Seguro en Línea (Tarjetas, PSE, Nequi)
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
@ -1983,7 +2008,7 @@ function initFreightCalculator() {
|
||||
|
||||
btnBookFreight.disabled = true;
|
||||
const originalText = btnBookFreight.innerHTML;
|
||||
btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Bold...';
|
||||
btnBookFreight.innerHTML = '⏳ Generando Enlace de Pago Seguro...';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
@ -2006,7 +2031,7 @@ function initFreightCalculator() {
|
||||
if (res.ok && data.success) {
|
||||
if (data.boldPaymentLink) {
|
||||
window.open(data.boldPaymentLink, '_blank');
|
||||
alert(`✅ ¡Guía #${data.guiaSeguimiento || 'PT'} generada!\n\nTarifa Liquidada: $${(data.tarifaFinal || data.tarifaCalculada).toLocaleString('es-CO')} COP\n${data.descuentoCupon ? '🎟️ Descuento aplicado: -$' + data.descuentoCupon.descuento.toLocaleString('es-CO') + ' COP\n' : ''}\nSe ha abierto la pasarela Bold Smart Checkout para completar el pago.`);
|
||||
alert(`✅ ¡Guía #${data.guiaSeguimiento || 'PT'} generada!\n\nTarifa Liquidada: $${(data.tarifaFinal || data.tarifaCalculada).toLocaleString('es-CO')} COP\n${data.descuentoCupon ? '🎟️ Descuento aplicado: -$' + data.descuentoCupon.descuento.toLocaleString('es-CO') + ' COP\n' : ''}\nSe ha abierto la pasarela de pago seguro en línea para completar el pago.`);
|
||||
} else {
|
||||
alert(`✅ Guía generada correctamente: #${data.guiaSeguimiento}`);
|
||||
}
|
||||
@ -2543,7 +2568,7 @@ function initSettlementsAndBreBSystem() {
|
||||
let actionBtnHtml = '';
|
||||
|
||||
if (p.estadoPago === 'PAGADO_BOLD') {
|
||||
badgeHtml = '<span class="badge-payment badge-payment-online">💳 Pagado Bold</span>';
|
||||
badgeHtml = '<span class="badge-payment badge-payment-online">💳 Pagado en Línea</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') {
|
||||
@ -2611,7 +2636,7 @@ function initSettlementsAndBreBSystem() {
|
||||
<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>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">En Línea / Bre-B</span>
|
||||
</div>
|
||||
|
||||
<div class="fin-metric-card">
|
||||
@ -3718,7 +3743,7 @@ function initUserProfileManager() {
|
||||
|
||||
function openProfileModal() {
|
||||
if (!currentUser) {
|
||||
const authModal = document.getElementById('modal-auth-dripzone');
|
||||
const authModal = document.getElementById('modal-auth-pacifictrans') || document.getElementById('modal-auth-dripzone');
|
||||
if (authModal) authModal.classList.add('active');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,23 +1,34 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
// Desactivar bufferCommands para que las operaciones fallen de inmediato y usen el almacenamiento autónomo si MongoDB está offline
|
||||
mongoose.set('bufferCommands', false);
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/transporte_cali_buenaventura';
|
||||
|
||||
let isConnected = false;
|
||||
|
||||
export function isDatabaseConnected(): boolean {
|
||||
return mongoose.connection.readyState === 1;
|
||||
}
|
||||
|
||||
export async function connectDatabase(): Promise<typeof mongoose> {
|
||||
if (isConnected) {
|
||||
if (mongoose.connection.readyState === 1) {
|
||||
isConnected = true;
|
||||
return mongoose;
|
||||
}
|
||||
|
||||
try {
|
||||
const conn = await mongoose.connect(MONGODB_URI, {
|
||||
autoIndex: true, // Asegura que los índices 2dsphere se construyan automáticamente
|
||||
serverSelectionTimeoutMS: 2500, // Timeout rápido para no retrasar la API
|
||||
connectTimeoutMS: 4000,
|
||||
});
|
||||
|
||||
isConnected = true;
|
||||
console.log(`✅ [MongoDB Self-Hosted] Conexión establecida exitosamente a: ${conn.connection.host}`);
|
||||
return conn;
|
||||
} catch (error) {
|
||||
isConnected = false;
|
||||
console.error('❌ [MongoDB Error] Fallo al conectar a la base de datos geoespacial:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@ -339,10 +339,9 @@ const server = http.createServer(async (req, res) => {
|
||||
return sendJson(200, { success: true, kpis });
|
||||
}
|
||||
|
||||
// 12.1. Administrador: Consultar grupos de WhatsApp para separación de notificaciones por App
|
||||
// 12.1. Administrador: Consultar grupos de WhatsApp para despacho oficial de Pacific Trans GO
|
||||
if (pathname === '/api/admin/whatsapp/groups' && req.method === 'GET') {
|
||||
const pacificJid = await whatsAppNotificationService.getPacificTransNotificationTarget();
|
||||
const dripzoneJid = await whatsAppNotificationService.getDripzoneNotificationTarget();
|
||||
return sendJson(200, {
|
||||
success: true,
|
||||
pacificTrans: {
|
||||
@ -350,11 +349,6 @@ const server = http.createServer(async (req, res) => {
|
||||
resolvedJid: pacificJid,
|
||||
isGroup: pacificJid.endsWith('@g.us'),
|
||||
},
|
||||
dripzone: {
|
||||
groupName: 'Dripzone Notificaciones',
|
||||
resolvedJid: dripzoneJid,
|
||||
isGroup: dripzoneJid.endsWith('@g.us'),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -19,7 +19,6 @@ export interface ITarifasConfig extends Document {
|
||||
numeroAtencionWhatsApp: string; // Número de WhatsApp oficial para atención, soporte y mensajes
|
||||
nombreSoporte: string; // Nombre del canal de soporte oficial
|
||||
grupoWhatsAppPacificTrans: string; // Grupo oficial para notificaciones de Pacific Trans Go
|
||||
grupoWhatsAppDripzone: string; // Grupo oficial para notificaciones de Dripzone
|
||||
estaActiva: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
@ -88,11 +87,6 @@ const TarifasConfigSchema = new Schema<ITarifasConfig>(
|
||||
default: 'Pacific Trans Notificaciones',
|
||||
trim: true,
|
||||
},
|
||||
grupoWhatsAppDripzone: {
|
||||
type: String,
|
||||
default: 'Dripzone Notificaciones',
|
||||
trim: true,
|
||||
},
|
||||
estaActiva: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@ -100,6 +94,7 @@ const TarifasConfigSchema = new Schema<ITarifasConfig>(
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
bufferCommands: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ export interface IUser extends Document {
|
||||
ciudad?: string;
|
||||
fcmToken?: string;
|
||||
|
||||
// Gestión de seguridad y autenticación avanzada (estilo DripZone)
|
||||
// Gestión de seguridad y autenticación avanzada independiente de Pacific Trans GO
|
||||
isActive: boolean;
|
||||
phoneVerified: boolean;
|
||||
twoFactorEnabled?: boolean;
|
||||
@ -163,6 +163,7 @@ const UserSchema = new Schema<IUser>(
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
bufferCommands: false,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -2,6 +2,8 @@ import crypto from 'crypto';
|
||||
import { User, IUser, UserRole } from '../models/User';
|
||||
import { whatsAppNotificationService } from './whatsapp-notification.service';
|
||||
import { isPlatformSuperAdmin, SUPER_ADMIN_PHONE, SUPER_ADMIN_EMAIL } from './user-admin.service';
|
||||
import { isDatabaseConnected } from '../config/database';
|
||||
import { localUserStore } from './local-user-store';
|
||||
|
||||
interface OtpRecord {
|
||||
code: string;
|
||||
@ -98,6 +100,7 @@ export class AuthService {
|
||||
email?: string;
|
||||
direccionTexto?: string;
|
||||
documentoIdentidad?: string;
|
||||
ciudad?: string;
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
token?: string;
|
||||
@ -133,98 +136,130 @@ export class AuthService {
|
||||
|
||||
const cleanPhone = record.phone.replace(/\D/g, '');
|
||||
const userEmail = params.email || record.email;
|
||||
const isSuper = isPlatformSuperAdmin(cleanPhone) || (userEmail && isPlatformSuperAdmin(userEmail));
|
||||
const assignedRole: UserRole = isSuper ? 'ADMIN' : 'PASAJERO';
|
||||
|
||||
// Buscar o registrar usuario en MongoDB
|
||||
let user = await User.findOne({
|
||||
$or: [
|
||||
{ telefono: cleanPhone },
|
||||
...(userEmail ? [{ email: userEmail.toLowerCase().trim() }] : []),
|
||||
],
|
||||
let user: any = null;
|
||||
|
||||
// 1. Intentar consultar o crear en MongoDB solo si está conectado, con timeout rápido
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('DB_TIMEOUT')), 2500)
|
||||
);
|
||||
const dbQuery = User.findOne({
|
||||
$or: [
|
||||
{ telefono: cleanPhone },
|
||||
...(userEmail ? [{ email: userEmail.toLowerCase().trim() }] : []),
|
||||
],
|
||||
});
|
||||
user = (await Promise.race([dbQuery, timeoutPromise])) as any;
|
||||
|
||||
if (!user) {
|
||||
const createPromise = User.create({
|
||||
appwriteId: isSuper ? 'usr-superadmin-platform' : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
|
||||
nombre: isSuper ? (params.nombre || 'Superadministrador Principal') : (params.nombre || `Usuario ${cleanPhone.slice(-4)}`),
|
||||
telefono: cleanPhone,
|
||||
email: userEmail || (isSuper ? SUPER_ADMIN_EMAIL : undefined),
|
||||
documentoIdentidad: params.documentoIdentidad || (isSuper ? '1000000001' : undefined),
|
||||
direccionTexto: params.direccionTexto || 'Cali, Valle del Cauca',
|
||||
ciudad: params.ciudad || 'Cali',
|
||||
rol: assignedRole,
|
||||
esSuperAdmin: isSuper,
|
||||
saldoPenalidad: 0,
|
||||
coordenadasDomicilio: {
|
||||
type: 'Point',
|
||||
coordinates: [-76.532, 3.4516],
|
||||
},
|
||||
isActive: true,
|
||||
phoneVerified: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'whatsapp_otp',
|
||||
lastLogin: new Date(),
|
||||
});
|
||||
user = (await Promise.race([createPromise, timeoutPromise])) as any;
|
||||
console.log(`👤 [Nuevo Usuario Verificado]: ${user.nombre} (+${user.telefono}) - Rol: ${user.rol} ${isSuper ? '(SUPERADMIN)' : ''}`);
|
||||
} else {
|
||||
user.phoneVerified = true;
|
||||
user.isActive = true;
|
||||
user.lastLogin = new Date();
|
||||
if (isSuper) {
|
||||
user.rol = 'ADMIN';
|
||||
user.esSuperAdmin = true;
|
||||
if (!user.nombre || user.nombre.startsWith('Usuario ')) {
|
||||
user.nombre = 'Superadministrador Principal';
|
||||
}
|
||||
}
|
||||
if (params.documentoIdentidad && !user.documentoIdentidad) {
|
||||
user.documentoIdentidad = params.documentoIdentidad;
|
||||
}
|
||||
if (params.nombre && user.nombre.startsWith('Usuario ')) {
|
||||
user.nombre = params.nombre;
|
||||
}
|
||||
if (userEmail && !user.email) {
|
||||
user.email = userEmail;
|
||||
}
|
||||
await Promise.race([user.save(), timeoutPromise]);
|
||||
}
|
||||
} catch (errDb) {
|
||||
console.warn('⚠️ [Auth DB Notice]: MongoDB no disponible o tiempo agotado. Continuando con almacenamiento autónomo:', errDb);
|
||||
user = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Almacenamiento local autónomo siempre sincronizado (Garantiza 100% de disponibilidad sin depender de DB externa)
|
||||
const localUser = localUserStore.upsert({
|
||||
id: user?._id?.toString(),
|
||||
nombre: params.nombre || user?.nombre,
|
||||
telefono: cleanPhone,
|
||||
email: userEmail || user?.email,
|
||||
documentoIdentidad: params.documentoIdentidad || user?.documentoIdentidad,
|
||||
direccionTexto: params.direccionTexto || user?.direccionTexto,
|
||||
ciudad: params.ciudad || user?.ciudad,
|
||||
rol: user?.rol || assignedRole,
|
||||
esSuperAdmin: isSuper || !!user?.esSuperAdmin,
|
||||
isActive: true,
|
||||
lastLogin: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const isSuper = isPlatformSuperAdmin(cleanPhone) || (userEmail && isPlatformSuperAdmin(userEmail));
|
||||
const assignedRole: UserRole = isSuper ? 'ADMIN' : (user?.rol || 'PASAJERO');
|
||||
|
||||
if (!user) {
|
||||
user = await User.create({
|
||||
appwriteId: isSuper ? 'usr-superadmin-platform' : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`,
|
||||
nombre: isSuper ? (params.nombre || 'Superadministrador Principal') : (params.nombre || `Usuario ${cleanPhone.slice(-4)}`),
|
||||
telefono: cleanPhone,
|
||||
email: userEmail || (isSuper ? SUPER_ADMIN_EMAIL : undefined),
|
||||
documentoIdentidad: params.documentoIdentidad || (isSuper ? '1000000001' : undefined),
|
||||
direccionTexto: params.direccionTexto || 'Cali, Valle del Cauca',
|
||||
ciudad: 'Cali',
|
||||
rol: assignedRole,
|
||||
esSuperAdmin: isSuper,
|
||||
saldoPenalidad: 0,
|
||||
coordenadasDomicilio: {
|
||||
type: 'Point',
|
||||
coordinates: [-76.532, 3.4516],
|
||||
},
|
||||
isActive: true,
|
||||
phoneVerified: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'whatsapp_otp',
|
||||
lastLogin: new Date(),
|
||||
});
|
||||
console.log(`👤 [Nuevo Usuario Verificado]: ${user.nombre} (+${user.telefono}) - Rol: ${user.rol} ${isSuper ? '(SUPERADMIN)' : ''}`);
|
||||
} else {
|
||||
user.phoneVerified = true;
|
||||
user.isActive = true;
|
||||
user.lastLogin = new Date();
|
||||
if (isSuper) {
|
||||
user.rol = 'ADMIN';
|
||||
user.esSuperAdmin = true;
|
||||
if (!user.nombre || user.nombre.startsWith('Usuario ')) {
|
||||
user.nombre = 'Superadministrador Principal';
|
||||
}
|
||||
}
|
||||
if (params.documentoIdentidad && !user.documentoIdentidad) {
|
||||
user.documentoIdentidad = params.documentoIdentidad;
|
||||
}
|
||||
if (params.nombre && user.nombre.startsWith('Usuario ')) {
|
||||
user.nombre = params.nombre;
|
||||
}
|
||||
if (userEmail && !user.email) {
|
||||
user.email = userEmail;
|
||||
}
|
||||
await user.save();
|
||||
}
|
||||
const finalUserId = (user?._id || localUser.id).toString();
|
||||
const finalRole: UserRole = user?.rol || localUser.rol;
|
||||
|
||||
// Generar Token de Sesión seguro firmado con expiración exacta de 24 horas
|
||||
const expiresAt = Date.now() + SESSION_DURATION_MS;
|
||||
const tokenPayload = {
|
||||
uid: user._id.toString(),
|
||||
uid: finalUserId,
|
||||
phone: cleanPhone,
|
||||
rol: user.rol,
|
||||
rol: finalRole,
|
||||
exp: expiresAt,
|
||||
};
|
||||
const b64 = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url');
|
||||
const sig = crypto.createHmac('sha256', JWT_SECRET).update(b64).digest('base64url');
|
||||
const token = `pt_${b64}.${sig}`;
|
||||
|
||||
SESSION_TOKENS.set(token, {
|
||||
userId: user._id.toString(),
|
||||
rol: user.rol,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
const userPayload = {
|
||||
id: user._id.toString(),
|
||||
_id: user._id.toString(),
|
||||
nombre: user.nombre,
|
||||
telefono: user.telefono,
|
||||
email: user.email,
|
||||
documentoIdentidad: user.documentoIdentidad,
|
||||
direccionTexto: user.direccionTexto,
|
||||
ciudad: user.ciudad,
|
||||
rol: user.rol,
|
||||
esSuperAdmin: isSuper || !!user.esSuperAdmin,
|
||||
phoneVerified: user.phoneVerified,
|
||||
saldoPenalidad: user.saldoPenalidad,
|
||||
id: finalUserId,
|
||||
_id: finalUserId,
|
||||
nombre: user?.nombre || localUser.nombre,
|
||||
telefono: cleanPhone,
|
||||
email: user?.email || localUser.email,
|
||||
documentoIdentidad: user?.documentoIdentidad || localUser.documentoIdentidad,
|
||||
direccionTexto: user?.direccionTexto || localUser.direccionTexto,
|
||||
ciudad: user?.ciudad || localUser.ciudad,
|
||||
rol: finalRole,
|
||||
esSuperAdmin: isSuper || !!user?.esSuperAdmin || !!localUser.esSuperAdmin,
|
||||
phoneVerified: true,
|
||||
saldoPenalidad: user?.saldoPenalidad || localUser.saldoPenalidad || 0,
|
||||
tokenExpiresAt: expiresAt,
|
||||
};
|
||||
|
||||
SESSION_TOKENS.set(token, {
|
||||
userId: finalUserId,
|
||||
rol: finalRole,
|
||||
expiresAt,
|
||||
cachedUser: userPayload,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token,
|
||||
@ -274,29 +309,62 @@ export class AuthService {
|
||||
expiresAt = session.expiresAt;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await User.findById(userId);
|
||||
if (user) {
|
||||
const isSuper = isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email) || !!user.esSuperAdmin;
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
_id: user._id.toString(),
|
||||
nombre: user.nombre,
|
||||
telefono: user.telefono,
|
||||
email: user.email,
|
||||
documentoIdentidad: user.documentoIdentidad,
|
||||
direccionTexto: user.direccionTexto,
|
||||
ciudad: user.ciudad,
|
||||
rol: isSuper ? 'ADMIN' : user.rol,
|
||||
esSuperAdmin: isSuper,
|
||||
phoneVerified: user.phoneVerified,
|
||||
saldoPenalidad: user.saldoPenalidad,
|
||||
isActive: user.isActive,
|
||||
tokenExpiresAt: expiresAt,
|
||||
};
|
||||
// 3. Revisar usuario en caché de sesión en memoria
|
||||
const cachedSession = SESSION_TOKENS.get(token);
|
||||
if (cachedSession?.cachedUser) {
|
||||
return cachedSession.cachedUser;
|
||||
}
|
||||
|
||||
// 4. Consultar en MongoDB si la base de datos está disponible
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('DB_TIMEOUT')), 2000)
|
||||
);
|
||||
const user = (await Promise.race([User.findById(userId), timeoutPromise])) as any;
|
||||
if (user) {
|
||||
const isSuper = isPlatformSuperAdmin(user.telefono) || isPlatformSuperAdmin(user.email) || !!user.esSuperAdmin;
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
_id: user._id.toString(),
|
||||
nombre: user.nombre,
|
||||
telefono: user.telefono,
|
||||
email: user.email,
|
||||
documentoIdentidad: user.documentoIdentidad,
|
||||
direccionTexto: user.direccionTexto,
|
||||
ciudad: user.ciudad,
|
||||
rol: isSuper ? 'ADMIN' : user.rol,
|
||||
esSuperAdmin: isSuper,
|
||||
phoneVerified: user.phoneVerified,
|
||||
saldoPenalidad: user.saldoPenalidad,
|
||||
isActive: user.isActive,
|
||||
tokenExpiresAt: expiresAt,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback al almacén local
|
||||
}
|
||||
} catch (e) {
|
||||
if (SESSION_TOKENS.get(token)?.cachedUser) return SESSION_TOKENS.get(token)?.cachedUser;
|
||||
}
|
||||
|
||||
// 5. Fallback al almacén local autónomo
|
||||
const local = localUserStore.findById(userId) || (userId.length === 10 || userId.length === 12 ? localUserStore.findByPhone(userId) : null);
|
||||
if (local) {
|
||||
return {
|
||||
id: local.id,
|
||||
_id: local.id,
|
||||
nombre: local.nombre,
|
||||
telefono: local.telefono,
|
||||
email: local.email,
|
||||
documentoIdentidad: local.documentoIdentidad,
|
||||
direccionTexto: local.direccionTexto,
|
||||
ciudad: local.ciudad,
|
||||
rol: local.rol,
|
||||
esSuperAdmin: local.esSuperAdmin,
|
||||
phoneVerified: local.phoneVerified,
|
||||
saldoPenalidad: local.saldoPenalidad,
|
||||
isActive: local.isActive,
|
||||
tokenExpiresAt: expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@ -0,0 +1,219 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { UserRole } from '../models/User';
|
||||
|
||||
export const SUPER_ADMIN_PHONE = '573167455877';
|
||||
export const SUPER_ADMIN_LOCAL_PHONE = '3167455877';
|
||||
export const SUPER_ADMIN_EMAIL = 'admin@pacifictrans.almeira.com.co';
|
||||
|
||||
export function isPlatformSuperAdmin(identifier?: string): boolean {
|
||||
if (!identifier) return false;
|
||||
const clean = identifier.replace(/\D/g, '');
|
||||
return (
|
||||
clean === SUPER_ADMIN_PHONE ||
|
||||
clean === SUPER_ADMIN_LOCAL_PHONE ||
|
||||
identifier.toLowerCase().trim() === SUPER_ADMIN_EMAIL
|
||||
);
|
||||
}
|
||||
|
||||
export interface LocalUserRecord {
|
||||
id: string;
|
||||
_id: string;
|
||||
appwriteId: string;
|
||||
nombre: string;
|
||||
telefono: string;
|
||||
email?: string;
|
||||
documentoIdentidad?: string;
|
||||
direccionTexto?: string;
|
||||
ciudad?: string;
|
||||
rol: UserRole;
|
||||
esSuperAdmin: boolean;
|
||||
phoneVerified: boolean;
|
||||
saldoPenalidad: number;
|
||||
isActive: boolean;
|
||||
twoFactorEnabled?: boolean;
|
||||
twoFactorMethod?: 'whatsapp_otp' | 'biometric' | 'both';
|
||||
totalViajesCompletados?: number;
|
||||
totalViajesCancelados?: number;
|
||||
lastLogin?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const DATA_DIR = path.resolve(process.cwd(), 'data');
|
||||
const STORE_PATH = path.join(DATA_DIR, 'pacifictrans_users.json');
|
||||
|
||||
class LocalUserStore {
|
||||
private static instance: LocalUserStore;
|
||||
private usersMap = new Map<string, LocalUserRecord>();
|
||||
private initialized = false;
|
||||
|
||||
private constructor() {
|
||||
this.ensureInitialized();
|
||||
}
|
||||
|
||||
public static getInstance(): LocalUserStore {
|
||||
if (!LocalUserStore.instance) {
|
||||
LocalUserStore.instance = new LocalUserStore();
|
||||
}
|
||||
return LocalUserStore.instance;
|
||||
}
|
||||
|
||||
private ensureInitialized() {
|
||||
if (this.initialized) return;
|
||||
this.initialized = true;
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
if (fs.existsSync(STORE_PATH)) {
|
||||
const raw = fs.readFileSync(STORE_PATH, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (Array.isArray(parsed)) {
|
||||
parsed.forEach((u: LocalUserRecord) => {
|
||||
if (u && u.id) {
|
||||
this.usersMap.set(u.id, u);
|
||||
if (u.telefono) {
|
||||
const clean = u.telefono.replace(/\D/g, '');
|
||||
this.usersMap.set(`phone:${clean}`, u);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('⚠️ [LocalUserStore] No se pudo leer almacén local, iniciando fresco:', err);
|
||||
}
|
||||
|
||||
// Asegurar siempre la existencia del Superadministrador Principal
|
||||
this.ensureSuperAdmin();
|
||||
}
|
||||
|
||||
private persist() {
|
||||
try {
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
}
|
||||
const uniqueUsers = Array.from(new Set(this.usersMap.values()));
|
||||
fs.writeFileSync(STORE_PATH, JSON.stringify(uniqueUsers, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
console.warn('⚠️ [LocalUserStore] Error al persistir usuarios locales:', err);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureSuperAdmin() {
|
||||
const cleanSuper = SUPER_ADMIN_PHONE.replace(/\D/g, '');
|
||||
let existing = this.findByPhone(cleanSuper);
|
||||
|
||||
if (!existing) {
|
||||
const superAdminUser: LocalUserRecord = {
|
||||
id: 'usr-superadmin-platform',
|
||||
_id: 'usr-superadmin-platform',
|
||||
appwriteId: 'usr-superadmin-platform',
|
||||
nombre: 'Superadministrador Principal',
|
||||
telefono: cleanSuper,
|
||||
email: SUPER_ADMIN_EMAIL,
|
||||
documentoIdentidad: '1000000001',
|
||||
direccionTexto: 'Cali, Valle del Cauca',
|
||||
ciudad: 'Cali',
|
||||
rol: 'ADMIN',
|
||||
esSuperAdmin: true,
|
||||
phoneVerified: true,
|
||||
saldoPenalidad: 0,
|
||||
isActive: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'whatsapp_otp',
|
||||
totalViajesCompletados: 0,
|
||||
totalViajesCancelados: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
this.upsert(superAdminUser);
|
||||
}
|
||||
}
|
||||
|
||||
public findByPhone(phone: string): LocalUserRecord | null {
|
||||
this.ensureInitialized();
|
||||
const clean = phone.replace(/\D/g, '');
|
||||
return this.usersMap.get(`phone:${clean}`) || null;
|
||||
}
|
||||
|
||||
public findById(id: string): LocalUserRecord | null {
|
||||
this.ensureInitialized();
|
||||
return this.usersMap.get(id) || null;
|
||||
}
|
||||
|
||||
public findByEmail(email: string): LocalUserRecord | null {
|
||||
this.ensureInitialized();
|
||||
const cleanEmail = email.toLowerCase().trim();
|
||||
for (const u of this.usersMap.values()) {
|
||||
if (u.email && u.email.toLowerCase().trim() === cleanEmail) {
|
||||
return u;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public upsert(user: Partial<LocalUserRecord> & { telefono: string }): LocalUserRecord {
|
||||
this.ensureInitialized();
|
||||
const cleanPhone = user.telefono.replace(/\D/g, '');
|
||||
const isSuper = isPlatformSuperAdmin(cleanPhone) || (user.email && isPlatformSuperAdmin(user.email));
|
||||
|
||||
const existing = this.findByPhone(cleanPhone) || (user.id ? this.findById(user.id) : null);
|
||||
const userId = existing?.id || user.id || (isSuper ? 'usr-superadmin-platform' : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`);
|
||||
|
||||
const nowIso = new Date().toISOString();
|
||||
const record: LocalUserRecord = {
|
||||
id: userId,
|
||||
_id: userId,
|
||||
appwriteId: existing?.appwriteId || user.appwriteId || userId,
|
||||
nombre: isSuper ? 'Superadministrador Principal' : (user.nombre || existing?.nombre || `Usuario ${cleanPhone.slice(-4)}`),
|
||||
telefono: cleanPhone,
|
||||
email: user.email || existing?.email || (isSuper ? SUPER_ADMIN_EMAIL : undefined),
|
||||
documentoIdentidad: user.documentoIdentidad || existing?.documentoIdentidad || (isSuper ? '1000000001' : undefined),
|
||||
direccionTexto: user.direccionTexto || existing?.direccionTexto || 'Cali, Valle del Cauca',
|
||||
ciudad: user.ciudad || existing?.ciudad || 'Cali',
|
||||
rol: isSuper ? 'ADMIN' : (user.rol || existing?.rol || 'PASAJERO'),
|
||||
esSuperAdmin: isSuper || !!existing?.esSuperAdmin,
|
||||
phoneVerified: true,
|
||||
saldoPenalidad: existing?.saldoPenalidad || user.saldoPenalidad || 0,
|
||||
isActive: user.isActive !== undefined ? user.isActive : (existing?.isActive !== undefined ? existing.isActive : true),
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'whatsapp_otp',
|
||||
totalViajesCompletados: existing?.totalViajesCompletados || 0,
|
||||
totalViajesCancelados: existing?.totalViajesCancelados || 0,
|
||||
lastLogin: user.lastLogin || nowIso,
|
||||
createdAt: existing?.createdAt || nowIso,
|
||||
updatedAt: nowIso,
|
||||
};
|
||||
|
||||
this.usersMap.set(userId, record);
|
||||
this.usersMap.set(`phone:${cleanPhone}`, record);
|
||||
this.persist();
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
public getAll(): LocalUserRecord[] {
|
||||
this.ensureInitialized();
|
||||
const unique = Array.from(new Set(this.usersMap.values()));
|
||||
return unique.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
}
|
||||
|
||||
public delete(id: string): boolean {
|
||||
this.ensureInitialized();
|
||||
const u = this.findById(id);
|
||||
if (!u) return false;
|
||||
if (isPlatformSuperAdmin(u.telefono) || u.esSuperAdmin) {
|
||||
throw new Error('Operación denegada: El Superadministrador Principal no puede ser eliminado.');
|
||||
}
|
||||
this.usersMap.delete(id);
|
||||
this.usersMap.delete(`phone:${u.telefono}`);
|
||||
this.persist();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const localUserStore = LocalUserStore.getInstance();
|
||||
@ -281,7 +281,7 @@ export class MessagingFleteService {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Servicio de mensajería cotizado y registrado. Pago en línea 100% generado en Bold.',
|
||||
message: 'Servicio de mensajería cotizado y registrado. Pago en línea seguro generado con éxito.',
|
||||
tripId: trip._id.toString(),
|
||||
desglose,
|
||||
boldPayment: boldRes,
|
||||
|
||||
@ -53,7 +53,7 @@ export class StatsReviewsService {
|
||||
destinatarioNombre: 'Hernán Darío P.',
|
||||
tipo: 'PASAJERO_A_CONDUCTOR',
|
||||
puntuacion: 5,
|
||||
comentario: 'La mejor opción para viajar a Buenaventura. Poder pagar con Nequi y Bold con antelación da mucha tranquilidad. El conductor conocía perfecto el estado de la vía.',
|
||||
comentario: 'La mejor opción para viajar a Buenaventura. Poder pagar con QR Bre-B o Nequi con antelación da mucha tranquilidad. El conductor conocía perfecto el estado de la vía.',
|
||||
etiquetas: ['Pago Rápido 💳', 'Amabilidad 🤝', 'Puntualidad ⏱️'],
|
||||
ruta: 'Cali ➔ Buenaventura',
|
||||
esPublico: true,
|
||||
|
||||
@ -17,7 +17,6 @@ export interface UpdateTarifasDTO {
|
||||
numeroAtencionWhatsApp?: string;
|
||||
nombreSoporte?: string;
|
||||
grupoWhatsAppPacificTrans?: string;
|
||||
grupoWhatsAppDripzone?: string;
|
||||
}
|
||||
|
||||
export class TariffsService {
|
||||
@ -124,10 +123,6 @@ export class TariffsService {
|
||||
config.grupoWhatsAppPacificTrans = updates.grupoWhatsAppPacificTrans.trim();
|
||||
}
|
||||
|
||||
if (updates.grupoWhatsAppDripzone !== undefined && updates.grupoWhatsAppDripzone.trim()) {
|
||||
config.grupoWhatsAppDripzone = updates.grupoWhatsAppDripzone.trim();
|
||||
}
|
||||
|
||||
config.updatedAt = new Date();
|
||||
await config.save();
|
||||
|
||||
|
||||
@ -102,7 +102,9 @@ export class TripSchedulingService {
|
||||
direccionRecogida: p.direccionRecogida,
|
||||
estadoPago:
|
||||
p.estadoPago === 'PAGADO_BOLD'
|
||||
? 'Pagado vía Bold'
|
||||
? 'Pagado en línea'
|
||||
: p.estadoPago === 'PAGADO_BRE_B'
|
||||
? 'Pagado vía QR Bre-B'
|
||||
: p.estadoPago === 'PAGADO_NEQUI'
|
||||
? 'Pagado vía Nequi'
|
||||
: 'Pago pendiente en efectivo',
|
||||
@ -149,7 +151,7 @@ export class TripSchedulingService {
|
||||
`• *Vehículo:* ${driverProfile.modeloVehiculo} (${driverProfile.colorVehiculo || 'Blanco'})\n` +
|
||||
`• *Placa:* ${driverProfile.placaVehiculo}\n` +
|
||||
`• *Teléfono:* +${conductor.telefono.replace(/\D/g, '')}\n\n` +
|
||||
`💳 *Estado de Pago:* ${p.estadoPago === 'PAGADO_BOLD' ? 'Pagado vía Bold' : 'Pendiente en efectivo'}\n\n` +
|
||||
`💳 *Estado de Pago:* ${p.estadoPago === 'PAGADO_BOLD' ? 'Pagado en línea' : (p.estadoPago === 'PAGADO_BRE_B' ? 'Pagado vía QR Bre-B' : 'Pendiente en efectivo')}\n\n` +
|
||||
`_Por favor permanece listo 5 minutos antes de tu hora de recogida._`;
|
||||
|
||||
await whatsAppNotificationService.sendTextMessage(p.telefono, msgPasajero);
|
||||
|
||||
@ -2,20 +2,21 @@ import { Types } from 'mongoose';
|
||||
import { User, IUser, UserRole } from '../models/User';
|
||||
import { DriverProfile, IDriverProfile } from '../models/DriverProfile';
|
||||
import { boldPaymentService } from './bold.service';
|
||||
import { isDatabaseConnected } from '../config/database';
|
||||
import {
|
||||
localUserStore,
|
||||
SUPER_ADMIN_PHONE,
|
||||
SUPER_ADMIN_LOCAL_PHONE,
|
||||
SUPER_ADMIN_EMAIL,
|
||||
isPlatformSuperAdmin,
|
||||
} from './local-user-store';
|
||||
|
||||
export const SUPER_ADMIN_PHONE = '573167455877';
|
||||
export const SUPER_ADMIN_LOCAL_PHONE = '3167455877';
|
||||
export const SUPER_ADMIN_EMAIL = 'admin@pacifictrans.almeira.com.co';
|
||||
|
||||
export function isPlatformSuperAdmin(identifier?: string): boolean {
|
||||
if (!identifier) return false;
|
||||
const clean = identifier.replace(/\D/g, '');
|
||||
return (
|
||||
clean === SUPER_ADMIN_PHONE ||
|
||||
clean === SUPER_ADMIN_LOCAL_PHONE ||
|
||||
identifier.toLowerCase().trim() === SUPER_ADMIN_EMAIL
|
||||
);
|
||||
}
|
||||
export {
|
||||
SUPER_ADMIN_PHONE,
|
||||
SUPER_ADMIN_LOCAL_PHONE,
|
||||
SUPER_ADMIN_EMAIL,
|
||||
isPlatformSuperAdmin,
|
||||
};
|
||||
|
||||
export interface UserSummaryDTO {
|
||||
id: string;
|
||||
@ -84,43 +85,76 @@ export class UserAdminService {
|
||||
* Obtiene la lista completa de usuarios con estado, roles y penalidades
|
||||
*/
|
||||
public async getAllUsers(): Promise<UserSummaryDTO[]> {
|
||||
const users = await User.find().sort({ createdAt: -1 }).lean();
|
||||
const result: UserSummaryDTO[] = [];
|
||||
|
||||
for (const u of users) {
|
||||
let driverProfile: any = null;
|
||||
if (u.rol === 'CONDUCTOR') {
|
||||
driverProfile = await DriverProfile.findOne({ userId: u._id }).lean();
|
||||
}
|
||||
|
||||
const isSuper = isPlatformSuperAdmin(u.telefono) || isPlatformSuperAdmin(u.email) || !!u.esSuperAdmin;
|
||||
|
||||
result.push({
|
||||
id: u._id.toString(),
|
||||
if (!isDatabaseConnected()) {
|
||||
return localUserStore.getAll().map((u) => ({
|
||||
id: u.id,
|
||||
appwriteId: u.appwriteId,
|
||||
nombre: u.nombre,
|
||||
telefono: u.telefono,
|
||||
email: u.email,
|
||||
documentoIdentidad: u.documentoIdentidad,
|
||||
esSuperAdmin: isSuper,
|
||||
rol: isSuper ? 'ADMIN' : u.rol,
|
||||
isActive: isSuper ? true : u.isActive,
|
||||
phoneVerified: isSuper ? true : u.phoneVerified,
|
||||
saldoPenalidad: isSuper ? 0 : u.saldoPenalidad,
|
||||
bloqueadoPorPenalidad: isSuper ? false : (u.saldoPenalidad || 0) > 0,
|
||||
esSuperAdmin: u.esSuperAdmin,
|
||||
rol: u.rol,
|
||||
isActive: u.isActive,
|
||||
phoneVerified: u.phoneVerified,
|
||||
saldoPenalidad: u.saldoPenalidad,
|
||||
bloqueadoPorPenalidad: u.saldoPenalidad > 0,
|
||||
totalViajesCompletados: u.totalViajesCompletados || 0,
|
||||
totalViajesCancelados: u.totalViajesCancelados || 0,
|
||||
twoFactorEnabled: u.twoFactorEnabled,
|
||||
twoFactorMethod: u.twoFactorMethod,
|
||||
biometricEnabled: u.biometricEnabled,
|
||||
biometricEnabled: false,
|
||||
direccionTexto: u.direccionTexto,
|
||||
ciudad: u.ciudad,
|
||||
driverProfile,
|
||||
createdAt: u.createdAt,
|
||||
});
|
||||
driverProfile: null,
|
||||
createdAt: new Date(u.createdAt),
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
try {
|
||||
const users = await User.find().sort({ createdAt: -1 }).lean();
|
||||
const result: UserSummaryDTO[] = [];
|
||||
|
||||
for (const u of users) {
|
||||
let driverProfile: any = null;
|
||||
if (u.rol === 'CONDUCTOR') {
|
||||
try {
|
||||
driverProfile = await DriverProfile.findOne({ userId: u._id }).lean();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const isSuper = isPlatformSuperAdmin(u.telefono) || isPlatformSuperAdmin(u.email) || !!u.esSuperAdmin;
|
||||
|
||||
result.push({
|
||||
id: u._id.toString(),
|
||||
appwriteId: u.appwriteId,
|
||||
nombre: u.nombre,
|
||||
telefono: u.telefono,
|
||||
email: u.email,
|
||||
documentoIdentidad: u.documentoIdentidad,
|
||||
esSuperAdmin: isSuper,
|
||||
rol: isSuper ? 'ADMIN' : u.rol,
|
||||
isActive: isSuper ? true : u.isActive,
|
||||
phoneVerified: isSuper ? true : u.phoneVerified,
|
||||
saldoPenalidad: isSuper ? 0 : u.saldoPenalidad,
|
||||
bloqueadoPorPenalidad: isSuper ? false : (u.saldoPenalidad || 0) > 0,
|
||||
totalViajesCompletados: u.totalViajesCompletados || 0,
|
||||
totalViajesCancelados: u.totalViajesCancelados || 0,
|
||||
twoFactorEnabled: u.twoFactorEnabled,
|
||||
twoFactorMethod: u.twoFactorMethod,
|
||||
biometricEnabled: u.biometricEnabled,
|
||||
direccionTexto: u.direccionTexto,
|
||||
ciudad: u.ciudad,
|
||||
driverProfile,
|
||||
createdAt: u.createdAt,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.warn('⚠️ [getAllUsers Error]:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -128,56 +162,76 @@ export class UserAdminService {
|
||||
* El Superadministrador siempre aparece en primera posición
|
||||
*/
|
||||
public async getAdministrators(): Promise<UserSummaryDTO[]> {
|
||||
// Asegurar que el Superadministrador esté registrado
|
||||
await this.ensureSuperAdminExists();
|
||||
if (!isDatabaseConnected()) {
|
||||
const all = await this.getAllUsers();
|
||||
return all
|
||||
.filter((u) => u.rol === 'ADMIN' || u.rol === 'OPERADOR' || u.esSuperAdmin)
|
||||
.sort((a, b) => (b.esSuperAdmin ? 1 : 0) - (a.esSuperAdmin ? 1 : 0));
|
||||
}
|
||||
|
||||
const admins = await User.find({
|
||||
$or: [
|
||||
{ rol: { $in: ['ADMIN', 'OPERADOR'] } },
|
||||
{ esSuperAdmin: true },
|
||||
{ telefono: { $in: [SUPER_ADMIN_PHONE, SUPER_ADMIN_LOCAL_PHONE] } },
|
||||
],
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
try {
|
||||
// Asegurar que el Superadministrador esté registrado
|
||||
await this.ensureSuperAdminExists();
|
||||
|
||||
const dtos: UserSummaryDTO[] = admins.map((u) => {
|
||||
const isSuper = isPlatformSuperAdmin(u.telefono) || isPlatformSuperAdmin(u.email) || !!u.esSuperAdmin;
|
||||
return {
|
||||
id: u._id.toString(),
|
||||
appwriteId: u.appwriteId,
|
||||
nombre: u.nombre,
|
||||
telefono: u.telefono,
|
||||
email: u.email,
|
||||
documentoIdentidad: u.documentoIdentidad,
|
||||
esSuperAdmin: isSuper,
|
||||
rol: isSuper ? 'ADMIN' : u.rol,
|
||||
isActive: isSuper ? true : u.isActive,
|
||||
phoneVerified: isSuper ? true : u.phoneVerified,
|
||||
saldoPenalidad: 0,
|
||||
bloqueadoPorPenalidad: false,
|
||||
totalViajesCompletados: u.totalViajesCompletados || 0,
|
||||
totalViajesCancelados: u.totalViajesCancelados || 0,
|
||||
direccionTexto: u.direccionTexto,
|
||||
ciudad: u.ciudad,
|
||||
createdAt: u.createdAt,
|
||||
};
|
||||
});
|
||||
const admins = await User.find({
|
||||
$or: [
|
||||
{ rol: { $in: ['ADMIN', 'OPERADOR'] } },
|
||||
{ esSuperAdmin: true },
|
||||
{ telefono: { $in: [SUPER_ADMIN_PHONE, SUPER_ADMIN_LOCAL_PHONE] } },
|
||||
],
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.lean();
|
||||
|
||||
// Superadministrador primero
|
||||
return dtos.sort((a, b) => (b.esSuperAdmin ? 1 : 0) - (a.esSuperAdmin ? 1 : 0));
|
||||
const dtos: UserSummaryDTO[] = admins.map((u) => {
|
||||
const isSuper = isPlatformSuperAdmin(u.telefono) || isPlatformSuperAdmin(u.email) || !!u.esSuperAdmin;
|
||||
return {
|
||||
id: u._id.toString(),
|
||||
appwriteId: u.appwriteId,
|
||||
nombre: u.nombre,
|
||||
telefono: u.telefono,
|
||||
email: u.email,
|
||||
documentoIdentidad: u.documentoIdentidad,
|
||||
esSuperAdmin: isSuper,
|
||||
rol: isSuper ? 'ADMIN' : u.rol,
|
||||
isActive: isSuper ? true : u.isActive,
|
||||
phoneVerified: isSuper ? true : u.phoneVerified,
|
||||
saldoPenalidad: 0,
|
||||
bloqueadoPorPenalidad: false,
|
||||
totalViajesCompletados: u.totalViajesCompletados || 0,
|
||||
totalViajesCancelados: u.totalViajesCancelados || 0,
|
||||
direccionTexto: u.direccionTexto,
|
||||
ciudad: u.ciudad,
|
||||
createdAt: u.createdAt,
|
||||
};
|
||||
});
|
||||
|
||||
// Superadministrador primero
|
||||
return dtos.sort((a, b) => (b.esSuperAdmin ? 1 : 0) - (a.esSuperAdmin ? 1 : 0));
|
||||
} catch (e) {
|
||||
console.warn('⚠️ [getAdministrators Notice]:', e);
|
||||
const all = await this.getAllUsers();
|
||||
return all
|
||||
.filter((u) => u.rol === 'ADMIN' || u.rol === 'OPERADOR' || u.esSuperAdmin)
|
||||
.sort((a, b) => (b.esSuperAdmin ? 1 : 0) - (a.esSuperAdmin ? 1 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asegura la existencia y rol inmutable del Superadministrador en MongoDB
|
||||
*/
|
||||
public async ensureSuperAdminExists(): Promise<any> {
|
||||
let superAdmin = await User.findOne({
|
||||
$or: [
|
||||
{ telefono: SUPER_ADMIN_PHONE },
|
||||
{ telefono: SUPER_ADMIN_LOCAL_PHONE },
|
||||
],
|
||||
});
|
||||
if (!isDatabaseConnected()) {
|
||||
return localUserStore.findByPhone(SUPER_ADMIN_PHONE);
|
||||
}
|
||||
|
||||
try {
|
||||
let superAdmin = await User.findOne({
|
||||
$or: [
|
||||
{ telefono: SUPER_ADMIN_PHONE },
|
||||
{ telefono: SUPER_ADMIN_LOCAL_PHONE },
|
||||
],
|
||||
});
|
||||
|
||||
if (!superAdmin) {
|
||||
superAdmin = await User.create({
|
||||
@ -218,9 +272,25 @@ export class UserAdminService {
|
||||
if (needsSave) {
|
||||
await superAdmin.save();
|
||||
}
|
||||
}
|
||||
|
||||
return superAdmin;
|
||||
localUserStore.upsert({
|
||||
id: superAdmin._id.toString(),
|
||||
nombre: superAdmin.nombre,
|
||||
telefono: superAdmin.telefono,
|
||||
email: superAdmin.email,
|
||||
documentoIdentidad: superAdmin.documentoIdentidad,
|
||||
direccionTexto: superAdmin.direccionTexto,
|
||||
ciudad: superAdmin.ciudad,
|
||||
rol: 'ADMIN',
|
||||
esSuperAdmin: true,
|
||||
});
|
||||
|
||||
return superAdmin;
|
||||
}
|
||||
} catch (errDb) {
|
||||
console.warn('⚠️ [ensureSuperAdminExists Notice]: MongoDB no disponible, usando almacén local:', errDb);
|
||||
return localUserStore.findByPhone(SUPER_ADMIN_PHONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -238,7 +308,14 @@ export class UserAdminService {
|
||||
throw new Error('Este número corresponde al Superadministrador y ya se encuentra configurado.');
|
||||
}
|
||||
|
||||
let existing = await User.findOne({ telefono: cleanPhone });
|
||||
let existing: any = null;
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
existing = await User.findOne({ telefono: cleanPhone });
|
||||
} catch (e) {
|
||||
existing = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
existing.rol = data.rol || 'ADMIN';
|
||||
@ -247,7 +324,19 @@ export class UserAdminService {
|
||||
if (data.email && data.email.trim()) existing.email = data.email.trim().toLowerCase();
|
||||
if (data.documentoIdentidad) existing.documentoIdentidad = data.documentoIdentidad.trim();
|
||||
if (data.direccionTexto) existing.direccionTexto = data.direccionTexto.trim();
|
||||
await existing.save();
|
||||
try { await existing.save(); } catch (e) {}
|
||||
|
||||
localUserStore.upsert({
|
||||
id: existing._id.toString(),
|
||||
nombre: existing.nombre,
|
||||
telefono: cleanPhone,
|
||||
email: existing.email,
|
||||
documentoIdentidad: existing.documentoIdentidad,
|
||||
direccionTexto: existing.direccionTexto,
|
||||
rol: existing.rol,
|
||||
esSuperAdmin: false,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
return {
|
||||
id: existing._id.toString(),
|
||||
@ -268,45 +357,66 @@ export class UserAdminService {
|
||||
};
|
||||
}
|
||||
|
||||
const newAdmin = await User.create({
|
||||
appwriteId: `admin-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
||||
let newAdmin: any = null;
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
newAdmin = await User.create({
|
||||
appwriteId: `admin-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
|
||||
nombre: (data.nombre || 'Nuevo Administrador').trim(),
|
||||
telefono: cleanPhone,
|
||||
email: data.email ? data.email.trim().toLowerCase() : undefined,
|
||||
documentoIdentidad: data.documentoIdentidad ? data.documentoIdentidad.trim() : undefined,
|
||||
rol: data.rol || 'ADMIN',
|
||||
esSuperAdmin: false,
|
||||
saldoPenalidad: 0,
|
||||
coordenadasDomicilio: {
|
||||
type: 'Point',
|
||||
coordinates: [-76.532, 3.4516],
|
||||
},
|
||||
direccionTexto: data.direccionTexto || 'Cali, Valle del Cauca',
|
||||
ciudad: 'Cali',
|
||||
isActive: true,
|
||||
phoneVerified: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'whatsapp_otp',
|
||||
});
|
||||
} catch (e) {
|
||||
newAdmin = null;
|
||||
}
|
||||
}
|
||||
|
||||
const localRecord = localUserStore.upsert({
|
||||
id: newAdmin?._id?.toString(),
|
||||
nombre: (data.nombre || 'Nuevo Administrador').trim(),
|
||||
telefono: cleanPhone,
|
||||
email: data.email ? data.email.trim().toLowerCase() : undefined,
|
||||
documentoIdentidad: data.documentoIdentidad ? data.documentoIdentidad.trim() : undefined,
|
||||
rol: data.rol || 'ADMIN',
|
||||
esSuperAdmin: false,
|
||||
saldoPenalidad: 0,
|
||||
coordenadasDomicilio: {
|
||||
type: 'Point',
|
||||
coordinates: [-76.532, 3.4516],
|
||||
},
|
||||
direccionTexto: data.direccionTexto || 'Cali, Valle del Cauca',
|
||||
ciudad: 'Cali',
|
||||
rol: data.rol || 'ADMIN',
|
||||
esSuperAdmin: false,
|
||||
isActive: true,
|
||||
phoneVerified: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorMethod: 'whatsapp_otp',
|
||||
});
|
||||
|
||||
console.log(`🛡️ [Nuevo Admin Creado]: ${newAdmin.nombre} (+${newAdmin.telefono}) Rol: ${newAdmin.rol}`);
|
||||
const finalId = (newAdmin?._id || localRecord.id).toString();
|
||||
console.log(`🛡️ [Nuevo Admin Creado]: ${localRecord.nombre} (+${localRecord.telefono}) Rol: ${localRecord.rol}`);
|
||||
|
||||
return {
|
||||
id: newAdmin._id.toString(),
|
||||
appwriteId: newAdmin.appwriteId,
|
||||
nombre: newAdmin.nombre,
|
||||
telefono: newAdmin.telefono,
|
||||
email: newAdmin.email,
|
||||
documentoIdentidad: newAdmin.documentoIdentidad,
|
||||
id: finalId,
|
||||
appwriteId: newAdmin?.appwriteId || localRecord.appwriteId,
|
||||
nombre: localRecord.nombre,
|
||||
telefono: localRecord.telefono,
|
||||
email: localRecord.email,
|
||||
documentoIdentidad: localRecord.documentoIdentidad,
|
||||
esSuperAdmin: false,
|
||||
rol: newAdmin.rol,
|
||||
isActive: newAdmin.isActive,
|
||||
phoneVerified: newAdmin.phoneVerified,
|
||||
rol: localRecord.rol,
|
||||
isActive: localRecord.isActive,
|
||||
phoneVerified: true,
|
||||
saldoPenalidad: 0,
|
||||
bloqueadoPorPenalidad: false,
|
||||
totalViajesCompletados: 0,
|
||||
totalViajesCancelados: 0,
|
||||
createdAt: newAdmin.createdAt,
|
||||
createdAt: new Date(localRecord.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
@ -314,82 +424,139 @@ export class UserAdminService {
|
||||
* El Superadministrador edita los datos de un administrador
|
||||
*/
|
||||
public async updateAdministrator(adminId: string, updates: UpdateAdminDTO, requesterUserId?: string): Promise<UserSummaryDTO> {
|
||||
const user = await User.findById(adminId);
|
||||
if (!user) throw new Error('Administrador no encontrado.');
|
||||
let user: any = null;
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
user = await User.findById(adminId);
|
||||
} catch (e) {
|
||||
user = null;
|
||||
}
|
||||
}
|
||||
|
||||
const isTargetSuper = isPlatformSuperAdmin(user.telefono) || !!user.esSuperAdmin;
|
||||
const local = localUserStore.findById(adminId);
|
||||
if (!user && !local) throw new Error('Administrador no encontrado.');
|
||||
|
||||
const target = user || local;
|
||||
const isTargetSuper = isPlatformSuperAdmin(target.telefono) || !!target.esSuperAdmin;
|
||||
|
||||
if (updates.nombre && updates.nombre.trim()) {
|
||||
user.nombre = updates.nombre.trim();
|
||||
target.nombre = updates.nombre.trim();
|
||||
}
|
||||
if (updates.email !== undefined) {
|
||||
user.email = updates.email.trim().toLowerCase();
|
||||
target.email = updates.email.trim().toLowerCase();
|
||||
}
|
||||
if (updates.documentoIdentidad !== undefined) {
|
||||
user.documentoIdentidad = updates.documentoIdentidad.trim();
|
||||
target.documentoIdentidad = updates.documentoIdentidad.trim();
|
||||
}
|
||||
if (updates.direccionTexto !== undefined) {
|
||||
user.direccionTexto = updates.direccionTexto.trim();
|
||||
target.direccionTexto = updates.direccionTexto.trim();
|
||||
}
|
||||
if (updates.ciudad !== undefined) {
|
||||
user.ciudad = updates.ciudad.trim();
|
||||
target.ciudad = updates.ciudad.trim();
|
||||
}
|
||||
|
||||
// El rol del Superadministrador nunca puede ser degradado
|
||||
if (!isTargetSuper && updates.rol) {
|
||||
user.rol = updates.rol;
|
||||
target.rol = updates.rol;
|
||||
}
|
||||
|
||||
await user.save();
|
||||
if (user) {
|
||||
try { await user.save(); } catch (e) {}
|
||||
}
|
||||
|
||||
localUserStore.upsert({
|
||||
id: adminId,
|
||||
nombre: target.nombre,
|
||||
telefono: target.telefono,
|
||||
email: target.email,
|
||||
documentoIdentidad: target.documentoIdentidad,
|
||||
direccionTexto: target.direccionTexto,
|
||||
ciudad: target.ciudad,
|
||||
rol: target.rol,
|
||||
esSuperAdmin: isTargetSuper,
|
||||
});
|
||||
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
appwriteId: user.appwriteId,
|
||||
nombre: user.nombre,
|
||||
telefono: user.telefono,
|
||||
email: user.email,
|
||||
documentoIdentidad: user.documentoIdentidad,
|
||||
id: adminId,
|
||||
appwriteId: target.appwriteId || adminId,
|
||||
nombre: target.nombre,
|
||||
telefono: target.telefono,
|
||||
email: target.email,
|
||||
documentoIdentidad: target.documentoIdentidad,
|
||||
esSuperAdmin: isTargetSuper,
|
||||
rol: isTargetSuper ? 'ADMIN' : user.rol,
|
||||
isActive: user.isActive,
|
||||
phoneVerified: user.phoneVerified,
|
||||
saldoPenalidad: user.saldoPenalidad,
|
||||
rol: isTargetSuper ? 'ADMIN' : target.rol,
|
||||
isActive: target.isActive,
|
||||
phoneVerified: true,
|
||||
saldoPenalidad: target.saldoPenalidad || 0,
|
||||
bloqueadoPorPenalidad: false,
|
||||
totalViajesCompletados: user.totalViajesCompletados || 0,
|
||||
totalViajesCancelados: user.totalViajesCancelados || 0,
|
||||
createdAt: user.createdAt,
|
||||
totalViajesCompletados: target.totalViajesCompletados || 0,
|
||||
totalViajesCancelados: target.totalViajesCancelados || 0,
|
||||
createdAt: target.createdAt instanceof Date ? target.createdAt : new Date(target.createdAt),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa o inactiva a un administrador (Superadministrador inmutable)
|
||||
*/
|
||||
public async toggleAdministratorActive(adminId: string, isActive: boolean): Promise<IUser> {
|
||||
const user = await User.findById(adminId);
|
||||
if (!user) throw new Error('Administrador no encontrado.');
|
||||
public async toggleAdministratorActive(adminId: string, isActive: boolean): Promise<any> {
|
||||
let user: any = null;
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
user = await User.findById(adminId);
|
||||
} catch (e) {
|
||||
user = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPlatformSuperAdmin(user.telefono) || !!user.esSuperAdmin) {
|
||||
const local = localUserStore.findById(adminId);
|
||||
if (!user && !local) throw new Error('Administrador no encontrado.');
|
||||
|
||||
const target = user || local;
|
||||
if (isPlatformSuperAdmin(target.telefono) || !!target.esSuperAdmin) {
|
||||
throw new Error('El Superadministrador no puede ser inactivado bajo ninguna circunstancia.');
|
||||
}
|
||||
|
||||
user.isActive = isActive;
|
||||
await user.save();
|
||||
return user;
|
||||
if (user) {
|
||||
user.isActive = isActive;
|
||||
try { await user.save(); } catch (e) {}
|
||||
}
|
||||
|
||||
localUserStore.upsert({
|
||||
id: adminId,
|
||||
telefono: target.telefono,
|
||||
isActive,
|
||||
});
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina formalmente a un administrador (Superadministrador no puede ser eliminado)
|
||||
*/
|
||||
public async deleteAdministrator(adminId: string, requesterUserId?: string): Promise<boolean> {
|
||||
const user = await User.findById(adminId);
|
||||
if (!user) throw new Error('Administrador no encontrado.');
|
||||
let user: any = null;
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
user = await User.findById(adminId);
|
||||
} catch (e) {
|
||||
user = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPlatformSuperAdmin(user.telefono) || !!user.esSuperAdmin) {
|
||||
const local = localUserStore.findById(adminId);
|
||||
if (!user && !local) throw new Error('Administrador no encontrado.');
|
||||
|
||||
const target = user || local;
|
||||
if (isPlatformSuperAdmin(target.telefono) || !!target.esSuperAdmin) {
|
||||
throw new Error('El Superadministrador es permanente y no puede ser eliminado del sistema.');
|
||||
}
|
||||
|
||||
await User.findByIdAndDelete(adminId);
|
||||
console.log(`🗑️ [Admin Eliminado]: ${user.nombre} (${user.telefono}) eliminado por el Superadministrador.`);
|
||||
if (user) {
|
||||
try { await User.findByIdAndDelete(adminId); } catch (e) {}
|
||||
}
|
||||
|
||||
localUserStore.delete(adminId);
|
||||
console.log(`🗑️ [Admin Eliminado]: ${target.nombre} (${target.telefono}) eliminado por el Superadministrador.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -397,51 +564,82 @@ export class UserAdminService {
|
||||
* Permite a los usuarios actualizar su información personal EXCEPTO el número de celular
|
||||
*/
|
||||
public async updateUserProfile(userId: string, updates: UpdateUserProfileDTO): Promise<any> {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) throw new Error('Usuario no encontrado.');
|
||||
let user: any = null;
|
||||
if (isDatabaseConnected()) {
|
||||
try {
|
||||
user = await User.findById(userId);
|
||||
} catch (e) {
|
||||
user = null;
|
||||
}
|
||||
}
|
||||
|
||||
const localUser = localUserStore.findById(userId) || (userId.length === 10 || userId.length === 12 ? localUserStore.findByPhone(userId) : null);
|
||||
if (!user && !localUser) throw new Error('Usuario no encontrado.');
|
||||
|
||||
const target = user || localUser;
|
||||
const currentClean = (target.telefono || '').replace(/\D/g, '');
|
||||
|
||||
// Validación estricta de inmutabilidad del número de celular
|
||||
if (updates.telefono) {
|
||||
const cleanInput = updates.telefono.replace(/\D/g, '');
|
||||
const currentClean = user.telefono.replace(/\D/g, '');
|
||||
if (cleanInput && cleanInput !== currentClean && !currentClean.endsWith(cleanInput) && !cleanInput.endsWith(currentClean)) {
|
||||
throw new Error(
|
||||
`Por estrictas políticas de seguridad y autenticación 2FA OTP, el número de celular registrado (+${user.telefono}) no puede ser modificado. Para cambiar tu número oficial, contacta a la central de soporte.`
|
||||
`Por estrictas políticas de seguridad y autenticación 2FA OTP, el número de celular registrado (+${target.telefono}) no puede ser modificado. Para cambiar tu número oficial, contacta a la central de soporte.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (updates.nombre && updates.nombre.trim().length >= 3) {
|
||||
user.nombre = updates.nombre.trim();
|
||||
target.nombre = updates.nombre.trim();
|
||||
}
|
||||
if (updates.documentoIdentidad !== undefined) {
|
||||
user.documentoIdentidad = updates.documentoIdentidad.trim();
|
||||
target.documentoIdentidad = updates.documentoIdentidad.trim();
|
||||
}
|
||||
if (updates.email !== undefined) {
|
||||
user.email = updates.email.trim().toLowerCase();
|
||||
target.email = updates.email.trim().toLowerCase();
|
||||
}
|
||||
if (updates.direccionTexto !== undefined) {
|
||||
user.direccionTexto = updates.direccionTexto.trim();
|
||||
target.direccionTexto = updates.direccionTexto.trim();
|
||||
}
|
||||
if (updates.ciudad !== undefined) {
|
||||
user.ciudad = updates.ciudad.trim();
|
||||
target.ciudad = updates.ciudad.trim();
|
||||
}
|
||||
|
||||
await user.save();
|
||||
if (user) {
|
||||
try {
|
||||
await user.save();
|
||||
} catch (errSave) {
|
||||
console.warn('⚠️ [updateUserProfile DB Save Notice]:', errSave);
|
||||
}
|
||||
}
|
||||
|
||||
localUserStore.upsert({
|
||||
id: userId,
|
||||
nombre: target.nombre,
|
||||
telefono: currentClean,
|
||||
email: target.email,
|
||||
documentoIdentidad: target.documentoIdentidad,
|
||||
direccionTexto: target.direccionTexto,
|
||||
ciudad: target.ciudad,
|
||||
rol: target.rol,
|
||||
esSuperAdmin: target.esSuperAdmin,
|
||||
});
|
||||
|
||||
const isSuper = isPlatformSuperAdmin(currentClean) || isPlatformSuperAdmin(target.email) || !!target.esSuperAdmin;
|
||||
|
||||
return {
|
||||
id: user._id.toString(),
|
||||
_id: user._id.toString(),
|
||||
nombre: user.nombre,
|
||||
telefono: user.telefono,
|
||||
email: user.email,
|
||||
documentoIdentidad: user.documentoIdentidad,
|
||||
direccionTexto: user.direccionTexto,
|
||||
ciudad: user.ciudad,
|
||||
rol: user.rol,
|
||||
esSuperAdmin: isPlatformSuperAdmin(user.telefono) || !!user.esSuperAdmin,
|
||||
phoneVerified: user.phoneVerified,
|
||||
saldoPenalidad: user.saldoPenalidad,
|
||||
id: userId,
|
||||
_id: userId,
|
||||
nombre: target.nombre,
|
||||
telefono: target.telefono,
|
||||
email: target.email,
|
||||
documentoIdentidad: target.documentoIdentidad,
|
||||
direccionTexto: target.direccionTexto,
|
||||
ciudad: target.ciudad,
|
||||
rol: target.rol,
|
||||
esSuperAdmin: isSuper,
|
||||
phoneVerified: true,
|
||||
saldoPenalidad: target.saldoPenalidad || 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -98,20 +98,6 @@ export class WhatsAppNotificationService {
|
||||
return this.config.adminPhone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destino oficial para notificaciones de Dripzone:
|
||||
* Grupo "Dripzone Notificaciones"
|
||||
*/
|
||||
public async getDripzoneNotificationTarget(): Promise<string> {
|
||||
const dripGroup = await this.findGroupJidByName('Dripzone Notificaciones');
|
||||
if (dripGroup) return dripGroup;
|
||||
|
||||
const altDrip = await this.findGroupJidByName('Dripzone');
|
||||
if (altDrip) return altDrip;
|
||||
|
||||
return this.config.adminPhone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Envía un mensaje de texto con formato WhatsApp a través de Evolution API
|
||||
* Soporta tanto números telefónicos individuales (+57...) como Grupos WhatsApp (...@g.us)
|
||||
@ -330,7 +316,7 @@ export class WhatsAppNotificationService {
|
||||
'es-CO'
|
||||
)} COP*.\n\n` +
|
||||
`Tu cuenta se encuentra inhabilitada temporalmente para nuevas reservas hasta liquidar este saldo.\n\n` +
|
||||
`💳 *Paga aquí en línea con Bold (PSE, Nequi, Bre-B o Tarjetas) para reactivar tu cuenta inmediatamente:*\n` +
|
||||
`💳 *Paga aquí en línea de forma segura (PSE, Nequi, QR Bre-B o Tarjetas) para reactivar tu cuenta inmediatamente:*\n` +
|
||||
`👉 ${params.linkBoldLiquidacion}\n\n` +
|
||||
`_Una vez realizado el pago, tu cuenta quedará desbloqueada automáticamente._`;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user