217 lines
10 KiB
Python
217 lines
10 KiB
Python
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()
|