From 573c9a095ab5c4c096db89b861fb1397abc2cdac Mon Sep 17 00:00:00 2001 From: AI Bot Date: Sun, 20 Sep 2026 19:16:07 -0500 Subject: [PATCH] feat(payments): mostrar identificador visual de modo (Produccion LIVE vs Pruebas SANDBOX) en metodos de pago y checkout --- .../src/app/admin/metodos-pago/page.tsx | 19 +++ .../src/app/api/payment-methods/route.ts | 11 +- dripzone-app/src/app/checkout/page.tsx | 75 ++++++++-- dripzone-app/src/types/index.ts | 1 + scripts/deliver_bold_modes_update.py | 139 ++++++++++++++++++ 5 files changed, 233 insertions(+), 12 deletions(-) create mode 100644 scripts/deliver_bold_modes_update.py diff --git a/dripzone-app/src/app/admin/metodos-pago/page.tsx b/dripzone-app/src/app/admin/metodos-pago/page.tsx index ec28c19..3d60826 100644 --- a/dripzone-app/src/app/admin/metodos-pago/page.tsx +++ b/dripzone-app/src/app/admin/metodos-pago/page.tsx @@ -745,6 +745,25 @@ export default function AdminPaymentMethodsPage() {

{method.name}

+ {/* IDENTIFICADOR DE ENTORNO: PRODUCCIÓN VS SANDBOX */} + {(method.type === 'bold' || method.boldChannel) ? ( + settings.boldEnvironment === 'sandbox' ? ( + + + MODO PRUEBAS (SANDBOX) + + ) : ( + + + PRODUCCIÓN (LIVE) + + ) + ) : ( + + PAGO DIRECTO + + )} + {method.badge && ( {method.badge} diff --git a/dripzone-app/src/app/api/payment-methods/route.ts b/dripzone-app/src/app/api/payment-methods/route.ts index dcc36bd..2db0d2b 100644 --- a/dripzone-app/src/app/api/payment-methods/route.ts +++ b/dripzone-app/src/app/api/payment-methods/route.ts @@ -5,16 +5,23 @@ import { updatePaymentMethodInAppwrite, deletePaymentMethodInAppwrite, syncDefaultPaymentMethodsToAppwrite, + getSettingsFromAppwrite, } from '@/lib/server-appwrite'; import { dataService, INITIAL_PAYMENT_METHODS } from '@/lib/data-service'; export async function GET() { try { const paymentMethods = await getPaymentMethodsFromAppwrite(); - return NextResponse.json({ success: true, paymentMethods }); + const settings = await getSettingsFromAppwrite(); + const boldEnvironment = settings?.boldEnvironment || 'production'; + const enriched = paymentMethods.map((m) => ({ + ...m, + environment: (m.type === 'bold' || m.boldChannel) ? boldEnvironment : undefined, + })); + return NextResponse.json({ success: true, paymentMethods: enriched, boldEnvironment }); } catch (error: any) { const fallback = dataService.getPaymentMethods(); - return NextResponse.json({ success: true, paymentMethods: fallback }); + return NextResponse.json({ success: true, paymentMethods: fallback, boldEnvironment: 'production' }); } } diff --git a/dripzone-app/src/app/checkout/page.tsx b/dripzone-app/src/app/checkout/page.tsx index cbcc6be..a6100fa 100644 --- a/dripzone-app/src/app/checkout/page.tsx +++ b/dripzone-app/src/app/checkout/page.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import Link from 'next/link'; -import { ArrowLeft, CheckCircle2, ShieldCheck, Truck, Zap, QrCode, Phone, MapPin, User, MessageCircle, ExternalLink, CreditCard } from 'lucide-react'; +import { ArrowLeft, CheckCircle2, ShieldCheck, Truck, Zap, QrCode, Phone, MapPin, User, MessageCircle, ExternalLink, CreditCard, AlertCircle } from 'lucide-react'; import { City, PaymentMethod, Order, StoreSettings } from '@/types'; import { INITIAL_CITIES, INITIAL_PAYMENT_METHODS, INITIAL_PRODUCTS } from '@/lib/data-service'; import { formatCOP } from '@/lib/pricing-engine'; @@ -68,6 +68,7 @@ export default function CheckoutPage() { checkoutUrl: string; qrImageUrl: string; amount: number; + environment?: string; } | null>(null); const [boldPaid, setBoldPaid] = useState(false); const [boldLoading, setBoldLoading] = useState(false); @@ -234,6 +235,7 @@ export default function CheckoutPage() { const officialWhatsapp = storeSettings?.whatsappNumber || '573106390296'; const boldLink = activePayment?.boldPaymentLink || storeSettings?.boldPaymentLink || 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C'; + const boldEnv = storeSettings?.boldEnvironment || activePayment?.environment || 'production'; const processOrderSubmission = async () => { setIsSubmitting(true); @@ -474,9 +476,22 @@ export default function CheckoutPage() { : `Paso 2: Anticipo de Separación (${formatCOP(orderCompleted.depositAmount || 10000)})`}
- - QR Bre-B / Bold - +
+ {boldPayment?.environment === 'sandbox' || boldEnv === 'sandbox' ? ( + + + SANDBOX (PRUEBAS) + + ) : ( + + + PRODUCCIÓN (LIVE) + + )} + + QR Bre-B / Bold + +

@@ -695,14 +710,39 @@ export default function CheckoutPage() { {/* 2. Método de Pago */}

-

- - 2. Selecciona tu Método de Pago -

+
+

+ + 2. Selecciona tu Método de Pago +

+ + {boldEnv === 'sandbox' ? ( + + + MODO PRUEBAS (SANDBOX) + + ) : ( + + + PRODUCCIÓN (LIVE) + + )} +
+ + {boldEnv === 'sandbox' && ( +
+ +
+

Pasarela Bold en Modo Pruebas Activo

+

Estás en entorno Sandbox. Los pagos generados con Bold son de prueba y no debitarán dinero real de tus cuentas.

+
+
+ )}
{paymentMethods.map((method) => { const isSelected = selectedPaymentId === method.id; + const isBold = method.type === 'bold' || Boolean(method.boldChannel); return (
-
+
{method.name}
-
+
+ {/* IDENTIFICADOR DE ENTORNO EN CADA MÉTODO */} + {isBold && ( + boldEnv === 'sandbox' ? ( + + + PRUEBAS SANDBOX + + ) : ( + + + EN VIVO LIVE + + ) + )} + {method.badge ? ( {method.badge} diff --git a/dripzone-app/src/types/index.ts b/dripzone-app/src/types/index.ts index 29565d9..d873e76 100644 --- a/dripzone-app/src/types/index.ts +++ b/dripzone-app/src/types/index.ts @@ -110,6 +110,7 @@ export interface PaymentMethod { boldChannel?: 'deposit' | 'pse' | 'card' | 'nequi' | 'bancolombia' | 'bre_b' | 'all' | 'custom'; badge?: string; icon?: string; + environment?: 'sandbox' | 'production'; description: string; requiresDeposit: boolean; depositAmount: number; diff --git a/scripts/deliver_bold_modes_update.py b/scripts/deliver_bold_modes_update.py new file mode 100644 index 0000000..11e9399 --- /dev/null +++ b/scripts/deliver_bold_modes_update.py @@ -0,0 +1,139 @@ +import paramiko +import os +import urllib.request +import json +import ssl +import sys +import io +import time + +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_BASE = "/home/carlos/apps/dripzone-app" +LOCAL_APK = r"c:\Users\Carlos\dev\almeira-web\dripzone-app\public\DripZone.apk" +REMOTE_APK = f"{REMOTE_BASE}/public/DripZone.apk" + +EVOLUTION_API_URL = "https://evolution.almeira.com.co" +ALMEIRA_INSTANCE_NAME = "AutomataAlmeira" +ALMEIRA_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943" +TARGET_PHONE = "573167455877" +APK_URL = "https://drip-zone.online/DripZone.apk" + +ctx = ssl.create_default_context() +ctx.check_hostname = False +ctx.verify_mode = ssl.CERT_NONE + +def main(): + print("[1/4] Verificando APK local...") + if not os.path.exists(LOCAL_APK): + print(f" [ERROR] El archivo {LOCAL_APK} no existe.") + sys.exit(1) + + size_mb = os.path.getsize(LOCAL_APK) / (1024 * 1024) + print(f" -> APK verificado: {LOCAL_APK} ({size_mb:.2f} MB)") + + print("[2/4] Subiendo APK a VPS vía SFTP...") + 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() + + try: + sftp.mkdir(f"{REMOTE_BASE}/public") + except: + pass + + def progress_cb(transferred, total): + pct = (transferred / total) * 100 + if transferred % (1024 * 1024 * 5) < 32768 or transferred == total: + print(f" -> Progreso SFTP: {transferred / (1024*1024):.1f} MB / {total / (1024*1024):.1f} MB ({pct:.0f}%)") + + sftp.put(LOCAL_APK, REMOTE_APK, callback=progress_cb) + sftp.close() + print(" -> APK subido exitosamente al VPS.") + + print("[3/4] Copiando APK a contenedores Docker activos...") + find_cmd = "docker ps --filter 'name=dripzone' --format '{{.ID}} {{.Names}}'" + stdin, stdout, stderr = ssh.exec_command(find_cmd) + containers = stdout.read().decode().strip().splitlines() + + for line in containers: + if not line.strip(): + continue + cid = line.split()[0] + cname = line.split()[1] if len(line.split()) > 1 else cid + print(f" -> Copiando a contenedor {cname} ({cid})...") + cp_cmd = f"docker cp {REMOTE_APK} {cid}:/app/public/DripZone.apk" + ssh.exec_command(cp_cmd) + + ssh.close() + print(" -> Despliegue en VPS completado.") + + print("[4/4] Enviando notificación y archivo APK a Carlos (+57 316 745 5877)...") + text_payload = { + "number": TARGET_PHONE, + "text": ( + "🚀 *Actualización DripZone: Switch de Entornos Bold (Producción / Sandbox) y Métodos por Canal*\n\n" + "Carlos, se completó con éxito la configuración y despliegue del sistema de medios de pago:\n\n" + "🟢 *1. Selector de Modo Producción vs Modo Pruebas (Sandbox):*\n" + "• En *Configuración* y en *Métodos de Pago* cuentas con un switch interactivo para alternar entre:\n" + " - 🟢 *Modo Producción (Live):* Pagos reales procesados por Bold.co con llaves de producción oficiales.\n" + " - 🟡 *Modo Pruebas (Sandbox):* Entorno de pruebas oficial con llaves sandbox para validar flujos de compra sin transacciones bancarias reales.\n\n" + "💳 *2. Métodos de Pago Organizados por Canales Oficiales Bold:*\n" + "• *Separación con $10.000 COP:* Anticipo en línea + Saldo Contraentrega.\n" + "• *PSE Directo:* Débito en línea desde todos los bancos.\n" + "• *Tarjetas Débito y Crédito:* Hasta 36 cuotas (Visa, Mastercard, Amex).\n" + "• *Nequi en Línea:* Notificación y clave dinámica vía Bold.\n" + "• *Botón Bancolombia:* Débito directo de cuenta.\n" + "• *Código QR Bre-B:* Interoperabilidad instantánea.\n" + "• *Checkout Global Bold:* Portal unificado.\n" + "• *Pago Contraentrega y Transferencias Directas.*\n\n" + "⚡ *3. Botón de Sincronización Rápida:*\n" + "• En el panel admin puedes restaurar con un solo clic todos los métodos oficiales en Appwrite.\n\n" + "📲 *Enlace directo de descarga APK:*\n" + f"{APK_URL}" + ) + } + + req_text = urllib.request.Request( + f"{EVOLUTION_API_URL}/message/sendText/{ALMEIRA_INSTANCE_NAME}", + data=json.dumps(text_payload).encode('utf-8'), + headers={"apikey": ALMEIRA_API_KEY, "Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req_text, context=ctx, timeout=20) as resp: + print(f" -> Mensaje de texto enviado a WhatsApp (Status: {resp.status})") + except Exception as e: + print(" [WARN WhatsApp Text]:", e) + + time.sleep(1) + + # Enviar documento APK + media_payload = { + "number": TARGET_PHONE, + "mediatype": "document", + "mimetype": "application/vnd.android.package-archive", + "fileName": "DripZone-v2.8-Bold-Modos.apk", + "caption": "📲 *DripZone Android v2.8 - Switch Producción/Pruebas Bold + Canales de Pago*", + "media": APK_URL + } + req_media = urllib.request.Request( + f"{EVOLUTION_API_URL}/message/sendMedia/{ALMEIRA_INSTANCE_NAME}", + data=json.dumps(media_payload).encode('utf-8'), + headers={"apikey": ALMEIRA_API_KEY, "Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req_media, context=ctx, timeout=90) as resp: + print(f" -> Documento APK enviado a WhatsApp (Status: {resp.status})") + except Exception as e: + print(" [WARN WhatsApp Document]:", e) + + print("\n🎉 ¡ENTREGA Y DESPLIEGUE FINALIZADOS CON ÉXITO!") + +if __name__ == "__main__": + main()