feat(payments): mostrar identificador visual de modo (Produccion LIVE vs Pruebas SANDBOX) en metodos de pago y checkout
This commit is contained in:
parent
d3a3637f96
commit
573c9a095a
@ -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}
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
139
scripts/deliver_bold_modes_update.py
Normal file
139
scripts/deliver_bold_modes_update.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user