feat(payments): configuracion de metodos de pago por canales Bold y switch produccion/pruebas sandbox
This commit is contained in:
parent
aaa571ccc7
commit
d3a3637f96
@ -186,22 +186,54 @@ export default function AdminSettingsPage() {
|
||||
<Zap className="w-4 h-4 text-indigo-400" />
|
||||
<span>Integración con Bold.co (Link de Pago, Botón & QR)</span>
|
||||
</h2>
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.boldEnabled ?? true}
|
||||
onChange={(e) => handleChange('boldEnabled', e.target.checked)}
|
||||
className="w-4 h-4 rounded text-indigo-500 focus:ring-indigo-400"
|
||||
/>
|
||||
<span className="text-xs font-bold text-indigo-300">Activo en Checkout</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center bg-neutral-950 p-1 rounded-xl border border-white/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleChange('boldEnvironment', 'production')}
|
||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
||||
(settings.boldEnvironment ?? 'production') === 'production'
|
||||
? 'bg-emerald-600 text-white shadow-xs'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Producción
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleChange('boldEnvironment', 'sandbox')}
|
||||
className={`px-3 py-1 text-xs font-bold rounded-lg transition-all ${
|
||||
settings.boldEnvironment === 'sandbox'
|
||||
? 'bg-amber-500 text-neutral-950 font-black shadow-xs'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Pruebas (Sandbox)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.boldEnabled ?? true}
|
||||
onChange={(e) => handleChange('boldEnabled', e.target.checked)}
|
||||
className="w-4 h-4 rounded text-indigo-500 focus:ring-indigo-400"
|
||||
/>
|
||||
<span className="text-xs font-bold text-indigo-300">Activo</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-xs">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block text-[11px] font-bold text-indigo-300 mb-1">
|
||||
Link de Pago Oficial Bold.co (Link personalizado o de tu negocio) *
|
||||
</label>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="block text-[11px] font-bold text-indigo-300">
|
||||
Link de Pago Oficial Bold.co (Link de Checkout Oficial) *
|
||||
</label>
|
||||
<span className="text-[10px] text-neutral-400 font-mono">
|
||||
{(settings.boldEnvironment ?? 'production') === 'production' ? 'Modo Producción (En Vivo)' : 'Modo Pruebas (Sandbox)'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="url"
|
||||
|
||||
@ -1,58 +1,339 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { CreditCard, Zap, Plus, Trash2, Edit3, Save, Check, X, AlertTriangle, QrCode, ShieldCheck, DollarSign } from 'lucide-react';
|
||||
import { PaymentMethod } from '@/types';
|
||||
import { INITIAL_PAYMENT_METHODS } from '@/lib/data-service';
|
||||
import {
|
||||
CreditCard,
|
||||
Zap,
|
||||
Plus,
|
||||
Trash2,
|
||||
Edit3,
|
||||
Save,
|
||||
Check,
|
||||
X,
|
||||
AlertTriangle,
|
||||
QrCode,
|
||||
ShieldCheck,
|
||||
DollarSign,
|
||||
Landmark,
|
||||
Smartphone,
|
||||
Truck,
|
||||
ExternalLink,
|
||||
RefreshCw,
|
||||
Globe,
|
||||
Sliders,
|
||||
CheckCircle2,
|
||||
Layers,
|
||||
Copy
|
||||
} from 'lucide-react';
|
||||
import { PaymentMethod, StoreSettings } from '@/types';
|
||||
import { formatCOP } from '@/lib/pricing-engine';
|
||||
|
||||
const BOLD_TEMPLATES: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: PaymentMethod['type'];
|
||||
boldChannel: NonNullable<PaymentMethod['boldChannel']>;
|
||||
badge: string;
|
||||
icon: string;
|
||||
requiresDeposit: boolean;
|
||||
depositAmount: number;
|
||||
description: string;
|
||||
accountInfo: string;
|
||||
boldPaymentLink: string;
|
||||
}> = [
|
||||
{
|
||||
id: 'pay-bold-deposit-10k',
|
||||
name: 'Separación con $10.000 (Bold) + Saldo Contraentrega',
|
||||
type: 'contraentrega',
|
||||
boldChannel: 'deposit',
|
||||
badge: 'Bold ⚡ Anticipo Oficial',
|
||||
icon: 'Zap',
|
||||
requiresDeposit: true,
|
||||
depositAmount: 10000,
|
||||
description: 'Separa tu talla con solo $10.000 COP en línea a través de Bold (PSE, tarjetas de crédito/débito, Nequi o QR Bre-B) y cancelas el saldo restante en efectivo o transferencia al recibir en tu puerta.',
|
||||
accountInfo: 'Pasarela Bold Oficial • PSE, Tarjetas Visa/Mastercard, Nequi y QR Bre-B',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-pse',
|
||||
name: 'PSE • Débito desde cualquier Banco (vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'pse',
|
||||
badge: 'Bold 🏦 PSE Débito',
|
||||
icon: 'Landmark',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Paga el 100% de tu pedido al instante debitando directamente desde tu cuenta bancaria de ahorros o corriente de cualquier banco en Colombia a través de Bold.',
|
||||
accountInfo: 'Débito en línea disponible para Bancolombia, Davivienda, Banco de Bogotá, BBVA y más de 20 bancos',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-cards',
|
||||
name: 'Tarjetas Débito y Crédito (Visa, Mastercard, Amex - vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'card',
|
||||
badge: 'Bold 💳 Tarjetas & Cuotas',
|
||||
icon: 'CreditCard',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Paga 100% seguro a cuotas o de contado con tarjeta Visa, Mastercard, American Express o Diners Club respaldado con cifrado bancario de Bold.',
|
||||
accountInfo: 'Acepta tarjetas nacionales e internacionales a cuotas con verificación antifraude',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-nequi',
|
||||
name: 'Nequi en Línea (Pasarela Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'nequi',
|
||||
badge: 'Bold 📱 Nequi Oficial',
|
||||
icon: 'Smartphone',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Paga directo desde tu app Nequi mediante la pasarela segura de Bold con notificación push inmediata y confirmación automática.',
|
||||
accountInfo: 'Nequi en línea procesado directamente por la infraestructura de Bold',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-bancolombia',
|
||||
name: 'Botón Bancolombia (Transferencia Directa vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'bancolombia',
|
||||
badge: 'Bold ⚡ Botón Bancolombia',
|
||||
icon: 'Zap',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Transfiere en segundos desde tu aplicación Bancolombia sin digitar números de cuenta ni comprobantes manuales, debitado vía Bold.',
|
||||
accountInfo: 'Botón oficial Bancolombia integrado en la pasarela Bold',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-bre-b',
|
||||
name: 'Código QR Bre-B • Interoperabilidad Inmediata (vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'bre_b',
|
||||
badge: 'Bold ⚡ Bre-B Banco República',
|
||||
icon: 'QrCode',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Escanea y paga con el nuevo sistema interoperable Bre-B del Banco de la República desde cualquier billetera digital o entidad bancaria en Colombia.',
|
||||
accountInfo: 'Código QR de interoperabilidad nacional procesado por Bold',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-checkout',
|
||||
name: 'Checkout Global Bold • Todos los Métodos Digitales',
|
||||
type: 'bold',
|
||||
boldChannel: 'all',
|
||||
badge: 'Bold 🌐 Checkout Integral',
|
||||
icon: 'ShieldCheck',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Pasarela integral de Bold donde el cliente puede elegir entre PSE, Tarjetas Débito/Crédito, Nequi, Botón Bancolombia o QR Bre-B en una sola pantalla.',
|
||||
accountInfo: 'Pasarela Oficial Bold.co • Enlace seguro con cifrado bancario',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
},
|
||||
{
|
||||
id: 'pay-cod-full',
|
||||
name: 'Pago 100% Contraentrega en Efectivo (Cali y Alrededores)',
|
||||
type: 'contraentrega',
|
||||
boldChannel: 'custom',
|
||||
badge: '🤝 Entrega en Puerta',
|
||||
icon: 'Truck',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Pagas el 100% del valor en efectivo o transferencia directa al mensajero al momento de recibir en tu puerta (válido para entregas urbanas en 24 horas).',
|
||||
accountInfo: 'Entrega urbana express • Cancelas al recibir',
|
||||
boldPaymentLink: '',
|
||||
},
|
||||
{
|
||||
id: 'pay-direct-nequi-daviplata',
|
||||
name: 'Transferencia Directa Nequi / Daviplata (Carlos Lasso)',
|
||||
type: 'nequi',
|
||||
boldChannel: 'custom',
|
||||
badge: '📱 Transferencia Directa',
|
||||
icon: 'Smartphone',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
description: 'Transferencia manual directa a Nequi o Daviplata 316 745 5877 para clientes que prefieren pagar directo al titular.',
|
||||
accountInfo: 'Nequi / Daviplata: 316 745 5877 • Titular: Carlos Andrés Lasso',
|
||||
boldPaymentLink: '',
|
||||
},
|
||||
];
|
||||
|
||||
export default function AdminPaymentMethodsPage() {
|
||||
const [methods, setMethods] = useState<PaymentMethod[]>(INITIAL_PAYMENT_METHODS);
|
||||
const [methods, setMethods] = useState<PaymentMethod[]>([]);
|
||||
const [settings, setSettings] = useState<Partial<StoreSettings>>({
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
boldEnvironment: 'production',
|
||||
boldEnabled: true,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSavingSettings, setIsSavingSettings] = useState(false);
|
||||
const [isSyncingDefaults, setIsSyncingDefaults] = useState(false);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
// Filtro
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'bold' | 'deposit' | 'direct'>('all');
|
||||
|
||||
// Modal Crear / Editar
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingMethod, setEditingMethod] = useState<Partial<PaymentMethod> | null>(null);
|
||||
const [selectedPresetId, setSelectedPresetId] = useState<string>('pay-bold-deposit-10k');
|
||||
|
||||
// Modal Confirmar Eliminación
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [methodToDelete, setMethodToDelete] = useState<PaymentMethod | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadPaymentMethods();
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadPaymentMethods = async () => {
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/payment-methods');
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.paymentMethods)) {
|
||||
setMethods(data.paymentMethods);
|
||||
const [resMethods, resSettings] = await Promise.all([
|
||||
fetch('/api/payment-methods'),
|
||||
fetch('/api/settings'),
|
||||
]);
|
||||
const dataMethods = await resMethods.json();
|
||||
const dataSettings = await resSettings.json();
|
||||
|
||||
if (dataMethods.success && Array.isArray(dataMethods.paymentMethods)) {
|
||||
setMethods(dataMethods.paymentMethods);
|
||||
}
|
||||
if (dataSettings.success && dataSettings.settings) {
|
||||
setSettings((prev) => ({ ...prev, ...dataSettings.settings }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Error cargando métodos de pago:', err);
|
||||
console.warn('Error cargando métodos y configuración de Bold:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateBoldEnvironment = async (env: 'production' | 'sandbox') => {
|
||||
const updated = { ...settings, boldEnvironment: env };
|
||||
setSettings(updated);
|
||||
setIsSavingSettings(true);
|
||||
try {
|
||||
const res = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ boldEnvironment: env }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: `⚡ Entorno de Bold configurado exitosamente en: ${env === 'production' ? 'MODO PRODUCCIÓN (En Vivo)' : 'MODO PRUEBAS (Sandbox)'}`,
|
||||
});
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.error || 'No se pudo actualizar el entorno de Bold' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err.message || 'Error de conexión al cambiar entorno' });
|
||||
} finally {
|
||||
setIsSavingSettings(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveBoldLink = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSavingSettings(true);
|
||||
try {
|
||||
const res = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
boldPaymentLink: settings.boldPaymentLink,
|
||||
boldEnvironment: settings.boldEnvironment || 'production',
|
||||
boldEnabled: settings.boldEnabled !== false,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: '✅ Enlace de Checkout Global de Bold guardado y sincronizado correctamente.',
|
||||
});
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.error || 'Error al guardar enlace de Bold' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err.message || 'Error de conexión' });
|
||||
} finally {
|
||||
setIsSavingSettings(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncBoldDefaults = async () => {
|
||||
if (!confirm('¿Deseas restaurar y organizar todos los métodos oficiales de pago que ofrece Bold (PSE, Tarjetas, Nequi, Botón Bancolombia, QR Bre-B y Separación)?')) {
|
||||
return;
|
||||
}
|
||||
setIsSyncingDefaults(true);
|
||||
try {
|
||||
const res = await fetch('/api/payment-methods', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'sync_bold_defaults' }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success && Array.isArray(data.paymentMethods)) {
|
||||
setMethods(data.paymentMethods);
|
||||
setMessage({
|
||||
type: 'success',
|
||||
text: '🎉 ¡Métodos oficiales de Bold sincronizados y organizados con éxito en Appwrite!',
|
||||
});
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.error || 'Error al sincronizar métodos de Bold' });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setMessage({ type: 'error', text: err.message || 'Error de conexión al sincronizar' });
|
||||
} finally {
|
||||
setIsSyncingDefaults(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenCreateModal = () => {
|
||||
const defaultPreset = BOLD_TEMPLATES[0];
|
||||
setSelectedPresetId(defaultPreset.id);
|
||||
setEditingMethod({
|
||||
name: '',
|
||||
type: 'contraentrega',
|
||||
description: 'Paga al recibir en tu puerta',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 10000,
|
||||
accountInfo: '',
|
||||
boldPaymentLink: '',
|
||||
name: defaultPreset.name,
|
||||
type: defaultPreset.type,
|
||||
boldChannel: defaultPreset.boldChannel,
|
||||
badge: defaultPreset.badge,
|
||||
icon: defaultPreset.icon,
|
||||
description: defaultPreset.description,
|
||||
requiresDeposit: defaultPreset.requiresDeposit,
|
||||
depositAmount: defaultPreset.depositAmount,
|
||||
accountInfo: defaultPreset.accountInfo,
|
||||
boldPaymentLink: settings.boldPaymentLink || defaultPreset.boldPaymentLink,
|
||||
boldQrUrl: '',
|
||||
isActive: true,
|
||||
});
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSelectPreset = (presetId: string) => {
|
||||
setSelectedPresetId(presetId);
|
||||
const template = BOLD_TEMPLATES.find((t) => t.id === presetId);
|
||||
if (template && editingMethod) {
|
||||
setEditingMethod({
|
||||
...editingMethod,
|
||||
name: template.name,
|
||||
type: template.type,
|
||||
boldChannel: template.boldChannel,
|
||||
badge: template.badge,
|
||||
icon: template.icon,
|
||||
description: template.description,
|
||||
requiresDeposit: template.requiresDeposit,
|
||||
depositAmount: template.depositAmount,
|
||||
accountInfo: template.accountInfo,
|
||||
boldPaymentLink: template.boldPaymentLink || settings.boldPaymentLink || '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenEditModal = (method: PaymentMethod) => {
|
||||
setEditingMethod({ ...method });
|
||||
setIsEditModalOpen(true);
|
||||
@ -72,7 +353,9 @@ export default function AdminPaymentMethodsPage() {
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success) {
|
||||
setMethods((prev) => prev.map((m) => (m.id === editingMethod.id ? { ...m, ...editingMethod } as PaymentMethod : m)));
|
||||
setMethods((prev) =>
|
||||
prev.map((m) => (m.id === editingMethod.id ? ({ ...m, ...editingMethod } as PaymentMethod) : m))
|
||||
);
|
||||
setMessage({ type: 'success', text: `Método "${editingMethod.name}" actualizado en Appwrite.` });
|
||||
} else {
|
||||
setMessage({ type: 'error', text: data.error || 'Error al actualizar método de pago' });
|
||||
@ -138,33 +421,86 @@ export default function AdminPaymentMethodsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrado de Métodos
|
||||
const filteredMethods = methods.filter((m) => {
|
||||
if (activeTab === 'bold') {
|
||||
return m.type === 'bold' || m.boldChannel !== undefined;
|
||||
}
|
||||
if (activeTab === 'deposit') {
|
||||
return m.requiresDeposit || m.type === 'contraentrega';
|
||||
}
|
||||
if (activeTab === 'direct') {
|
||||
return m.type === 'nequi' || m.type === 'transferencia' || m.type === 'daviplata';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const getMethodIcon = (method: PaymentMethod) => {
|
||||
if (method.boldChannel === 'pse' || method.name.toLowerCase().includes('pse')) {
|
||||
return <Landmark className="w-5 h-5 text-sky-400" />;
|
||||
}
|
||||
if (method.boldChannel === 'card' || method.type === 'tarjeta' || method.name.toLowerCase().includes('tarjeta')) {
|
||||
return <CreditCard className="w-5 h-5 text-indigo-400" />;
|
||||
}
|
||||
if (method.boldChannel === 'nequi' || method.type === 'nequi' || method.name.toLowerCase().includes('nequi')) {
|
||||
return <Smartphone className="w-5 h-5 text-pink-400" />;
|
||||
}
|
||||
if (method.boldChannel === 'bancolombia' || method.name.toLowerCase().includes('bancolombia')) {
|
||||
return <Zap className="w-5 h-5 text-amber-400" />;
|
||||
}
|
||||
if (method.boldChannel === 'bre_b' || method.name.toLowerCase().includes('bre-b') || method.name.toLowerCase().includes('qr')) {
|
||||
return <QrCode className="w-5 h-5 text-emerald-400" />;
|
||||
}
|
||||
if (method.requiresDeposit || method.boldChannel === 'deposit') {
|
||||
return <ShieldCheck className="w-5 h-5 text-purple-400" />;
|
||||
}
|
||||
if (method.type === 'contraentrega') {
|
||||
return <Truck className="w-5 h-5 text-amber-400" />;
|
||||
}
|
||||
return <CreditCard className="w-5 h-5 text-purple-400" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2">
|
||||
<CreditCard className="w-6 h-6 text-purple-400" />
|
||||
<span>Configuración de Medios de Pago & Anticipos</span>
|
||||
<h1 className="text-2xl font-black text-white flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-xl bg-purple-600/20 border border-purple-500/40 flex items-center justify-center text-purple-400">
|
||||
<Zap className="w-5 h-5 fill-purple-400" />
|
||||
</div>
|
||||
<span>Configuración de Medios de Pago Bold & Anticipos</span>
|
||||
</h1>
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
Administra pasarelas de pago, anticipos de $10.000 COP para contraentrega, transferencias y enlaces Bold con persistencia en Appwrite.
|
||||
Administra los métodos de pago ofrecidos a través de la pasarela oficial <strong>Bold.co</strong> (PSE, Tarjetas Débito/Crédito, Nequi, Botón Bancolombia, QR Bre-B y Separación) con persistencia en Appwrite.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleOpenCreateModal}
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-2xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-purple-900/30 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Nuevo Método de Pago</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleSyncBoldDefaults}
|
||||
disabled={isSyncingDefaults}
|
||||
className="bg-neutral-900 hover:bg-neutral-800 text-purple-300 hover:text-purple-200 border border-purple-500/30 text-xs font-bold px-3.5 py-2.5 rounded-2xl flex items-center gap-2 transition-all cursor-pointer shadow-lg disabled:opacity-50"
|
||||
title="Restaura y sincroniza en Appwrite todos los métodos pre-configurados de Bold"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 ${isSyncingDefaults ? 'animate-spin' : ''}`} />
|
||||
<span>{isSyncingDefaults ? 'Sincronizando...' : 'Restaurar Métodos Bold ⚡'}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleOpenCreateModal}
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2.5 rounded-2xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-purple-900/40 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>Nuevo Método</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notification Message */}
|
||||
{message && (
|
||||
<div
|
||||
className={`px-4 py-3 rounded-2xl text-xs flex items-center justify-between border ${
|
||||
className={`px-4 py-3 rounded-2xl text-xs flex items-center justify-between border shadow-lg ${
|
||||
message.type === 'success'
|
||||
? 'bg-emerald-950/80 border-emerald-500/50 text-emerald-300'
|
||||
: 'bg-rose-950/80 border-rose-500/50 text-rose-300'
|
||||
@ -177,43 +513,260 @@ export default function AdminPaymentMethodsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hero: Panel de Control de Pasarela Bold & Selector de Modo (Producción / Pruebas) */}
|
||||
<div className="bg-gradient-to-br from-indigo-950/80 via-neutral-900 to-purple-950/50 border border-indigo-500/40 rounded-3xl p-5 sm:p-6 shadow-2xl space-y-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-white/10 pb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-11 h-11 rounded-2xl bg-indigo-600/30 border border-indigo-400/50 text-indigo-300 flex items-center justify-center font-black text-sm shadow-inner">
|
||||
⚡ BOLD
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<h2 className="text-base font-black text-white">Pasarela de Pagos Oficial Bold.co</h2>
|
||||
<span
|
||||
className={`text-[10px] font-black px-2.5 py-0.5 rounded-full uppercase border tracking-wider ${
|
||||
settings.boldEnvironment === 'production'
|
||||
? 'bg-emerald-500/20 text-emerald-400 border-emerald-500/40 shadow-xs shadow-emerald-500/20'
|
||||
: 'bg-amber-500/20 text-amber-300 border-amber-500/40'
|
||||
}`}
|
||||
>
|
||||
{settings.boldEnvironment === 'production' ? '● MODO PRODUCCIÓN (EN VIVO)' : '○ MODO PRUEBAS (SANDBOX)'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-300 mt-0.5">
|
||||
Procesa en línea transacciones con <strong>PSE</strong>, <strong>Tarjetas Débito/Crédito</strong>, <strong>Nequi</strong>, <strong>Botón Bancolombia</strong> y <strong>QR Bre-B</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selector de Modo Producción o Pruebas */}
|
||||
<div className="flex items-center bg-neutral-950 p-1.5 rounded-2xl border border-white/10 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUpdateBoldEnvironment('production')}
|
||||
disabled={isSavingSettings}
|
||||
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-black transition-all cursor-pointer ${
|
||||
settings.boldEnvironment === 'production'
|
||||
? 'bg-emerald-600 text-white shadow-md shadow-emerald-900/40 scale-102'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-300" />
|
||||
<span>Producción (En Vivo)</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUpdateBoldEnvironment('sandbox')}
|
||||
disabled={isSavingSettings}
|
||||
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-black transition-all cursor-pointer ${
|
||||
settings.boldEnvironment === 'sandbox'
|
||||
? 'bg-amber-500 text-neutral-950 shadow-md shadow-amber-900/40 scale-102'
|
||||
: 'text-neutral-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Sliders className="w-3.5 h-3.5" />
|
||||
<span>Pruebas (Sandbox)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulario rápido para Enlace Oficial Bold */}
|
||||
<form onSubmit={handleSaveBoldLink} className="grid grid-cols-1 sm:grid-cols-12 gap-3 items-end">
|
||||
<div className="sm:col-span-8 space-y-1">
|
||||
<label className="text-[11px] font-bold text-indigo-200 flex items-center justify-between">
|
||||
<span>Enlace de Checkout Global Bold (Link Oficial DripZone) *</span>
|
||||
<span className="text-[10px] text-neutral-400 font-mono">
|
||||
{settings.boldEnvironment === 'production' ? 'Cobros Reales' : 'Modo Seguro Pruebas'}
|
||||
</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="url"
|
||||
required
|
||||
value={settings.boldPaymentLink || ''}
|
||||
onChange={(e) => setSettings({ ...settings, boldPaymentLink: e.target.value })}
|
||||
placeholder="https://checkout.bold.co/payment/LNK_AJPFKOPB0C"
|
||||
className="w-full bg-neutral-950/90 border border-indigo-500/40 rounded-2xl pl-3.5 pr-10 py-2.5 text-xs text-white font-mono outline-hidden focus:border-indigo-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (settings.boldPaymentLink) {
|
||||
navigator.clipboard.writeText(settings.boldPaymentLink);
|
||||
setMessage({ type: 'success', text: 'Enlace copiado al portapapeles.' });
|
||||
}
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-white cursor-pointer"
|
||||
title="Copiar enlace"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-4 flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSavingSettings}
|
||||
className="flex-1 bg-indigo-600 hover:bg-indigo-500 text-white font-bold text-xs py-2.5 px-4 rounded-2xl flex items-center justify-center gap-1.5 transition-all cursor-pointer shadow-lg shadow-indigo-900/30 disabled:opacity-50"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
<span>{isSavingSettings ? 'Guardando...' : 'Guardar Enlace'}</span>
|
||||
</button>
|
||||
|
||||
{settings.boldPaymentLink && (
|
||||
<a
|
||||
href={settings.boldPaymentLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-neutral-800 hover:bg-neutral-700 text-indigo-300 border border-indigo-500/30 text-xs font-bold p-2.5 rounded-2xl flex items-center justify-center transition-all"
|
||||
title="Abrir y Probar Checkout Bold en vivo"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Badges de soporte oficial Bold */}
|
||||
<div className="flex flex-wrap gap-2 pt-1 border-t border-white/5 text-[11px]">
|
||||
<span className="bg-sky-950/50 text-sky-300 border border-sky-800/40 px-2.5 py-1 rounded-xl flex items-center gap-1 font-semibold">
|
||||
<Landmark className="w-3 h-3 text-sky-400" />
|
||||
<span>PSE Débito</span>
|
||||
</span>
|
||||
<span className="bg-indigo-950/50 text-indigo-300 border border-indigo-800/40 px-2.5 py-1 rounded-xl flex items-center gap-1 font-semibold">
|
||||
<CreditCard className="w-3 h-3 text-indigo-400" />
|
||||
<span>Tarjetas Visa / Mastercard / Amex</span>
|
||||
</span>
|
||||
<span className="bg-pink-950/50 text-pink-300 border border-pink-800/40 px-2.5 py-1 rounded-xl flex items-center gap-1 font-semibold">
|
||||
<Smartphone className="w-3 h-3 text-pink-400" />
|
||||
<span>Nequi en Línea</span>
|
||||
</span>
|
||||
<span className="bg-amber-950/50 text-amber-300 border border-amber-800/40 px-2.5 py-1 rounded-xl flex items-center gap-1 font-semibold">
|
||||
<Zap className="w-3 h-3 text-amber-400" />
|
||||
<span>Botón Bancolombia</span>
|
||||
</span>
|
||||
<span className="bg-emerald-950/50 text-emerald-300 border border-emerald-800/40 px-2.5 py-1 rounded-xl flex items-center gap-1 font-semibold">
|
||||
<QrCode className="w-3 h-3 text-emerald-400" />
|
||||
<span>QR Bre-B Banco de la República</span>
|
||||
</span>
|
||||
<span className="bg-purple-950/50 text-purple-300 border border-purple-800/40 px-2.5 py-1 rounded-xl flex items-center gap-1 font-semibold">
|
||||
<ShieldCheck className="w-3 h-3 text-purple-400" />
|
||||
<span>Separación $10.000 + Contraentrega</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtros / Tabs */}
|
||||
<div className="flex flex-wrap gap-2 border-b border-neutral-800 pb-3">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={`text-xs font-bold px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
activeTab === 'all'
|
||||
? 'bg-purple-600 text-white shadow-md shadow-purple-900/30'
|
||||
: 'bg-neutral-900 text-neutral-400 hover:text-white border border-neutral-800'
|
||||
}`}
|
||||
>
|
||||
<Layers className="w-3.5 h-3.5" />
|
||||
<span>Todos ({methods.length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('bold')}
|
||||
className={`text-xs font-bold px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
activeTab === 'bold'
|
||||
? 'bg-indigo-600 text-white shadow-md shadow-indigo-900/30'
|
||||
: 'bg-neutral-900 text-neutral-400 hover:text-white border border-neutral-800'
|
||||
}`}
|
||||
>
|
||||
<Zap className="w-3.5 h-3.5 text-indigo-400" />
|
||||
<span>Pasarela Bold Oficial ({methods.filter((m) => m.type === 'bold' || m.boldChannel).length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('deposit')}
|
||||
className={`text-xs font-bold px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
activeTab === 'deposit'
|
||||
? 'bg-purple-600 text-white shadow-md shadow-purple-900/30'
|
||||
: 'bg-neutral-900 text-neutral-400 hover:text-white border border-neutral-800'
|
||||
}`}
|
||||
>
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-purple-400" />
|
||||
<span>Separación & Contraentrega ({methods.filter((m) => m.requiresDeposit || m.type === 'contraentrega').length})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('direct')}
|
||||
className={`text-xs font-bold px-3.5 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||
activeTab === 'direct'
|
||||
? 'bg-purple-600 text-white shadow-md shadow-purple-900/30'
|
||||
: 'bg-neutral-900 text-neutral-400 hover:text-white border border-neutral-800'
|
||||
}`}
|
||||
>
|
||||
<Smartphone className="w-3.5 h-3.5 text-pink-400" />
|
||||
<span>Transferencias Directas ({methods.filter((m) => m.type === 'nequi' || m.type === 'transferencia' || m.type === 'daviplata').length})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Methods Grid / Cards */}
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="py-12 text-center text-xs text-neutral-400">Cargando métodos de pago desde Appwrite...</div>
|
||||
) : methods.length === 0 ? (
|
||||
<div className="py-12 text-center text-xs text-neutral-400">No hay métodos de pago registrados. Haz clic en "Nuevo Método de Pago".</div>
|
||||
<div className="py-12 text-center text-xs text-neutral-400 animate-pulse">
|
||||
Cargando métodos de pago desde Appwrite...
|
||||
</div>
|
||||
) : filteredMethods.length === 0 ? (
|
||||
<div className="py-12 text-center text-xs text-neutral-400 space-y-3 bg-neutral-900/50 border border-neutral-800 rounded-3xl p-6">
|
||||
<p>No hay métodos de pago en esta categoría.</p>
|
||||
<button
|
||||
onClick={handleSyncBoldDefaults}
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-4 py-2 rounded-xl transition-all inline-flex items-center gap-2 cursor-pointer"
|
||||
>
|
||||
<RefreshCw className="w-3.5 h-3.5" />
|
||||
<span>Restaurar Métodos Oficiales de Bold</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
methods.map((method) => (
|
||||
filteredMethods.map((method) => (
|
||||
<div
|
||||
key={method.id}
|
||||
className={`border rounded-3xl p-5 sm:p-6 space-y-4 transition-all ${
|
||||
className={`border rounded-3xl p-5 sm:p-6 space-y-4 transition-all shadow-md ${
|
||||
method.isActive
|
||||
? 'bg-neutral-900 border-neutral-800'
|
||||
: 'bg-neutral-950/60 border-neutral-900 opacity-70'
|
||||
? 'bg-neutral-900 border-neutral-800 hover:border-neutral-700'
|
||||
: 'bg-neutral-950/60 border-neutral-900 opacity-60'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-neutral-800/80 pb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 rounded-2xl bg-neutral-950 border border-neutral-800 text-purple-400 flex items-center justify-center font-bold shrink-0">
|
||||
{method.type === 'contraentrega' ? (
|
||||
<Zap className="w-5 h-5 fill-purple-400 text-purple-400" />
|
||||
) : method.type === 'bold' ? (
|
||||
<QrCode className="w-5 h-5 text-emerald-400" />
|
||||
) : (
|
||||
<CreditCard className="w-5 h-5 text-purple-400" />
|
||||
)}
|
||||
<div className="flex items-center space-x-3.5">
|
||||
<div className="w-11 h-11 rounded-2xl bg-neutral-950 border border-neutral-800 flex items-center justify-center font-bold shrink-0 shadow-inner">
|
||||
{getMethodIcon(method)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-bold text-white text-base">{method.name}</h3>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="font-black text-white text-base">{method.name}</h3>
|
||||
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{method.requiresDeposit && (
|
||||
<span className="text-[10px] bg-amber-500/20 text-amber-300 font-bold px-2 py-0.5 rounded-full border border-amber-500/30">
|
||||
<span className="text-[10px] bg-purple-500/20 text-purple-300 font-bold px-2 py-0.5 rounded-full border border-purple-500/30">
|
||||
Anticipo {formatCOP(method.depositAmount)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!method.requiresDeposit && method.type === 'bold' && (
|
||||
<span className="text-[10px] bg-emerald-500/20 text-emerald-300 font-bold px-2 py-0.5 rounded-full border border-emerald-500/30">
|
||||
100% en Línea
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-neutral-500 font-mono mt-0.5">
|
||||
<span>Tipo: {method.type}</span>
|
||||
{method.boldChannel && <span>• Canal Bold: {method.boldChannel}</span>}
|
||||
</div>
|
||||
<span className="text-xs text-neutral-500 font-mono">Tipo: {method.type}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -250,29 +803,31 @@ export default function AdminPaymentMethodsPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs">
|
||||
<div>
|
||||
<span className="text-neutral-500 block mb-0.5">Descripción para el Cliente:</span>
|
||||
<p className="text-neutral-300 font-medium">{method.description || 'Sin descripción'}</p>
|
||||
<p className="text-neutral-300 font-medium leading-relaxed">{method.description || 'Sin descripción'}</p>
|
||||
</div>
|
||||
|
||||
{method.accountInfo && (
|
||||
<div>
|
||||
<span className="text-neutral-500 block mb-0.5">Cuentas / Datos Bancarios:</span>
|
||||
<p className="text-emerald-400 font-mono font-bold">{method.accountInfo}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{method.accountInfo && (
|
||||
<div>
|
||||
<span className="text-neutral-500 block mb-0.5">Cuentas o Instrucciones:</span>
|
||||
<p className="text-emerald-400 font-mono font-bold">{method.accountInfo}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{method.boldPaymentLink && (
|
||||
<div>
|
||||
<span className="text-neutral-500 block mb-0.5">Link de Pago Bold:</span>
|
||||
<a
|
||||
href={method.boldPaymentLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-purple-400 hover:underline truncate block"
|
||||
>
|
||||
{method.boldPaymentLink}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{method.boldPaymentLink && (
|
||||
<div>
|
||||
<span className="text-neutral-500 block mb-0.5">Enlace de Pago Bold:</span>
|
||||
<a
|
||||
href={method.boldPaymentLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-400 hover:text-indigo-300 hover:underline truncate block font-mono"
|
||||
>
|
||||
{method.boldPaymentLink}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
@ -281,9 +836,9 @@ export default function AdminPaymentMethodsPage() {
|
||||
|
||||
{/* Modal para Crear / Editar Método de Pago */}
|
||||
{isEditModalOpen && editingMethod && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/85 backdrop-blur-md animate-fade-in">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl w-full max-w-lg overflow-hidden shadow-2xl">
|
||||
<div className="flex items-center justify-between p-5 border-b border-neutral-800 bg-neutral-950/50">
|
||||
<div className="flex items-center justify-between p-5 border-b border-neutral-800 bg-neutral-950/70">
|
||||
<h3 className="font-bold text-white text-sm flex items-center gap-2">
|
||||
<CreditCard className="w-4 h-4 text-purple-400" />
|
||||
<span>{editingMethod.id ? 'Editar Método de Pago' : 'Nuevo Método de Pago'}</span>
|
||||
@ -296,14 +851,34 @@ export default function AdminPaymentMethodsPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveMethod} className="p-5 space-y-4">
|
||||
<form onSubmit={handleSaveMethod} className="p-5 space-y-4 max-h-[80vh] overflow-y-auto">
|
||||
{/* Plantilla rápida de Bold */}
|
||||
{!editingMethod.id && (
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-indigo-300 mb-1">
|
||||
⚡ Selecciona un Canal Oficial de Bold (Auto-completar):
|
||||
</label>
|
||||
<select
|
||||
value={selectedPresetId}
|
||||
onChange={(e) => handleSelectPreset(e.target.value)}
|
||||
className="w-full bg-neutral-950 border border-indigo-500/40 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-indigo-400"
|
||||
>
|
||||
{BOLD_TEMPLATES.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Nombre Visible *</label>
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Nombre Visible para el Cliente *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Ej. Separación con $10.000 + Saldo Contraentrega"
|
||||
placeholder="Ej. PSE • Débito desde cualquier Banco (vía Bold)"
|
||||
value={editingMethod.name || ''}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, name: e.target.value })}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
@ -313,20 +888,49 @@ export default function AdminPaymentMethodsPage() {
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Tipo de Método</label>
|
||||
<select
|
||||
value={editingMethod.type || 'contraentrega'}
|
||||
value={editingMethod.type || 'bold'}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, type: e.target.value as any })}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
>
|
||||
<option value="contraentrega">Contraentrega (Efectivo / Flete)</option>
|
||||
<option value="bold">Bold.co (En Línea: PSE / Tarjetas / Nequi / QR)</option>
|
||||
<option value="contraentrega">Contraentrega (Con o sin anticipo)</option>
|
||||
<option value="nequi">Nequi</option>
|
||||
<option value="bancolombia">Bancolombia</option>
|
||||
<option value="daviplata">Daviplata</option>
|
||||
<option value="bancolombia">Bancolombia Transferencia</option>
|
||||
<option value="bold">Bold.co (PSE / Tarjetas / QR)</option>
|
||||
<option value="transferencia">Transferencia Bancaria General</option>
|
||||
<option value="otro">Otro Método Personalizado</option>
|
||||
<option value="otro">Otro Método</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Canal Específico Bold</label>
|
||||
<select
|
||||
value={editingMethod.boldChannel || 'all'}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, boldChannel: e.target.value as any })}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
>
|
||||
<option value="deposit">Separación / Anticipo ($10.000)</option>
|
||||
<option value="pse">PSE Débito Bancario</option>
|
||||
<option value="card">Tarjetas de Crédito / Débito</option>
|
||||
<option value="nequi">Nequi vía Bold</option>
|
||||
<option value="bancolombia">Botón Bancolombia</option>
|
||||
<option value="bre_b">Código QR Bre-B</option>
|
||||
<option value="all">Checkout Global Bold</option>
|
||||
<option value="custom">Canal Personalizado</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Badge / Etiqueta Visual</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej. Bold 🏦 PSE Débito"
|
||||
value={editingMethod.badge || ''}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, badge: e.target.value })}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Monto de Separación / Anticipo (COP)</label>
|
||||
<input
|
||||
@ -337,6 +941,7 @@ export default function AdminPaymentMethodsPage() {
|
||||
}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
/>
|
||||
<span className="text-[10px] text-neutral-500">Dejar en 0 para pago 100% anticipado o contraentrega total.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -344,7 +949,7 @@ export default function AdminPaymentMethodsPage() {
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Descripción para el Cliente</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="Ej. Separa con $10.000 COP vía Bold oficial y paga el saldo en efectivo o mediante Bold al recibir."
|
||||
placeholder="Ej. Paga de forma 100% segura con PSE desde cualquier cuenta bancaria en Colombia."
|
||||
value={editingMethod.description || ''}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, description: e.target.value })}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
@ -355,7 +960,7 @@ export default function AdminPaymentMethodsPage() {
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Cuentas o Instrucciones de Pago</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej. Pasarela Bold Oficial • PSE / Tarjetas / QR"
|
||||
placeholder="Ej. Pasarela Bold Oficial • Cifrado bancario seguro"
|
||||
value={editingMethod.accountInfo || ''}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, accountInfo: e.target.value })}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
@ -366,7 +971,7 @@ export default function AdminPaymentMethodsPage() {
|
||||
<label className="block text-[11px] font-bold text-neutral-400 mb-1">Enlace de Pago Bold.co (Opcional)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://bold.co/p/tu-enlace"
|
||||
placeholder="https://checkout.bold.co/payment/LNK_AJPFKOPB0C"
|
||||
value={editingMethod.boldPaymentLink || ''}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, boldPaymentLink: e.target.value })}
|
||||
className="w-full bg-neutral-950 border border-neutral-800 rounded-xl px-3 py-2 text-xs text-white outline-hidden focus:border-purple-500"
|
||||
@ -374,12 +979,12 @@ export default function AdminPaymentMethodsPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2 border-t border-neutral-800">
|
||||
<label className="flex items-center space-x-2 text-xs text-amber-400 font-bold cursor-pointer">
|
||||
<label className="flex items-center space-x-2 text-xs text-purple-400 font-bold cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(editingMethod.requiresDeposit)}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, requiresDeposit: e.target.checked })}
|
||||
className="rounded text-amber-500 focus:ring-0"
|
||||
className="rounded text-purple-500 focus:ring-0"
|
||||
/>
|
||||
<span>Requiere Anticipo de Separación (ej. $10.000 COP)</span>
|
||||
</label>
|
||||
@ -389,7 +994,7 @@ export default function AdminPaymentMethodsPage() {
|
||||
type="checkbox"
|
||||
checked={editingMethod.isActive !== false}
|
||||
onChange={(e) => setEditingMethod({ ...editingMethod, isActive: e.target.checked })}
|
||||
className="rounded text-purple-500 focus:ring-0"
|
||||
className="rounded text-emerald-500 focus:ring-0"
|
||||
/>
|
||||
<span>Método Habilitado en la Tienda</span>
|
||||
</label>
|
||||
@ -405,7 +1010,7 @@ export default function AdminPaymentMethodsPage() {
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer shadow-lg shadow-purple-900/30"
|
||||
className="bg-purple-600 hover:bg-purple-500 text-white font-bold text-xs px-5 py-2 rounded-xl flex items-center gap-1.5 transition-all cursor-pointer shadow-lg shadow-purple-900/40"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
<span>Guardar en Appwrite</span>
|
||||
@ -418,7 +1023,7 @@ export default function AdminPaymentMethodsPage() {
|
||||
|
||||
{/* Modal para Confirmar Eliminación */}
|
||||
{isDeleteModalOpen && methodToDelete && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in">
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/85 backdrop-blur-md animate-fade-in">
|
||||
<div className="bg-neutral-900 border border-neutral-800 rounded-3xl w-full max-w-sm overflow-hidden p-6 space-y-4 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-rose-500/20 text-rose-400 flex items-center justify-center mx-auto border border-rose-500/30">
|
||||
<AlertTriangle className="w-6 h-6" />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createBoldPayment } from '@/lib/bold-service';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
import { updateOrderInAppwrite, getOrdersFromAppwrite } from '@/lib/server-appwrite';
|
||||
import { updateOrderInAppwrite, getOrdersFromAppwrite, getSettingsFromAppwrite } from '@/lib/server-appwrite';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
@ -14,8 +14,12 @@ export async function POST(request: Request) {
|
||||
|
||||
const description = type === 'deposit'
|
||||
? `Anticipo Separacion DripZone #${orderId}`
|
||||
: type === 'full'
|
||||
? `Pago Total DripZone #${orderId}`
|
||||
: `Saldo Contraentrega DripZone #${orderId}`;
|
||||
|
||||
const settings = await getSettingsFromAppwrite();
|
||||
|
||||
const payment = await createBoldPayment({
|
||||
orderId,
|
||||
amount: Number(amount),
|
||||
@ -23,7 +27,7 @@ export async function POST(request: Request) {
|
||||
payerEmail: payerEmail || 'info@drip-zone.online',
|
||||
redirectionUrl: redirectionUrl || `https://drip-zone.online/checkout?orderId=${orderId}`,
|
||||
type,
|
||||
});
|
||||
}, settings);
|
||||
|
||||
if (!payment.success) {
|
||||
return NextResponse.json({ success: false, error: payment.error }, { status: 500 });
|
||||
|
||||
@ -4,8 +4,9 @@ import {
|
||||
createPaymentMethodInAppwrite,
|
||||
updatePaymentMethodInAppwrite,
|
||||
deletePaymentMethodInAppwrite,
|
||||
syncDefaultPaymentMethodsToAppwrite,
|
||||
} from '@/lib/server-appwrite';
|
||||
import { dataService } from '@/lib/data-service';
|
||||
import { dataService, INITIAL_PAYMENT_METHODS } from '@/lib/data-service';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@ -20,6 +21,10 @@ export async function GET() {
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (body.action === 'sync_bold_defaults') {
|
||||
const synced = await syncDefaultPaymentMethodsToAppwrite();
|
||||
return NextResponse.json({ success: true, paymentMethods: synced });
|
||||
}
|
||||
const created = await createPaymentMethodInAppwrite(body);
|
||||
return NextResponse.json({ success: true, paymentMethod: created });
|
||||
} catch (error: any) {
|
||||
|
||||
@ -216,14 +216,24 @@ export default function CheckoutPage() {
|
||||
const shippingCost = activeCity.shippingCost;
|
||||
const totalAmount = subtotal + shippingCost;
|
||||
|
||||
// Regla: Para el primer pedido se cobra separación fija ($10.000 COP) sin importar cuántos productos lleve.
|
||||
// Tras la primera compra culminada satisfactoriamente, el cliente paga 100% contraentrega.
|
||||
const isMandatoryDeposit = userRequiresDeposit;
|
||||
const depositAmount = isMandatoryDeposit ? 10000 : 0;
|
||||
// Lógica de Cobro:
|
||||
// - Si el método es Bold 100% en línea (PSE, Tarjetas, Nequi, etc. sin separación): cobra el totalAmount.
|
||||
// - Si el método requiere separación (ej. $10.000 COP) o el usuario la tiene obligatoria: cobra el anticipo ($10.000 COP) y el saldo queda contraentrega.
|
||||
// - Si es 100% contraentrega en efectivo (ej. Cali): depósito $0 y saldo total contraentrega.
|
||||
const is100PercentOnline = activePayment?.type === 'bold' && !activePayment?.requiresDeposit;
|
||||
const isSeparation = Boolean(activePayment?.requiresDeposit || userRequiresDeposit);
|
||||
const isMandatoryDeposit = isSeparation;
|
||||
|
||||
const depositAmount = is100PercentOnline
|
||||
? totalAmount
|
||||
: isSeparation
|
||||
? (activePayment?.depositAmount || 10000)
|
||||
: 0;
|
||||
|
||||
const balanceDue = Math.max(0, totalAmount - depositAmount);
|
||||
|
||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573106390296';
|
||||
const boldLink = storeSettings?.boldPaymentLink || 'https://drip-zone.online';
|
||||
const boldLink = activePayment?.boldPaymentLink || storeSettings?.boldPaymentLink || 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C';
|
||||
|
||||
const processOrderSubmission = async () => {
|
||||
setIsSubmitting(true);
|
||||
@ -261,7 +271,7 @@ export default function CheckoutPage() {
|
||||
origin: { y: 0.6 },
|
||||
});
|
||||
|
||||
// Si la orden requiere depósito, solicitar cobro en Bold inmediatamente
|
||||
// Si la orden requiere cobro en línea vía Bold (anticipo de separación o pago total 100%)
|
||||
if (depositAmount > 0) {
|
||||
setBoldLoading(true);
|
||||
fetch('/api/bold/create-payment', {
|
||||
@ -270,7 +280,7 @@ export default function CheckoutPage() {
|
||||
body: JSON.stringify({
|
||||
orderId: data.order.id,
|
||||
amount: depositAmount,
|
||||
type: 'deposit',
|
||||
type: is100PercentOnline ? 'full' : 'deposit',
|
||||
payerEmail: '',
|
||||
redirectionUrl: window.location.href,
|
||||
}),
|
||||
@ -458,7 +468,11 @@ export default function CheckoutPage() {
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-2.5">
|
||||
<div className="flex items-center space-x-2 text-white font-bold text-xs uppercase">
|
||||
<QrCode className="w-4 h-4 text-purple-400" />
|
||||
<span>Paso 2: Anticipo de Separación ($10.000 COP)</span>
|
||||
<span>
|
||||
{orderCompleted.balanceDue === 0
|
||||
? `Paso 2: Pago en Línea 100% Seguro (${formatCOP(orderCompleted.totalAmount)})`
|
||||
: `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
|
||||
@ -478,7 +492,7 @@ export default function CheckoutPage() {
|
||||
/>
|
||||
<div className="text-center space-y-0.5">
|
||||
<p className="text-xs font-black text-neutral-900">
|
||||
Total a Pagar: $10.000 COP
|
||||
Total a Pagar: {formatCOP(orderCompleted.depositAmount || (orderCompleted.balanceDue === 0 ? orderCompleted.totalAmount : 10000))} COP
|
||||
</p>
|
||||
<p className="text-[10px] font-semibold text-neutral-600">
|
||||
⚡ Compatible con Bre-B e Interoperabilidad Bancaria
|
||||
@ -500,16 +514,20 @@ export default function CheckoutPage() {
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white font-black text-xs py-3 px-4 rounded-xl flex items-center justify-center gap-2 shadow-lg transition-all"
|
||||
>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
<span>Pagar $10.000 con PSE, Tarjetas, Nequi o Bre-B</span>
|
||||
<span>
|
||||
Pagar {formatCOP(orderCompleted.depositAmount || (orderCompleted.balanceDue === 0 ? orderCompleted.totalAmount : 10000))} con PSE, Tarjetas, Nequi o Bre-B
|
||||
</span>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-white/10 flex justify-between items-center text-xs">
|
||||
<span className="text-neutral-400">🤝 Saldo restante contraentrega (en efectivo o transferencia):</span>
|
||||
<span className="text-neutral-400">🤝 Saldo restante contraentrega:</span>
|
||||
<span className="text-emerald-400 font-bold font-mono">
|
||||
{formatCOP(Math.max(0, orderCompleted.totalAmount - (orderCompleted.depositAmount || 10000)))} COP
|
||||
{orderCompleted.balanceDue === 0
|
||||
? '$0 COP (Despacho Prioritario)'
|
||||
: `${formatCOP(orderCompleted.balanceDue || Math.max(0, orderCompleted.totalAmount - (orderCompleted.depositAmount || 10000)))} COP`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -707,11 +725,21 @@ export default function CheckoutPage() {
|
||||
<span className="font-black text-white text-sm">{method.name}</span>
|
||||
</div>
|
||||
|
||||
{method.requiresDeposit && (
|
||||
<span className="bg-white/10 text-white border border-white/20 text-[10px] font-black px-2 py-0.5 rounded-full uppercase">
|
||||
Separa con $10.000
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{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}
|
||||
</span>
|
||||
) : method.requiresDeposit ? (
|
||||
<span className="bg-white/10 text-white border border-white/20 text-[10px] font-black px-2 py-0.5 rounded-full uppercase">
|
||||
Separa con {formatCOP(method.depositAmount || 10000)}
|
||||
</span>
|
||||
) : method.type === 'bold' ? (
|
||||
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-[10px] font-bold px-2 py-0.5 rounded-full uppercase">
|
||||
100% en Línea
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-neutral-400 pl-6">{method.description}</p>
|
||||
@ -719,15 +747,15 @@ export default function CheckoutPage() {
|
||||
{isSelected && (method.accountInfo || (method.type === 'bold' && boldLink)) && (
|
||||
<div className="ml-6 mt-2 bg-neutral-900 p-3 rounded-xl border border-white/10 text-xs space-y-2 text-white">
|
||||
{method.accountInfo && <p className="font-semibold">{method.accountInfo}</p>}
|
||||
{method.type === 'bold' && boldLink && (
|
||||
{method.type === 'bold' && (method.boldPaymentLink || boldLink) && (
|
||||
<a
|
||||
href={boldLink}
|
||||
href={method.boldPaymentLink || boldLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold px-3 py-1.5 rounded-lg shadow-sm"
|
||||
>
|
||||
<ExternalLink className="w-3.5 h-3.5" />
|
||||
<span>Pagar en Línea (PSE, Tarjetas o Nequi)</span>
|
||||
<span>Pagar en Línea con Bold (PSE, Tarjetas, Nequi o Bre-B)</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
* Soporta: QR Bre-B (Interoperable), PSE, Nequi, Botón Bancolombia y Tarjetas
|
||||
*/
|
||||
import crypto from 'crypto';
|
||||
import { dataService } from './data-service';
|
||||
import { StoreSettings } from '@/types';
|
||||
|
||||
export interface BoldConfig {
|
||||
environment: 'sandbox' | 'production';
|
||||
@ -11,23 +13,24 @@ export interface BoldConfig {
|
||||
apiBaseUrl: string;
|
||||
}
|
||||
|
||||
export function getBoldConfig(): BoldConfig {
|
||||
const env = (process.env.BOLD_ENVIRONMENT || 'sandbox').toLowerCase() as 'sandbox' | 'production';
|
||||
export function getBoldConfig(overrideSettings?: Partial<StoreSettings> | null): BoldConfig {
|
||||
const settings = overrideSettings || (typeof dataService?.getSettings === 'function' ? dataService.getSettings() : null);
|
||||
const env = (settings?.boldEnvironment || process.env.BOLD_ENVIRONMENT || 'production').toLowerCase() as 'sandbox' | 'production';
|
||||
|
||||
if (env === 'production') {
|
||||
return {
|
||||
environment: 'production',
|
||||
identityKey: process.env.BOLD_PROD_IDENTITY_KEY || 'xPlwpPJaT7J604ZY5upk0s9ZZu54p4UGQqrZnU0OzU0',
|
||||
secretKey: process.env.BOLD_PROD_SECRET_KEY || 'Yyzm4vZjWe_N_I_5c1g7Dw',
|
||||
identityKey: settings?.boldProdIdentityKey || process.env.BOLD_PROD_IDENTITY_KEY || 'xPlwpPJaT7J604ZY5upk0s9ZZu54p4UGQqrZnU0OzU0',
|
||||
secretKey: settings?.boldProdSecretKey || process.env.BOLD_PROD_SECRET_KEY || 'Yyzm4vZjWe_N_I_5c1g7Dw',
|
||||
apiBaseUrl: 'https://integrations.api.bold.co',
|
||||
};
|
||||
}
|
||||
|
||||
// Por defecto: Modo pruebas (Sandbox)
|
||||
// Por defecto cuando está en modo Pruebas (Sandbox)
|
||||
return {
|
||||
environment: 'sandbox',
|
||||
identityKey: process.env.BOLD_SANDBOX_IDENTITY_KEY || 'GatyXH-WVfX1pv17z8GAoNekbqps8Mdxdvj0yRg4BSk',
|
||||
secretKey: process.env.BOLD_SANDBOX_SECRET_KEY || 'BAFlCPYr6UFtQCvObJxTIA',
|
||||
identityKey: settings?.boldSandboxIdentityKey || process.env.BOLD_SANDBOX_IDENTITY_KEY || 'GatyXH-WVfX1pv17z8GAoNekbqps8Mdxdvj0yRg4BSk',
|
||||
secretKey: settings?.boldSandboxSecretKey || process.env.BOLD_SANDBOX_SECRET_KEY || 'BAFlCPYr6UFtQCvObJxTIA',
|
||||
apiBaseUrl: 'https://integrations.api.bold.co',
|
||||
};
|
||||
}
|
||||
@ -38,7 +41,7 @@ export interface CreatePaymentParams {
|
||||
description: string;
|
||||
payerEmail?: string;
|
||||
redirectionUrl?: string;
|
||||
type?: 'deposit' | 'delivery_balance';
|
||||
type?: 'deposit' | 'delivery_balance' | 'full';
|
||||
}
|
||||
|
||||
export interface BoldPaymentResult {
|
||||
@ -55,8 +58,8 @@ export interface BoldPaymentResult {
|
||||
/**
|
||||
* Crea un link de cobro en Bold con soporte para QR Bre-B y todos los métodos digitales
|
||||
*/
|
||||
export async function createBoldPayment(params: CreatePaymentParams): Promise<BoldPaymentResult> {
|
||||
const config = getBoldConfig();
|
||||
export async function createBoldPayment(params: CreatePaymentParams, overrideSettings?: Partial<StoreSettings> | null): Promise<BoldPaymentResult> {
|
||||
const config = getBoldConfig(overrideSettings);
|
||||
const { orderId, amount, description, payerEmail, redirectionUrl } = params;
|
||||
|
||||
try {
|
||||
@ -67,7 +70,7 @@ export async function createBoldPayment(params: CreatePaymentParams): Promise<Bo
|
||||
total_amount: Math.round(amount),
|
||||
tip_amount: 0,
|
||||
},
|
||||
reference: `${orderId}${params.type === 'delivery_balance' ? '-BAL' : '-DEP'}`,
|
||||
reference: `${orderId}${params.type === 'delivery_balance' ? '-BAL' : params.type === 'full' ? '-FULL' : '-DEP'}`,
|
||||
description: description || `Pago DripZone #${orderId}`,
|
||||
};
|
||||
|
||||
|
||||
@ -119,47 +119,128 @@ export const INITIAL_CITIES: City[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// Métodos de Pago configurables por el Admin
|
||||
// Métodos de Pago configurables por el Admin organizados a través de Bold.co
|
||||
export const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
|
||||
{
|
||||
id: 'pay-bold-link',
|
||||
name: 'Pago en Línea • PSE, Tarjetas Débito/Crédito o QR Bre-B',
|
||||
type: 'bold',
|
||||
description: 'Paga de forma rápida y 100% segura. Acepta transferencias PSE (todos los bancos), Tarjetas Visa/Mastercard/Amex, Nequi, Daviplata y QR Bre-B interoperable.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldPaymentLink: 'https://drip-zone.online',
|
||||
accountInfo: 'PSE (cualquier banco), Tarjetas Débito/Crédito, QR Bre-B, Nequi y Daviplata',
|
||||
id: 'pay-bold-deposit-10k',
|
||||
name: 'Separación con $10.000 (Bold) + Saldo Contraentrega',
|
||||
type: 'contraentrega',
|
||||
boldChannel: 'deposit',
|
||||
badge: 'Bold ⚡ Anticipo Oficial',
|
||||
icon: 'Zap',
|
||||
description: 'Separa tu talla con solo $10.000 COP en línea a través de Bold (PSE, tarjetas de crédito/débito, Nequi o QR Bre-B) y cancelas el saldo restante en efectivo o transferencia al recibir en tu puerta.',
|
||||
requiresDeposit: true,
|
||||
depositAmount: 10000,
|
||||
accountInfo: 'Pasarela Bold Oficial • PSE, Tarjetas Visa/Mastercard, Nequi y QR Bre-B',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-deposit-10k',
|
||||
name: 'Separación con $10.000 (PSE / Tarjeta / Nequi) + Saldo Contraentrega',
|
||||
type: 'contraentrega',
|
||||
description: 'Asegura tu talla con solo $10.000 COP en línea (PSE, tarjetas débito/crédito, Nequi o QR Bre-B) y cancelas el saldo restante en efectivo o transferencia al recibir en tu puerta.',
|
||||
requiresDeposit: true,
|
||||
depositAmount: 10000,
|
||||
accountInfo: 'PSE, Tarjetas Débito/Crédito (Visa, Mastercard), QR Bre-B interoperable',
|
||||
boldPaymentLink: 'https://drip-zone.online',
|
||||
id: 'pay-bold-pse',
|
||||
name: 'PSE • Débito desde cualquier Banco (vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'pse',
|
||||
badge: 'Bold 🏦 PSE Débito',
|
||||
icon: 'Landmark',
|
||||
description: 'Paga el 100% de tu pedido al instante debitando directamente desde tu cuenta bancaria de ahorros o corriente de cualquier banco en Colombia a través de Bold.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Débito en línea disponible para Bancolombia, Davivienda, Banco de Bogotá, BBVA y más de 20 bancos',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-cards',
|
||||
name: 'Tarjetas Débito y Crédito (Visa, Mastercard, Amex - vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'card',
|
||||
badge: 'Bold 💳 Tarjetas & Cuotas',
|
||||
icon: 'CreditCard',
|
||||
description: 'Paga 100% seguro a cuotas o de contado con tarjeta Visa, Mastercard, American Express o Diners Club respaldado con cifrado bancario de Bold.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Acepta tarjetas nacionales e internacionales a cuotas con verificación antifraude',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-nequi',
|
||||
name: 'Nequi en Línea (Pasarela Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'nequi',
|
||||
badge: 'Bold 📱 Nequi Oficial',
|
||||
icon: 'Smartphone',
|
||||
description: 'Paga directo desde tu app Nequi mediante la pasarela segura de Bold con notificación push inmediata y confirmación automática.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Nequi en línea procesado directamente por la infraestructura de Bold',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-bancolombia',
|
||||
name: 'Botón Bancolombia (Transferencia Directa vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'bancolombia',
|
||||
badge: 'Bold ⚡ Botón Bancolombia',
|
||||
icon: 'Zap',
|
||||
description: 'Transfiere en segundos desde tu aplicación Bancolombia sin digitar números de cuenta ni comprobantes manuales, debitado vía Bold.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Botón oficial Bancolombia integrado en la pasarela Bold',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-bre-b',
|
||||
name: 'Código QR Bre-B • Interoperabilidad Inmediata (vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'bre_b',
|
||||
badge: 'Bold ⚡ Bre-B Banco República',
|
||||
icon: 'QrCode',
|
||||
description: 'Escanea y paga con el nuevo sistema interoperable Bre-B del Banco de la República desde cualquier billetera digital o entidad bancaria en Colombia.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Código QR de interoperabilidad nacional procesado por Bold',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-checkout',
|
||||
name: 'Checkout Global Bold • Todos los Métodos Digitales',
|
||||
type: 'bold',
|
||||
boldChannel: 'all',
|
||||
badge: 'Bold 🌐 Checkout Integral',
|
||||
icon: 'ShieldCheck',
|
||||
description: 'Pasarela integral de Bold donde el cliente puede elegir entre PSE, Tarjetas Débito/Crédito, Nequi, Botón Bancolombia o QR Bre-B en una sola pantalla.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Pasarela Oficial Bold.co • Enlace seguro con cifrado bancario',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-cod-full',
|
||||
name: 'Pago 100% Contraentrega (Cali y Alrededores)',
|
||||
name: 'Pago 100% Contraentrega en Efectivo (Cali y Alrededores)',
|
||||
type: 'contraentrega',
|
||||
description: 'Pagas el 100% del valor en efectivo o transferencia al recibir en tu puerta (válido para entregas urbanas en 24 horas).',
|
||||
badge: '🤝 Entrega en Puerta',
|
||||
icon: 'Truck',
|
||||
description: 'Pagas el 100% del valor en efectivo o transferencia directa al mensajero al momento de recibir en tu puerta (válido para entregas urbanas en 24 horas).',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Entrega urbana express • Cancelas al recibir',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-nequi-full',
|
||||
name: 'Transferencia Anticipada',
|
||||
type: 'bold',
|
||||
description: 'Paga anticipadamente en línea con PSE, Tarjeta de Crédito/Débito o Nequi.',
|
||||
id: 'pay-direct-nequi-daviplata',
|
||||
name: 'Transferencia Directa Nequi / Daviplata (Carlos Lasso)',
|
||||
type: 'nequi',
|
||||
badge: '📱 Transferencia Directa',
|
||||
icon: 'Smartphone',
|
||||
description: 'Transferencia manual directa a Nequi o Daviplata 316 745 5877 para clientes que prefieren pagar directo al titular.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'PSE, Tarjetas y transferencias bancarias',
|
||||
accountInfo: 'Nequi / Daviplata: 316 745 5877 • Titular: Carlos Andrés Lasso',
|
||||
isActive: false,
|
||||
},
|
||||
];
|
||||
@ -324,11 +405,16 @@ export const DEFAULT_STORE_SETTINGS: StoreSettings = {
|
||||
nequiAccount: '310 639 0296',
|
||||
daviplataAccount: '310 639 0296',
|
||||
accountHolder: 'DripZone Oficial',
|
||||
boldPaymentLink: 'https://drip-zone.online',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
boldApiKey: '',
|
||||
boldMerchantId: '',
|
||||
boldQrUrl: '',
|
||||
boldEnabled: true,
|
||||
boldEnvironment: 'production',
|
||||
boldSandboxIdentityKey: 'GatyXH-WVfX1pv17z8GAoNekbqps8Mdxdvj0yRg4BSk',
|
||||
boldSandboxSecretKey: 'BAFlCPYr6UFtQCvObJxTIA',
|
||||
boldProdIdentityKey: 'xPlwpPJaT7J604ZY5upk0s9ZZu54p4UGQqrZnU0OzU0',
|
||||
boldProdSecretKey: 'Yyzm4vZjWe_N_I_5c1g7Dw',
|
||||
defaultProfitMargin: 35,
|
||||
defaultDepositAmount: 10000,
|
||||
autoPublishSupplierPosts: true,
|
||||
|
||||
@ -129,18 +129,36 @@ export async function getPaymentMethodsFromAppwrite(): Promise<PaymentMethod[]>
|
||||
Query.limit(100),
|
||||
]);
|
||||
if (res.documents && res.documents.length > 0) {
|
||||
return res.documents.map((d: any) => ({
|
||||
id: d.$id,
|
||||
name: d.name || '',
|
||||
type: d.type || 'contraentrega',
|
||||
description: d.description || '',
|
||||
requiresDeposit: Boolean(d.requiresDeposit),
|
||||
depositAmount: typeof d.depositAmount === 'number' ? d.depositAmount : 0,
|
||||
accountInfo: d.accountInfo || '',
|
||||
boldPaymentLink: d.boldPaymentLink || '',
|
||||
boldQrUrl: d.boldQrUrl || '',
|
||||
isActive: d.isActive !== false,
|
||||
}));
|
||||
return res.documents.map((d: any) => {
|
||||
let boldChannel = d.type === 'bold' ? 'all' : undefined;
|
||||
let badge = '';
|
||||
let icon = '';
|
||||
let accountInfo = d.accountInfo || '';
|
||||
if (typeof accountInfo === 'string' && accountInfo.startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(accountInfo);
|
||||
boldChannel = parsed.boldChannel || boldChannel;
|
||||
badge = parsed.badge || '';
|
||||
icon = parsed.icon || '';
|
||||
accountInfo = parsed.info || '';
|
||||
} catch (_) {}
|
||||
}
|
||||
return {
|
||||
id: d.$id,
|
||||
name: d.name || '',
|
||||
type: d.type || 'contraentrega',
|
||||
boldChannel: boldChannel as PaymentMethod['boldChannel'],
|
||||
badge: badge || '',
|
||||
icon: icon || '',
|
||||
description: d.description || '',
|
||||
requiresDeposit: Boolean(d.requiresDeposit),
|
||||
depositAmount: typeof d.depositAmount === 'number' ? d.depositAmount : 0,
|
||||
accountInfo,
|
||||
boldPaymentLink: d.boldPaymentLink || '',
|
||||
boldQrUrl: d.boldQrUrl || '',
|
||||
isActive: d.isActive !== false,
|
||||
};
|
||||
});
|
||||
}
|
||||
return INITIAL_PAYMENT_METHODS;
|
||||
} catch (e: any) {
|
||||
@ -151,13 +169,19 @@ export async function getPaymentMethodsFromAppwrite(): Promise<PaymentMethod[]>
|
||||
|
||||
export async function createPaymentMethodInAppwrite(method: Partial<PaymentMethod>): Promise<PaymentMethod> {
|
||||
const newId = method.id || `pay-${Date.now()}`;
|
||||
const payload = {
|
||||
const meta = {
|
||||
boldChannel: method.boldChannel || (method.type === 'bold' ? 'all' : 'deposit'),
|
||||
badge: method.badge || '',
|
||||
icon: method.icon || '',
|
||||
info: method.accountInfo || '',
|
||||
};
|
||||
const payload: Record<string, any> = {
|
||||
name: method.name || 'Nuevo Método de Pago',
|
||||
type: method.type || 'contraentrega',
|
||||
description: method.description || '',
|
||||
requiresDeposit: Boolean(method.requiresDeposit),
|
||||
depositAmount: method.depositAmount || 0,
|
||||
accountInfo: method.accountInfo || '',
|
||||
accountInfo: JSON.stringify(meta),
|
||||
boldPaymentLink: method.boldPaymentLink || '',
|
||||
boldQrUrl: method.boldQrUrl || '',
|
||||
isActive: method.isActive !== false,
|
||||
@ -165,22 +189,52 @@ export async function createPaymentMethodInAppwrite(method: Partial<PaymentMetho
|
||||
|
||||
try {
|
||||
const doc = await serverDatabases.createDocument(DB_ID, 'payment_methods', newId, payload);
|
||||
return { id: doc.$id, ...payload };
|
||||
return {
|
||||
id: doc.$id,
|
||||
name: doc.name,
|
||||
type: doc.type,
|
||||
boldChannel: meta.boldChannel as any,
|
||||
badge: meta.badge,
|
||||
icon: meta.icon,
|
||||
description: doc.description,
|
||||
requiresDeposit: doc.requiresDeposit,
|
||||
depositAmount: doc.depositAmount,
|
||||
accountInfo: meta.info,
|
||||
boldPaymentLink: doc.boldPaymentLink,
|
||||
boldQrUrl: doc.boldQrUrl,
|
||||
isActive: doc.isActive,
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error('Error createPaymentMethodInAppwrite:', e.message);
|
||||
return { id: newId, ...payload };
|
||||
return { id: newId, ...method } as PaymentMethod;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePaymentMethodInAppwrite(id: string, updates: Partial<PaymentMethod>): Promise<PaymentMethod | null> {
|
||||
try {
|
||||
// Si viene actualización de metadatos o info, obtener actual para merge
|
||||
let currentMeta: Record<string, any> = {};
|
||||
try {
|
||||
const currentDoc = await serverDatabases.getDocument(DB_ID, 'payment_methods', id);
|
||||
if (currentDoc.accountInfo && currentDoc.accountInfo.startsWith('{')) {
|
||||
currentMeta = JSON.parse(currentDoc.accountInfo);
|
||||
} else {
|
||||
currentMeta = { info: currentDoc.accountInfo || '' };
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (updates.boldChannel !== undefined) currentMeta.boldChannel = updates.boldChannel;
|
||||
if (updates.badge !== undefined) currentMeta.badge = updates.badge;
|
||||
if (updates.icon !== undefined) currentMeta.icon = updates.icon;
|
||||
if (updates.accountInfo !== undefined) currentMeta.info = updates.accountInfo;
|
||||
|
||||
const cleanUpdates: Record<string, any> = {};
|
||||
if (updates.name !== undefined) cleanUpdates.name = updates.name;
|
||||
if (updates.type !== undefined) cleanUpdates.type = updates.type;
|
||||
if (updates.description !== undefined) cleanUpdates.description = updates.description;
|
||||
if (updates.requiresDeposit !== undefined) cleanUpdates.requiresDeposit = updates.requiresDeposit;
|
||||
if (updates.depositAmount !== undefined) cleanUpdates.depositAmount = updates.depositAmount;
|
||||
if (updates.accountInfo !== undefined) cleanUpdates.accountInfo = updates.accountInfo;
|
||||
cleanUpdates.accountInfo = JSON.stringify(currentMeta);
|
||||
if (updates.boldPaymentLink !== undefined) cleanUpdates.boldPaymentLink = updates.boldPaymentLink;
|
||||
if (updates.boldQrUrl !== undefined) cleanUpdates.boldQrUrl = updates.boldQrUrl;
|
||||
if (updates.isActive !== undefined) cleanUpdates.isActive = updates.isActive;
|
||||
@ -190,10 +244,13 @@ export async function updatePaymentMethodInAppwrite(id: string, updates: Partial
|
||||
id: doc.$id,
|
||||
name: doc.name,
|
||||
type: doc.type,
|
||||
boldChannel: currentMeta.boldChannel as PaymentMethod['boldChannel'],
|
||||
badge: currentMeta.badge,
|
||||
icon: currentMeta.icon,
|
||||
description: doc.description,
|
||||
requiresDeposit: doc.requiresDeposit,
|
||||
depositAmount: doc.depositAmount,
|
||||
accountInfo: doc.accountInfo,
|
||||
accountInfo: currentMeta.info,
|
||||
boldPaymentLink: doc.boldPaymentLink,
|
||||
boldQrUrl: doc.boldQrUrl,
|
||||
isActive: doc.isActive,
|
||||
@ -214,6 +271,61 @@ export async function deletePaymentMethodInAppwrite(id: string): Promise<boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincroniza o restaura los métodos de pago oficiales de Bold en Appwrite
|
||||
*/
|
||||
export async function syncDefaultPaymentMethodsToAppwrite(): Promise<PaymentMethod[]> {
|
||||
try {
|
||||
// 1. Obtener existentes en Appwrite
|
||||
const existing = await serverDatabases.listDocuments(DB_ID, 'payment_methods', [Query.limit(100)]);
|
||||
for (const doc of existing.documents) {
|
||||
try {
|
||||
await serverDatabases.deleteDocument(DB_ID, 'payment_methods', doc.$id);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 2. Insertar los métodos oficiales organizados de Bold
|
||||
const synced: PaymentMethod[] = [];
|
||||
for (const m of INITIAL_PAYMENT_METHODS) {
|
||||
const meta = {
|
||||
boldChannel: m.boldChannel || 'all',
|
||||
badge: m.badge || '',
|
||||
icon: m.icon || '',
|
||||
info: m.accountInfo || '',
|
||||
};
|
||||
const payload: Record<string, any> = {
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
description: m.description || '',
|
||||
requiresDeposit: Boolean(m.requiresDeposit),
|
||||
depositAmount: m.depositAmount || 0,
|
||||
accountInfo: JSON.stringify(meta),
|
||||
boldPaymentLink: m.boldPaymentLink || '',
|
||||
boldQrUrl: m.boldQrUrl || '',
|
||||
isActive: m.isActive !== false,
|
||||
};
|
||||
try {
|
||||
const created = await serverDatabases.createDocument(DB_ID, 'payment_methods', m.id, payload);
|
||||
synced.push({
|
||||
id: created.$id,
|
||||
...payload,
|
||||
boldChannel: meta.boldChannel,
|
||||
badge: meta.badge,
|
||||
icon: meta.icon,
|
||||
accountInfo: meta.info,
|
||||
} as PaymentMethod);
|
||||
} catch (err) {
|
||||
console.warn(`Error al insertar ${m.id} en Appwrite:`, err);
|
||||
synced.push(m);
|
||||
}
|
||||
}
|
||||
return synced;
|
||||
} catch (err: any) {
|
||||
console.error('Error syncDefaultPaymentMethodsToAppwrite:', err);
|
||||
return INITIAL_PAYMENT_METHODS;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. PEDIDOS & LOGÍSTICA (CRUD PERSISTENTE EN APPWRITE)
|
||||
// =========================================================================
|
||||
@ -442,6 +554,21 @@ export async function deleteSupplierInAppwrite(id: string): Promise<boolean> {
|
||||
export async function getSettingsFromAppwrite(): Promise<StoreSettings | null> {
|
||||
try {
|
||||
const doc = await serverDatabases.getDocument(DB_ID, 'store_settings', 'main_settings');
|
||||
let boldSandboxIdentityKey = '';
|
||||
let boldSandboxSecretKey = '';
|
||||
let boldProdIdentityKey = '';
|
||||
let boldProdSecretKey = '';
|
||||
|
||||
if (doc.boldApiKey && typeof doc.boldApiKey === 'string' && doc.boldApiKey.startsWith('{')) {
|
||||
try {
|
||||
const parsedKeys = JSON.parse(doc.boldApiKey);
|
||||
boldSandboxIdentityKey = parsedKeys.boldSandboxIdentityKey || '';
|
||||
boldSandboxSecretKey = parsedKeys.boldSandboxSecretKey || '';
|
||||
boldProdIdentityKey = parsedKeys.boldProdIdentityKey || '';
|
||||
boldProdSecretKey = parsedKeys.boldProdSecretKey || '';
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return {
|
||||
storeName: doc.storeName || 'DripZone',
|
||||
slogan: doc.slogan || '',
|
||||
@ -459,6 +586,11 @@ export async function getSettingsFromAppwrite(): Promise<StoreSettings | null> {
|
||||
boldMerchantId: doc.boldMerchantId || '',
|
||||
boldQrUrl: doc.boldQrUrl || '',
|
||||
boldEnabled: doc.boldEnabled !== false,
|
||||
boldEnvironment: (doc.boldMerchantId === 'sandbox' ? 'sandbox' : 'production'),
|
||||
boldSandboxIdentityKey,
|
||||
boldSandboxSecretKey,
|
||||
boldProdIdentityKey,
|
||||
boldProdSecretKey,
|
||||
defaultProfitMargin: doc.defaultProfitMargin || 35,
|
||||
defaultDepositAmount: doc.defaultDepositAmount || 10000,
|
||||
};
|
||||
@ -469,7 +601,45 @@ export async function getSettingsFromAppwrite(): Promise<StoreSettings | null> {
|
||||
|
||||
export async function updateSettingsInAppwrite(settings: Partial<StoreSettings>): Promise<StoreSettings> {
|
||||
try {
|
||||
const doc = await serverDatabases.updateDocument(DB_ID, 'store_settings', 'main_settings', settings);
|
||||
const cleanUpdates: Record<string, any> = {};
|
||||
if (settings.storeName !== undefined) cleanUpdates.storeName = settings.storeName;
|
||||
if (settings.slogan !== undefined) cleanUpdates.slogan = settings.slogan;
|
||||
if (settings.email !== undefined) cleanUpdates.email = settings.email;
|
||||
if (settings.phone !== undefined) cleanUpdates.phone = settings.phone;
|
||||
if (settings.whatsappNumber !== undefined) cleanUpdates.whatsappNumber = settings.whatsappNumber;
|
||||
if (settings.instagramHandle !== undefined) cleanUpdates.instagramHandle = settings.instagramHandle;
|
||||
if (settings.address !== undefined) cleanUpdates.address = settings.address;
|
||||
if (settings.city !== undefined) cleanUpdates.city = settings.city;
|
||||
if (settings.nequiAccount !== undefined) cleanUpdates.nequiAccount = settings.nequiAccount;
|
||||
if (settings.daviplataAccount !== undefined) cleanUpdates.daviplataAccount = settings.daviplataAccount;
|
||||
if (settings.accountHolder !== undefined) cleanUpdates.accountHolder = settings.accountHolder;
|
||||
if (settings.boldPaymentLink !== undefined) cleanUpdates.boldPaymentLink = settings.boldPaymentLink;
|
||||
if (settings.boldQrUrl !== undefined) cleanUpdates.boldQrUrl = settings.boldQrUrl;
|
||||
if (settings.boldEnabled !== undefined) cleanUpdates.boldEnabled = settings.boldEnabled;
|
||||
if (settings.defaultProfitMargin !== undefined) cleanUpdates.defaultProfitMargin = settings.defaultProfitMargin;
|
||||
if (settings.defaultDepositAmount !== undefined) cleanUpdates.defaultDepositAmount = settings.defaultDepositAmount;
|
||||
|
||||
// Almacenar el entorno (production | sandbox) en boldMerchantId
|
||||
if (settings.boldEnvironment !== undefined) {
|
||||
cleanUpdates.boldMerchantId = settings.boldEnvironment;
|
||||
}
|
||||
|
||||
// Almacenar claves API en boldApiKey como JSON
|
||||
if (
|
||||
settings.boldSandboxIdentityKey !== undefined ||
|
||||
settings.boldSandboxSecretKey !== undefined ||
|
||||
settings.boldProdIdentityKey !== undefined ||
|
||||
settings.boldProdSecretKey !== undefined
|
||||
) {
|
||||
cleanUpdates.boldApiKey = JSON.stringify({
|
||||
boldSandboxIdentityKey: settings.boldSandboxIdentityKey || '',
|
||||
boldSandboxSecretKey: settings.boldSandboxSecretKey || '',
|
||||
boldProdIdentityKey: settings.boldProdIdentityKey || '',
|
||||
boldProdSecretKey: settings.boldProdSecretKey || '',
|
||||
});
|
||||
}
|
||||
|
||||
const doc = await serverDatabases.updateDocument(DB_ID, 'store_settings', 'main_settings', cleanUpdates);
|
||||
return {
|
||||
storeName: doc.storeName,
|
||||
slogan: doc.slogan,
|
||||
@ -487,10 +657,16 @@ export async function updateSettingsInAppwrite(settings: Partial<StoreSettings>)
|
||||
boldMerchantId: doc.boldMerchantId,
|
||||
boldQrUrl: doc.boldQrUrl,
|
||||
boldEnabled: doc.boldEnabled,
|
||||
boldEnvironment: doc.boldMerchantId === 'sandbox' ? 'sandbox' : 'production',
|
||||
boldSandboxIdentityKey: settings.boldSandboxIdentityKey,
|
||||
boldSandboxSecretKey: settings.boldSandboxSecretKey,
|
||||
boldProdIdentityKey: settings.boldProdIdentityKey,
|
||||
boldProdSecretKey: settings.boldProdSecretKey,
|
||||
defaultProfitMargin: doc.defaultProfitMargin,
|
||||
defaultDepositAmount: doc.defaultDepositAmount,
|
||||
};
|
||||
} catch (e) {
|
||||
} catch (e: any) {
|
||||
console.error('Error updateSettingsInAppwrite:', e.message);
|
||||
return settings as StoreSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,6 +107,9 @@ export interface PaymentMethod {
|
||||
name: string;
|
||||
code?: string;
|
||||
type: 'contraentrega' | 'nequi' | 'daviplata' | 'bancolombia' | 'tarjeta' | 'bold' | 'transferencia' | 'efectivo' | 'otro';
|
||||
boldChannel?: 'deposit' | 'pse' | 'card' | 'nequi' | 'bancolombia' | 'bre_b' | 'all' | 'custom';
|
||||
badge?: string;
|
||||
icon?: string;
|
||||
description: string;
|
||||
requiresDeposit: boolean;
|
||||
depositAmount: number;
|
||||
@ -219,6 +222,11 @@ export interface StoreSettings {
|
||||
boldMerchantId?: string;
|
||||
boldQrUrl?: string;
|
||||
boldEnabled?: boolean;
|
||||
boldEnvironment?: 'sandbox' | 'production';
|
||||
boldSandboxIdentityKey?: string;
|
||||
boldSandboxSecretKey?: string;
|
||||
boldProdIdentityKey?: string;
|
||||
boldProdSecretKey?: string;
|
||||
autoPublishSupplierPosts?: boolean;
|
||||
defaultProfitMargin: number;
|
||||
defaultDepositAmount: number;
|
||||
|
||||
19
scripts/check_appwrite_docker.py
Normal file
19
scripts/check_appwrite_docker.py
Normal file
@ -0,0 +1,19 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('161.97.93.156', username='carlos', password='Innova1978+*?', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('docker ps --filter "name=appwrite" --format "{{.Names}}"')
|
||||
names = stdout.read().decode('utf-8', errors='replace').strip().splitlines()
|
||||
print("Appwrite containers:", names)
|
||||
|
||||
# Find workers or database container
|
||||
for name in names:
|
||||
if 'worker' in name or 'database' in name or 'mariadb' in name:
|
||||
print(f"\n--- Logs from {name} ---")
|
||||
_, out, err = ssh.exec_command(f"docker logs {name} --tail 20")
|
||||
print(out.read().decode('utf-8', errors='replace'))
|
||||
print(err.read().decode('utf-8', errors='replace'))
|
||||
|
||||
ssh.close()
|
||||
19
scripts/check_worker_logs.py
Normal file
19
scripts/check_worker_logs.py
Normal file
@ -0,0 +1,19 @@
|
||||
import paramiko
|
||||
import sys
|
||||
import io
|
||||
|
||||
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')
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('161.97.93.156', username='carlos', password='Innova1978+*?', timeout=10)
|
||||
|
||||
container = 'appwrite-worker-databases-sss6crntvrcs54qbhvhy5728'
|
||||
print(f"Fetching logs for {container}...")
|
||||
_, stdout, stderr = ssh.exec_command(f'docker logs {container} --tail 50')
|
||||
print(stdout.read().decode('utf-8', errors='replace'))
|
||||
print(stderr.read().decode('utf-8', errors='replace'))
|
||||
|
||||
ssh.close()
|
||||
30
scripts/clean_failed_attributes.js
Normal file
30
scripts/clean_failed_attributes.js
Normal file
@ -0,0 +1,30 @@
|
||||
const sdk = require('node-appwrite');
|
||||
|
||||
const client = new sdk.Client()
|
||||
.setEndpoint('https://appwrite.almeira.com.co/v1')
|
||||
.setProject('69b84df5003bd8957c5c')
|
||||
.setKey('standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3');
|
||||
|
||||
const dbs = new sdk.Databases(client);
|
||||
const DB_ID = 'sneakers_multistore_db';
|
||||
|
||||
async function clean() {
|
||||
const colls = ['payment_methods', 'store_settings'];
|
||||
for (const coll of colls) {
|
||||
console.log(`Checking attributes for ${coll}...`);
|
||||
const res = await dbs.listAttributes(DB_ID, coll);
|
||||
for (const a of res.attributes) {
|
||||
if (a.status === 'failed') {
|
||||
console.log(`Deleting failed attribute: ${coll}.${a.key}`);
|
||||
try {
|
||||
await dbs.deleteAttribute(DB_ID, coll, a.key);
|
||||
console.log(` -> Deleted ${a.key}`);
|
||||
} catch (e) {
|
||||
console.error(` -> Failed to delete ${a.key}:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clean().catch(console.error);
|
||||
28
scripts/inspect_coll.js
Normal file
28
scripts/inspect_coll.js
Normal file
@ -0,0 +1,28 @@
|
||||
const sdk = require('node-appwrite');
|
||||
|
||||
const client = new sdk.Client()
|
||||
.setEndpoint('https://appwrite.almeira.com.co/v1')
|
||||
.setProject('69b84df5003bd8957c5c')
|
||||
.setKey('standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3');
|
||||
|
||||
const dbs = new sdk.Databases(client);
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const res = await dbs.listAttributes('sneakers_multistore_db', 'payment_methods');
|
||||
console.log('Attributes in payment_methods:');
|
||||
for (const a of res.attributes) {
|
||||
console.log(`- ${a.key} (${a.type}, status: ${a.status}, required: ${a.required}, default: ${a.default})`);
|
||||
}
|
||||
|
||||
const res2 = await dbs.listAttributes('sneakers_multistore_db', 'store_settings');
|
||||
console.log('\nAttributes in store_settings:');
|
||||
for (const a of res2.attributes) {
|
||||
console.log(`- ${a.key} (${a.type}, status: ${a.status}, required: ${a.required}, default: ${a.default})`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
192
scripts/sync_payment_methods.js
Normal file
192
scripts/sync_payment_methods.js
Normal file
@ -0,0 +1,192 @@
|
||||
const { Client, Databases, Query } = require('node-appwrite');
|
||||
|
||||
const client = new Client()
|
||||
.setEndpoint('https://appwrite.almeira.com.co/v1')
|
||||
.setProject('69b84df5003bd8957c5c')
|
||||
.setKey('standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3');
|
||||
|
||||
const databases = new Databases(client);
|
||||
const DB_ID = 'sneakers_multistore_db';
|
||||
const COLL_ID = 'payment_methods';
|
||||
|
||||
const INITIAL_PAYMENT_METHODS = [
|
||||
{
|
||||
id: 'pay-bold-deposit-10k',
|
||||
name: 'Separación con $10.000 COP + Saldo Contraentrega',
|
||||
type: 'bold',
|
||||
boldChannel: 'deposit',
|
||||
badge: 'Bold 💳 PSE / Nequi / Tarjeta ($10.000)',
|
||||
icon: 'ShieldCheck',
|
||||
description: 'Asegura tu pedido pagando solo $10.000 COP de anticipo vía Bold (PSE, Nequi o Tarjetas). El saldo restante lo pagas en efectivo o transferencia cuando recibas el paquete en tu puerta.',
|
||||
requiresDeposit: true,
|
||||
depositAmount: 10000,
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
boldQrUrl: 'https://storage.almeira.com.co/v1/storage/buckets/products-images/files/bold-qr-bre-b/view?project=6761c57300305a415ff6',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-pse',
|
||||
name: 'PSE • Débito desde cualquier banco (vía Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'pse',
|
||||
badge: 'Bold 🏦 PSE Directo',
|
||||
icon: 'Landmark',
|
||||
description: 'Paga de forma inmediata con débito directo desde tu cuenta bancaria (Bancolombia, Davivienda, BBVA, Banco de Bogotá, Nequi, etc.) a través de la pasarela oficial Bold.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-cards',
|
||||
name: 'Tarjetas Débito y Crédito (Visa, Mastercard, Amex)',
|
||||
type: 'bold',
|
||||
boldChannel: 'card',
|
||||
badge: 'Bold 💳 Tarjetas (Cuotas)',
|
||||
icon: 'CreditCard',
|
||||
description: 'Paga con tarjeta de crédito (hasta 36 cuotas) o débito con CVV de cualquier franquicia nacional o internacional mediante el checkout seguro de Bold.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-nequi',
|
||||
name: 'Nequi en Línea (Pasarela Bold)',
|
||||
type: 'bold',
|
||||
boldChannel: 'nequi',
|
||||
badge: 'Bold 📱 Nequi en Línea',
|
||||
icon: 'Smartphone',
|
||||
description: 'Paga directamente desde tu app Nequi confirmando la notificación push o con tu clave dinámica a través de Bold.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-bancolombia',
|
||||
name: 'Botón Bancolombia (Transferencia Directa)',
|
||||
type: 'bold',
|
||||
boldChannel: 'bancolombia',
|
||||
badge: 'Bold 🟡 Botón Bancolombia',
|
||||
icon: 'Wallet',
|
||||
description: 'Transfiere al instante desde tu app Bancolombia o A la Mano sin comisiones adicionales vía Bold.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-bre-b',
|
||||
name: 'Código QR Bre-B • Interoperabilidad Inmediata',
|
||||
type: 'bold',
|
||||
boldChannel: 'bre_b',
|
||||
badge: 'Bold ⚡ QR Bre-B',
|
||||
icon: 'QrCode',
|
||||
description: 'Escanea el código QR oficial de Bold compatible con la red nacional Bre-B desde cualquier entidad financiera aliada para pago instantáneo.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldQrUrl: 'https://storage.almeira.com.co/v1/storage/buckets/products-images/files/bold-qr-bre-b/view?project=6761c57300305a415ff6',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-bold-checkout',
|
||||
name: 'Checkout Global Bold • Todos los Métodos Digitales',
|
||||
type: 'bold',
|
||||
boldChannel: 'all',
|
||||
badge: 'Bold 🛡️ Todos los Métodos',
|
||||
icon: 'Zap',
|
||||
description: 'Accede al portal completo de Bold donde podrás escoger entre PSE, Tarjetas, Nequi, Botón Bancolombia o efectivo en puntos autorizados.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C',
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-cod-full',
|
||||
name: 'Pago 100% Contraentrega en Efectivo',
|
||||
type: 'contraentrega',
|
||||
boldChannel: 'custom',
|
||||
badge: '💵 Contraentrega Cali',
|
||||
icon: 'Banknote',
|
||||
description: 'Paga el valor total en efectivo al domiciliario al momento de recibir tu pedido. Disponible en Cali y cobertura local.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 'pay-direct-nequi-daviplata',
|
||||
name: 'Transferencia Directa Nequi / Daviplata',
|
||||
type: 'transferencia',
|
||||
boldChannel: 'custom',
|
||||
badge: '📲 Transferencia Directa',
|
||||
icon: 'Send',
|
||||
description: 'Transfiere directamente a las cuentas oficiales de la tienda y envía tu comprobante para despacho prioritario.',
|
||||
requiresDeposit: false,
|
||||
depositAmount: 0,
|
||||
accountInfo: 'Nequi / Daviplata: 316 745 5877 • Titular: Carlos Lasso',
|
||||
isActive: true,
|
||||
}
|
||||
];
|
||||
|
||||
async function sync() {
|
||||
console.log('[1] Obteniendo metodos existentes en Appwrite...');
|
||||
const existing = await databases.listDocuments(DB_ID, COLL_ID, [Query.limit(100)]);
|
||||
console.log(`Encontrados ${existing.documents.length} metodos existentes.`);
|
||||
|
||||
for (const doc of existing.documents) {
|
||||
try {
|
||||
await databases.deleteDocument(DB_ID, COLL_ID, doc.$id);
|
||||
console.log(` - Eliminado: ${doc.$id}`);
|
||||
} catch (e) {
|
||||
console.warn(` - Error eliminando ${doc.$id}:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[2] Insertando metodos organizados por canales Bold...');
|
||||
for (const m of INITIAL_PAYMENT_METHODS) {
|
||||
const metaPayload = {
|
||||
boldChannel: m.boldChannel,
|
||||
badge: m.badge,
|
||||
icon: m.icon,
|
||||
info: m.accountInfo || ''
|
||||
};
|
||||
const payload = {
|
||||
name: m.name,
|
||||
type: m.type,
|
||||
description: m.description,
|
||||
requiresDeposit: m.requiresDeposit,
|
||||
depositAmount: m.depositAmount,
|
||||
accountInfo: JSON.stringify(metaPayload),
|
||||
boldPaymentLink: m.boldPaymentLink || '',
|
||||
boldQrUrl: m.boldQrUrl || '',
|
||||
isActive: m.isActive
|
||||
};
|
||||
try {
|
||||
await databases.createDocument(DB_ID, COLL_ID, m.id, payload);
|
||||
console.log(` + Creado con exito: [${m.id}] ${m.name}`);
|
||||
} catch (e) {
|
||||
console.error(` x Error creando [${m.id}]:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n[3] Verificando store_settings (modo produccion/pruebas)...');
|
||||
const SETTINGS_COLL = 'store_settings';
|
||||
const settingsList = await databases.listDocuments(DB_ID, SETTINGS_COLL, [Query.limit(5)]);
|
||||
if (settingsList.documents.length > 0) {
|
||||
const sDoc = settingsList.documents[0];
|
||||
console.log(`Configuracion actual: ID=${sDoc.$id}, boldMerchantId=${sDoc.boldMerchantId}, boldPaymentLink=${sDoc.boldPaymentLink}`);
|
||||
|
||||
// Guardar boldEnvironment en boldMerchantId y API keys en boldApiKey
|
||||
await databases.updateDocument(DB_ID, SETTINGS_COLL, sDoc.$id, {
|
||||
boldMerchantId: 'production',
|
||||
boldPaymentLink: 'https://checkout.bold.co/payment/LNK_AJPFKOPB0C'
|
||||
});
|
||||
console.log(' -> Actualizado store_settings con boldMerchantId: production y boldPaymentLink oficial.');
|
||||
}
|
||||
|
||||
console.log('\n✅ SINCRONIZACION COMPLETADA EXITOSAMENTE!');
|
||||
}
|
||||
|
||||
sync().catch(console.error);
|
||||
Loading…
Reference in New Issue
Block a user