feat(payments): mostrar identificador visual de modo (Produccion LIVE vs Pruebas SANDBOX) en metodos de pago y checkout

This commit is contained in:
AI Bot 2026-09-20 19:16:07 -05:00
parent d3a3637f96
commit 573c9a095a
5 changed files with 233 additions and 12 deletions

View File

@ -745,6 +745,25 @@ export default function AdminPaymentMethodsPage() {
<div className="flex flex-wrap items-center gap-2">
<h3 className="font-black text-white text-base">{method.name}</h3>
{/* IDENTIFICADOR DE ENTORNO: PRODUCCIÓN VS SANDBOX */}
{(method.type === 'bold' || method.boldChannel) ? (
settings.boldEnvironment === 'sandbox' ? (
<span className="text-[10px] bg-amber-500/20 text-amber-300 font-black px-2.5 py-0.5 rounded-full border border-amber-500/40 flex items-center gap-1 shadow-xs">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-ping" />
MODO PRUEBAS (SANDBOX)
</span>
) : (
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 font-black px-2.5 py-0.5 rounded-full border border-emerald-500/40 flex items-center gap-1 shadow-xs">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
PRODUCCIÓN (LIVE)
</span>
)
) : (
<span className="text-[10px] bg-neutral-800 text-neutral-400 font-semibold px-2 py-0.5 rounded-full border border-neutral-700">
PAGO DIRECTO
</span>
)}
{method.badge && (
<span className="text-[10px] bg-indigo-500/20 text-indigo-300 font-black px-2.5 py-0.5 rounded-full border border-indigo-500/30">
{method.badge}

View File

@ -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' });
}
}

View File

@ -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)})`}
</span>
</div>
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] px-2.5 py-0.5 rounded-full font-bold">
QR Bre-B / Bold
</span>
<div className="flex items-center gap-2">
{boldPayment?.environment === 'sandbox' || boldEnv === 'sandbox' ? (
<span className="bg-amber-500/20 text-amber-300 border border-amber-500/40 text-[9px] px-2 py-0.5 rounded-full font-black flex items-center gap-1 shadow-xs">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-ping" />
SANDBOX (PRUEBAS)
</span>
) : (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 text-[9px] px-2 py-0.5 rounded-full font-black flex items-center gap-1 shadow-xs">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
PRODUCCIÓN (LIVE)
</span>
)}
<span className="bg-purple-500/20 text-purple-300 border border-purple-500/40 text-[10px] px-2.5 py-0.5 rounded-full font-bold">
QR Bre-B / Bold
</span>
</div>
</div>
<p className="text-xs text-neutral-300 leading-relaxed">
@ -695,14 +710,39 @@ export default function CheckoutPage() {
{/* 2. Método de Pago */}
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl p-5 sm:p-6 shadow-sm space-y-4">
<h3 className="font-black text-white text-base flex items-center gap-2">
<Zap className="w-5 h-5 text-white" />
<span>2. Selecciona tu Método de Pago</span>
</h3>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<h3 className="font-black text-white text-base flex items-center gap-2">
<Zap className="w-5 h-5 text-purple-400" />
<span>2. Selecciona tu Método de Pago</span>
</h3>
{boldEnv === 'sandbox' ? (
<span className="bg-amber-500/20 text-amber-300 border border-amber-500/40 text-[10px] font-black px-2.5 py-1 rounded-full flex items-center gap-1.5 shadow-sm w-fit">
<span className="w-2 h-2 rounded-full bg-amber-400 animate-ping shrink-0" />
<span>MODO PRUEBAS (SANDBOX)</span>
</span>
) : (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 text-[10px] font-black px-2.5 py-1 rounded-full flex items-center gap-1.5 shadow-sm w-fit">
<span className="w-2 h-2 rounded-full bg-emerald-400 shrink-0" />
<span>PRODUCCIÓN (LIVE)</span>
</span>
)}
</div>
{boldEnv === 'sandbox' && (
<div className="bg-amber-950/40 border border-amber-500/30 rounded-2xl p-3 flex items-start gap-2.5 text-xs text-amber-200">
<AlertCircle className="w-4 h-4 text-amber-400 shrink-0 mt-0.5" />
<div>
<p className="font-bold text-amber-300">Pasarela Bold en Modo Pruebas Activo</p>
<p className="text-[11px] text-amber-200/80 mt-0.5">Estás en entorno Sandbox. Los pagos generados con Bold son de prueba y no debitarán dinero real de tus cuentas.</p>
</div>
</div>
)}
<div className="space-y-2.5">
{paymentMethods.map((method) => {
const isSelected = selectedPaymentId === method.id;
const isBold = method.type === 'bold' || Boolean(method.boldChannel);
return (
<div
key={method.id}
@ -713,7 +753,7 @@ export default function CheckoutPage() {
: 'border-neutral-800 hover:border-neutral-700 bg-neutral-950'
}`}
>
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div className="flex items-center space-x-2">
<div
className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
@ -725,7 +765,22 @@ export default function CheckoutPage() {
<span className="font-black text-white text-sm">{method.name}</span>
</div>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-1.5 pl-6 sm:pl-0">
{/* IDENTIFICADOR DE ENTORNO EN CADA MÉTODO */}
{isBold && (
boldEnv === 'sandbox' ? (
<span className="bg-amber-500/20 text-amber-300 border border-amber-500/40 text-[9px] font-black px-2 py-0.5 rounded-full flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse" />
PRUEBAS SANDBOX
</span>
) : (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 text-[9px] font-black px-2 py-0.5 rounded-full flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
EN VIVO LIVE
</span>
)
)}
{method.badge ? (
<span className="bg-indigo-500/20 text-indigo-300 border border-indigo-500/30 text-[10px] font-black px-2 py-0.5 rounded-full uppercase">
{method.badge}

View File

@ -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;

View File

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