import paramiko import os import urllib.request import json import ssl import sys import io import time import tarfile 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+*?" REMOTE_TEMP_TAR = "/home/carlos/pacifictrans_update.tar.gz" LOCAL_BACKEND = r"c:\Users\Carlos\dev\almeira-web\transporte-cali-buenaventura\backend" LOCAL_APK = r"c:\Users\Carlos\dev\almeira-web\pacifictrans-app\public\PacifictransGo.apk" LOCAL_TAR = r"c:\Users\Carlos\dev\almeira-web\pacifictrans_update.tar.gz" EVOLUTION_API_URL = "https://evolution.almeira.com.co" EVOLUTION_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943" INSTANCE_NAME = "AutomataAlmeira" TARGET_PHONE = "573167455877" APK_URL = "https://pacifictrans.almeira.com.co/PacifictransGo.apk" API_URL = "https://pacifictrans.almeira.com.co" ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE def create_bundle_tar(): print("[1/5] Creando empaquetado de actualización de backend...", flush=True) if os.path.exists(LOCAL_TAR): os.remove(LOCAL_TAR) with tarfile.open(LOCAL_TAR, "w:gz") as tar: # Añadir dist dist_path = os.path.join(LOCAL_BACKEND, "dist") if os.path.exists(dist_path): tar.add(dist_path, arcname="dist") # Añadir public public_path = os.path.join(LOCAL_BACKEND, "public") if os.path.exists(public_path): tar.add(public_path, arcname="public") # Añadir migrations mig_path = os.path.join(LOCAL_BACKEND, "migrations") if os.path.exists(mig_path): tar.add(mig_path, arcname="migrations") # Añadir data data_path = os.path.join(LOCAL_BACKEND, "data") if os.path.exists(data_path): tar.add(data_path, arcname="data") # Añadir node_modules de node-cron y csv-writer for pkg in ['node-cron', 'csv-writer']: pkg_path = os.path.join(LOCAL_BACKEND, "node_modules", pkg) if os.path.exists(pkg_path): tar.add(pkg_path, arcname=f"node_modules/{pkg}") # package.json pkg_json = os.path.join(LOCAL_BACKEND, "package.json") if os.path.exists(pkg_json): tar.add(pkg_json, arcname="package.json") size_mb = os.path.getsize(LOCAL_TAR) / (1024 * 1024) print(f" -> Archivo comprimido creado: {LOCAL_TAR} ({size_mb:.2f} MB)", flush=True) def deploy_to_vps(): print("[2/5] Conectando por SSH al VPS y desplegando actualización...", flush=True) ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=30) # Subir tar sftp = ssh.open_sftp() print(f" -> Subiendo {REMOTE_TEMP_TAR} al VPS...", flush=True) sftp.put(LOCAL_TAR, REMOTE_TEMP_TAR) # Subir APK al path público de respaldo if os.path.exists(LOCAL_APK): print(" -> Subiendo APK actualizado al VPS...", flush=True) sftp.put(LOCAL_APK, "/home/carlos/apps/dripzone-app/public/PacifictransGo.apk") sftp.close() # Detectar contenedores activos de Pacific Trans stdin, stdout, stderr = ssh.exec_command("docker ps --format '{{.Names}}'") containers = [c.strip() for c in stdout.read().decode('utf-8').splitlines() if c.strip()] pacific_containers = [c for c in containers if any(k in c.lower() for k in ['tayk', 'pacific'])] if not pacific_containers: # Fallback a cualquier contenedor con drip o web si pacific no tiene nombre explícito pacific_containers = [c for c in containers if 'tayk' in c.lower()] print(f" -> Contenedores destino encontrados: {pacific_containers}", flush=True) for c in pacific_containers: print(f" -> Aplicando actualización en contenedor Coolify: {c}...", flush=True) # Copiar tar dentro del contenedor y descomprimir ssh.exec_command(f"docker cp {REMOTE_TEMP_TAR} {c}:/app/pacifictrans_update.tar.gz") stdin, stdout, stderr = ssh.exec_command( f"docker exec {c} sh -c 'cd /app && tar -xzf pacifictrans_update.tar.gz && rm -f pacifictrans_update.tar.gz'" ) stdout.channel.recv_exit_status() # Copiar APK ssh.exec_command(f"docker cp /home/carlos/apps/dripzone-app/public/PacifictransGo.apk {c}:/app/public/PacifictransGo.apk") # Reiniciar contenedor en Coolify print(f" -> Reiniciando contenedor {c}...", flush=True) stdin, stdout, stderr = ssh.exec_command(f"docker restart {c}") stdout.channel.recv_exit_status() print(f" [OK] Contenedor {c} reiniciado con nueva versión.", flush=True) # Limpiar tar temporal en VPS ssh.exec_command(f"rm -f {REMOTE_TEMP_TAR}") ssh.close() def verify_endpoints(): print("[3/5] Verificando endpoints online en producción...", flush=True) time.sleep(5) # 1. Healthcheck try: req = urllib.request.Request(f"{API_URL}/api/health") with urllib.request.urlopen(req, context=ctx, timeout=15) as res: data = json.loads(res.read().decode('utf-8')) print(f" [OK] /api/health: {data.get('status')} ({res.status})", flush=True) except Exception as e: print(f" [Aviso Healthcheck]: {e}", flush=True) # 2. Reporte Mensual try: req = urllib.request.Request(f"{API_URL}/api/admin/billing/reporte-mensual?mes=10&anio=2026") with urllib.request.urlopen(req, context=ctx, timeout=15) as res: data = json.loads(res.read().decode('utf-8')) print(f" [OK] /api/admin/billing/reporte-mensual: HTTP {res.status}, {data.get('count', 0)} cierres encontrados", flush=True) except Exception as e: print(f" [Aviso Reporte]: {e}", flush=True) # 3. CSV Export try: req = urllib.request.Request(f"{API_URL}/api/admin/billing/reporte-mensual?mes=10&anio=2026&format=csv") with urllib.request.urlopen(req, context=ctx, timeout=15) as res: first_line = res.readline().decode('utf-8', errors='replace').strip() print(f" [OK] /api/admin/billing/reporte-mensual?format=csv: HTTP {res.status}, Cabeceras: {first_line[:60]}...", flush=True) except Exception as e: print(f" [Aviso CSV]: {e}", flush=True) def notify_whatsapp(): print("[4/5] Enviando notificación de consolidación y facturación a WhatsApp (+57 316 745 5877)...", flush=True) text_msg = ( "📑 *Pacific Trans GO: Consolidación Mensual de Comisiones & Facturación DIAN*\n\n" "¡Hola Carlos! Se ha implementado con éxito la arquitectura de consolidación mensual y control fiscal en la plataforma:\n\n" "1️⃣ *Esquema y Migración de Base de Datos:*\n" "• Tabla: `cierres_facturacion_mensual` (UUID PK, Conductor ID, NIT/Cédula, Razón Social, Correo, Mes, Año, Comisiones Acumuladas, Total Viajes, Estado Facturación, Fecha Cierre).\n" "• Migración SQL creada en `migrations/001_create_cierres_facturacion_mensual.sql` y Modelo Mongoose con persistencia autónoma.\n\n" "2️⃣ *Cron Job de Corte Mensual Automático:*\n" "• Programado para ejecutarse el día 1 de cada mes a las 00:00 horas (`0 0 1 * *`).\n" "• ⚠️ *Cálculo Estricto:* Basado *ÚNICAMENTE* en la comisión de software cobrada/retenida a los conductores y *NUNCA* en el valor total de los viajes.\n" "• Registro inicial en estado `'pendiente_proceso'`.\n\n" "3️⃣ *Endpoint de Exportación a Excel / CSV (DIAN):*\n" "• `GET /api/admin/billing/reporte-mensual?mes=X&anio=Y&format=csv`\n" "• Columnas exactas para copiado/pegado masivo en el portal DIAN: `ID_Cierre, NIT/Cédula, Conductor, Correo, Total_Comisión, Cantidad_Viajes` con codificación UTF-8 BOM.\n\n" "4️⃣ *Acciones en Masa y Control de Estados:*\n" "• `POST /api/admin/billing/marcar-facturado` recibiendo arreglo de `IDs_Cierre`.\n" "• Actualiza el estado a `'facturado_manual'` para evitar duplicidad de facturas en la DIAN.\n\n" "5️⃣ *Panel Administrativo Web:*\n" "• Sub-pestaña `📑 6. Cierres & Facturación DIAN` con selector de mes/año, KPIs fiscales, disparador manual de corte, selección múltiple y descarga directa del archivo CSV.\n\n" f"📲 *APK Android Actualizado:* {APK_URL}" ) req_text = urllib.request.Request( f"{EVOLUTION_API_URL}/message/sendText/{INSTANCE_NAME}", data=json.dumps({"number": TARGET_PHONE, "text": text_msg, "delay": 300}).encode("utf-8"), headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"} ) try: with urllib.request.urlopen(req_text, context=ctx, timeout=40) as resp: print(" [OK] Mensaje de texto enviado a WhatsApp (Status:", resp.status, ")", flush=True) except Exception as e: print(" [Aviso texto WhatsApp]:", e, flush=True) # Envío del archivo binario APK print("[5/5] Enviando archivo APK como documento a WhatsApp...", flush=True) req_media = urllib.request.Request( f"{EVOLUTION_API_URL}/message/sendMedia/{INSTANCE_NAME}", data=json.dumps({ "number": TARGET_PHONE, "mediaMessage": { "mediatype": "document", "caption": "📲 PacifictransGo.apk - Instalador Oficial Android (Pacific Trans GO)", "media": APK_URL, "fileName": "PacifictransGo.apk" } }).encode("utf-8"), headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"} ) try: with urllib.request.urlopen(req_media, context=ctx, timeout=60) as resp: print(" [OK] Documento APK enviado a WhatsApp (Status:", resp.status, ")", flush=True) except Exception as e: print(" [Aviso documento APK WhatsApp]:", e, flush=True) print("\n✅ [DESPLIEGUE Y NOTIFICACIÓN DIAN COMPLETADOS EXITOSAMENTE]", flush=True) def main(): create_bundle_tar() deploy_to_vps() verify_endpoints() notify_whatsapp() if __name__ == '__main__': main()