almeira-web/scripts/deploy_billing_update.py

217 lines
10 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()