import { Client, Databases, ID, Query } from 'node-appwrite'; import { City, PaymentMethod, Order, Product, Supplier, StoreSettings, SupplierPost, MonitoredWhatsAppGroup, MessageTemplate, CustomProductRequest, Coupon, } from '@/types'; import { INITIAL_CITIES, INITIAL_PAYMENT_METHODS, INITIAL_SUPPLIERS, isGroupIgnoredOrBlacklisted } from './data-service'; import { DEFAULT_MESSAGE_TEMPLATES } from './templates-service'; import { sanitizePublicationTitle, sanitizePublicationText } from './sanitizer'; const endpoint = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1'; const projectId = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c'; const apiKey = process.env.APPWRITE_API_KEY || 'standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3'; const databaseId = process.env.NEXT_PUBLIC_APPWRITE_DATABASE_ID || 'sneakers_multistore_db'; const client = new Client() .setEndpoint(endpoint) .setProject(projectId) .setKey(apiKey); export const serverDatabases = new Databases(client); export const DB_ID = databaseId; // ========================================================================= // 1. CIUDADES Y FLETES (CRUD PERSISTENTE EN APPWRITE) // ========================================================================= export async function getCitiesFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'cities', [ Query.limit(100), Query.orderAsc('name'), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => ({ id: d.$id, name: d.name || '', department: d.department || 'Colombia', shippingCost: typeof d.shippingCost === 'number' ? d.shippingCost : 0, deliveryTime: d.deliveryTime || 'Entrega en 24 Horas', allowsContraentrega: Boolean(d.allowsContraentrega), firstExchangeFree: Boolean(d.firstExchangeFree), exchangePolicyNote: d.exchangePolicyNote || '', isActive: d.isActive !== false, })); } return INITIAL_CITIES; } catch (e: any) { console.warn('Error getCitiesFromAppwrite:', e.message); return INITIAL_CITIES; } } export async function createCityInAppwrite(city: Partial): Promise { const newId = city.id || `city-${Date.now()}`; const payload = { name: city.name || 'Nueva Ciudad', department: city.department || 'Colombia', shippingCost: city.shippingCost || 0, deliveryTime: city.deliveryTime || 'Entrega en 24 Horas', allowsContraentrega: city.allowsContraentrega !== false, firstExchangeFree: Boolean(city.firstExchangeFree), exchangePolicyNote: city.exchangePolicyNote || '', isActive: city.isActive !== false, }; try { const doc = await serverDatabases.createDocument(DB_ID, 'cities', newId, payload); return { id: doc.$id, ...payload }; } catch (e: any) { console.error('Error createCityInAppwrite:', e.message); return { id: newId, ...payload }; } } export async function updateCityInAppwrite(id: string, updates: Partial): Promise { try { const cleanUpdates: Record = {}; if (updates.name !== undefined) cleanUpdates.name = updates.name; if (updates.department !== undefined) cleanUpdates.department = updates.department; if (updates.shippingCost !== undefined) cleanUpdates.shippingCost = updates.shippingCost; if (updates.deliveryTime !== undefined) cleanUpdates.deliveryTime = updates.deliveryTime; if (updates.allowsContraentrega !== undefined) cleanUpdates.allowsContraentrega = updates.allowsContraentrega; if (updates.firstExchangeFree !== undefined) cleanUpdates.firstExchangeFree = updates.firstExchangeFree; if (updates.exchangePolicyNote !== undefined) cleanUpdates.exchangePolicyNote = updates.exchangePolicyNote; if (updates.isActive !== undefined) cleanUpdates.isActive = updates.isActive; const doc = await serverDatabases.updateDocument(DB_ID, 'cities', id, cleanUpdates); return { id: doc.$id, name: doc.name, department: doc.department, shippingCost: doc.shippingCost, deliveryTime: doc.deliveryTime, allowsContraentrega: doc.allowsContraentrega, firstExchangeFree: doc.firstExchangeFree, exchangePolicyNote: doc.exchangePolicyNote, isActive: doc.isActive, }; } catch (e: any) { console.error('Error updateCityInAppwrite:', e.message); return null; } } export async function deleteCityInAppwrite(id: string): Promise { try { await serverDatabases.deleteDocument(DB_ID, 'cities', id); return true; } catch (e: any) { console.error('Error deleteCityInAppwrite:', e.message); return false; } } // ========================================================================= // 2. MÉTODOS DE PAGO & ANTICIPOS (CRUD PERSISTENTE EN APPWRITE) // ========================================================================= export async function getPaymentMethodsFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'payment_methods', [ Query.limit(100), ]); if (res.documents && res.documents.length > 0) { 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) { console.warn('Error getPaymentMethodsFromAppwrite:', e.message); return INITIAL_PAYMENT_METHODS; } } export async function createPaymentMethodInAppwrite(method: Partial): Promise { const newId = method.id || `pay-${Date.now()}`; const meta = { boldChannel: method.boldChannel || (method.type === 'bold' ? 'all' : 'deposit'), badge: method.badge || '', icon: method.icon || '', info: method.accountInfo || '', }; const payload: Record = { name: method.name || 'Nuevo Método de Pago', type: method.type || 'contraentrega', description: method.description || '', requiresDeposit: Boolean(method.requiresDeposit), depositAmount: method.depositAmount || 0, accountInfo: JSON.stringify(meta), boldPaymentLink: method.boldPaymentLink || '', boldQrUrl: method.boldQrUrl || '', isActive: method.isActive !== false, }; try { const doc = await serverDatabases.createDocument(DB_ID, 'payment_methods', newId, 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, ...method } as PaymentMethod; } } export async function updatePaymentMethodInAppwrite(id: string, updates: Partial): Promise { try { // Si viene actualización de metadatos o info, obtener actual para merge let currentMeta: Record = {}; 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 = {}; 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; 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; const doc = await serverDatabases.updateDocument(DB_ID, 'payment_methods', id, cleanUpdates); return { 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: currentMeta.info, boldPaymentLink: doc.boldPaymentLink, boldQrUrl: doc.boldQrUrl, isActive: doc.isActive, }; } catch (e: any) { console.error('Error updatePaymentMethodInAppwrite:', e.message); return null; } } export async function deletePaymentMethodInAppwrite(id: string): Promise { try { await serverDatabases.deleteDocument(DB_ID, 'payment_methods', id); return true; } catch (e: any) { console.error('Error deletePaymentMethodInAppwrite:', e.message); return false; } } /** * Sincroniza o restaura los métodos de pago oficiales de Bold en Appwrite */ export async function syncDefaultPaymentMethodsToAppwrite(): Promise { 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 = { 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) // ========================================================================= export async function getOrdersFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'orders', [ Query.limit(100), Query.orderDesc('$createdAt'), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => { let items = []; try { items = typeof d.items === 'string' ? JSON.parse(d.items) : (d.items || []); } catch (_) { items = []; } return { id: d.$id, customerName: d.customerName || 'Cliente', customerPhone: d.customerPhone || '', address: d.address || '', cityId: d.cityId || 'city-cali', cityName: d.cityName || 'Cali', items, subtotal: typeof d.subtotal === 'number' ? d.subtotal : 0, shippingCost: typeof d.shippingCost === 'number' ? d.shippingCost : 0, totalAmount: typeof d.totalAmount === 'number' ? d.totalAmount : 0, depositAmount: typeof d.depositAmount === 'number' ? d.depositAmount : 10000, balanceDue: typeof d.balanceDue === 'number' ? d.balanceDue : 0, paymentMethodId: d.paymentMethodId || 'pay-contraentrega', paymentMethodName: d.paymentMethodName || 'Contraentrega', status: d.status || 'pending', notes: d.notes || '', source: (d.source as any) || (d.$id?.includes('WA') || (d.notes && /whatsapp|catálogo/i.test(d.notes)) ? 'whatsapp' : 'web'), couponCode: d.couponCode || '', discountAmount: typeof d.discountAmount === 'number' ? d.discountAmount : 0, createdAt: d.createdAt || d.$createdAt, }; }); } return []; } catch (e: any) { console.warn('Error getOrdersFromAppwrite:', e.message); return []; } } export async function createOrderInAppwrite(order: Partial): Promise { const newId = order.id || `ord-${Date.now()}`; const total = order.totalAmount || 0; const deposit = order.depositAmount !== undefined ? order.depositAmount : 10000; const balance = order.balanceDue !== undefined ? order.balanceDue : Math.max(0, total - deposit); const resolvedSource = order.source || (newId.includes('WA') ? 'whatsapp' : 'web'); const payload = { customerName: order.customerName || 'Cliente', customerPhone: order.customerPhone || '', address: order.address || '', cityId: order.cityId || 'city-cali', cityName: order.cityName || 'Cali', items: JSON.stringify(order.items || []), subtotal: order.subtotal || total, shippingCost: order.shippingCost || 0, totalAmount: total, depositAmount: deposit, balanceDue: balance, paymentMethodId: order.paymentMethodId || 'pay-contraentrega', paymentMethodName: order.paymentMethodName || 'Contraentrega', status: order.status || 'pending', notes: order.notes || '', couponCode: order.couponCode || '', discountAmount: typeof order.discountAmount === 'number' ? order.discountAmount : 0, createdAt: order.createdAt || new Date().toISOString(), }; try { const doc = await serverDatabases.createDocument(DB_ID, 'orders', newId, payload); // Si la orden utilizó un cupón de descuento, registrar el uso del cupón if (order.couponCode) { incrementCouponUsage(order.couponCode).catch((cErr) => { console.warn('Error registrando uso de cupón:', cErr.message); }); } return { id: doc.$id, ...payload, items: order.items || [], source: resolvedSource, status: payload.status as any, }; } catch (e: any) { console.error('Error createOrderInAppwrite:', e.message); return { id: newId, ...payload, items: order.items || [], source: resolvedSource, status: payload.status as any, }; } } export async function updateOrderInAppwrite(id: string, updates: Partial): Promise { try { const cleanUpdates: Record = {}; if (updates.customerName !== undefined) cleanUpdates.customerName = updates.customerName; if (updates.customerPhone !== undefined) cleanUpdates.customerPhone = updates.customerPhone; if (updates.address !== undefined) cleanUpdates.address = updates.address; if (updates.cityId !== undefined) cleanUpdates.cityId = updates.cityId; if (updates.cityName !== undefined) cleanUpdates.cityName = updates.cityName; if (updates.items !== undefined) cleanUpdates.items = JSON.stringify(updates.items); if (updates.subtotal !== undefined) cleanUpdates.subtotal = updates.subtotal; if (updates.shippingCost !== undefined) cleanUpdates.shippingCost = updates.shippingCost; if (updates.totalAmount !== undefined) cleanUpdates.totalAmount = updates.totalAmount; if (updates.depositAmount !== undefined) cleanUpdates.depositAmount = updates.depositAmount; if (updates.balanceDue !== undefined) cleanUpdates.balanceDue = updates.balanceDue; if (updates.paymentMethodId !== undefined) cleanUpdates.paymentMethodId = updates.paymentMethodId; if (updates.paymentMethodName !== undefined) cleanUpdates.paymentMethodName = updates.paymentMethodName; if (updates.status !== undefined) cleanUpdates.status = updates.status; if (updates.notes !== undefined) cleanUpdates.notes = updates.notes; if (updates.couponCode !== undefined) cleanUpdates.couponCode = updates.couponCode; if (updates.discountAmount !== undefined) cleanUpdates.discountAmount = updates.discountAmount; const doc = await serverDatabases.updateDocument(DB_ID, 'orders', id, cleanUpdates); let items = []; try { items = typeof doc.items === 'string' ? JSON.parse(doc.items) : (doc.items || []); } catch (_) { items = updates.items || []; } return { id: doc.$id, customerName: doc.customerName, customerPhone: doc.customerPhone, address: doc.address, cityId: doc.cityId, cityName: doc.cityName, items, subtotal: doc.subtotal, shippingCost: doc.shippingCost, totalAmount: doc.totalAmount, depositAmount: doc.depositAmount, balanceDue: doc.balanceDue, paymentMethodId: doc.paymentMethodId, paymentMethodName: doc.paymentMethodName, status: doc.status, notes: doc.notes, createdAt: doc.createdAt || doc.$createdAt, }; } catch (e: any) { console.error('Error updateOrderInAppwrite:', e.message); return null; } } export async function deleteOrderInAppwrite(id: string): Promise { try { await serverDatabases.deleteDocument(DB_ID, 'orders', id); return true; } catch (e: any) { console.error('Error deleteOrderInAppwrite:', e.message); return false; } } // ========================================================================= // 4. PROVEEDORES (CRUD PERSISTENTE EN APPWRITE) // ========================================================================= export async function getSuppliersFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'suppliers', [ Query.limit(100), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => ({ id: d.$id, name: d.name || '', phone: d.phone || '', contactPerson: d.contactPerson || '', address: d.address || '', notes: d.notes || '', isActive: d.isActive !== false, })); } return INITIAL_SUPPLIERS; } catch (e: any) { console.warn('Error getSuppliersFromAppwrite:', e.message); return INITIAL_SUPPLIERS; } } export async function createSupplierInAppwrite(supplier: Partial): Promise { const newId = supplier.id || `sup-${Date.now()}`; const payload = { name: supplier.name || 'Nuevo Proveedor', phone: supplier.phone || '573167455877', contactPerson: supplier.contactPerson || '', address: supplier.address || '', notes: supplier.notes || '', isActive: supplier.isActive !== false, }; try { const doc = await serverDatabases.createDocument(DB_ID, 'suppliers', newId, payload); return { id: doc.$id, ...payload }; } catch (e: any) { return { id: newId, ...payload }; } } export async function updateSupplierInAppwrite(id: string, updates: Partial): Promise { try { const doc = await serverDatabases.updateDocument(DB_ID, 'suppliers', id, updates); return { id: doc.$id, name: doc.name, phone: doc.phone, contactPerson: doc.contactPerson, address: doc.address, notes: doc.notes, isActive: doc.isActive, }; } catch (e) { return null; } } export async function deleteSupplierInAppwrite(id: string): Promise { try { await serverDatabases.deleteDocument(DB_ID, 'suppliers', id); return true; } catch (e) { return false; } } // ========================================================================= // 5. CONFIGURACIÓN DE LA TIENDA // ========================================================================= export async function getSettingsFromAppwrite(): Promise { 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 || '', email: doc.email || '', phone: doc.phone || '', whatsappNumber: doc.whatsappNumber || '', instagramHandle: doc.instagramHandle || '', address: doc.address || '', city: doc.city || 'Cali, Colombia', nequiAccount: doc.nequiAccount || '', daviplataAccount: doc.daviplataAccount || '', accountHolder: doc.accountHolder || '', boldPaymentLink: doc.boldPaymentLink || '', boldApiKey: doc.boldApiKey || '', 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, }; } catch (e) { return null; } } export async function updateSettingsInAppwrite(settings: Partial): Promise { try { const cleanUpdates: Record = {}; 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, email: doc.email, phone: doc.phone, whatsappNumber: doc.whatsappNumber, instagramHandle: doc.instagramHandle, address: doc.address, city: doc.city, nequiAccount: doc.nequiAccount, daviplataAccount: doc.daviplataAccount, accountHolder: doc.accountHolder, boldPaymentLink: doc.boldPaymentLink, boldApiKey: doc.boldApiKey, 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: any) { console.error('Error updateSettingsInAppwrite:', e.message); return settings as StoreSettings; } } // ========================================================================= // 6. PRODUCTOS Y CATÁLOGO // ========================================================================= export async function getProductsFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'products', [ Query.limit(100), Query.orderDesc('$createdAt'), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => { let sizes = ['37', '38', '39', '40', '41', '42', '43']; let images = ['https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80']; let colors = []; try { if (d.sizes) sizes = typeof d.sizes === 'string' ? JSON.parse(d.sizes) : d.sizes; } catch (_) {} try { if (d.images) images = typeof d.images === 'string' ? JSON.parse(d.images) : d.images; if (Array.isArray(images)) { images = images.map((img: string) => { if (img && typeof img === 'string') { if (img.startsWith('data:image') && d.whatsappMessageId) { return `https://drip-zone.online/api/whatsapp-media?id=${encodeURIComponent(d.whatsappMessageId)}`; } if (img.startsWith('/')) { return `https://drip-zone.online${img}`; } } return img; }); } } catch (_) {} try { if (d.colors) colors = typeof d.colors === 'string' ? JSON.parse(d.colors) : d.colors; } catch (_) {} const brand = d.brand || 'Sneakers'; const cleanName = sanitizePublicationTitle(d.name || 'Zapatillas', [d.supplierName], brand); const cleanDesc = sanitizePublicationText(d.description || '', [d.supplierName], d.supplierPrice); return { id: d.$id, name: cleanName, modelName: cleanName, colorName: d.colorName || '', brand: brand, supplierPrice: d.supplierPrice || 90000, retailPrice: d.retailPrice || 125000, originalPrice: d.originalPrice || Math.round((d.retailPrice || 125000) * 1.35), sizes, images, colors, description: cleanDesc, category: d.category || 'Calzado / Sneakers', inStock: d.inStock !== false, isActive: d.isActive !== false, supplierName: d.supplierName || '', supplierPhone: d.supplierPhone || '', whatsappMessageId: d.whatsappMessageId || '', createdAt: d.createdAt || d.$createdAt || new Date().toISOString(), publishedAt: d.publishedAt || d.createdAt || d.$createdAt || new Date().toISOString(), autoDisabledAt: d.autoDisabledAt || undefined, manuallyEnabled: d.manuallyEnabled === true || d.manuallyEnabled === 'true' || false, }; }); } return []; } catch (e: any) { return []; } } export async function saveProductToAppwrite(product: Product): Promise { const brand = product.brand || 'Sneakers'; const cleanName = sanitizePublicationTitle(product.name, [product.supplierName], brand); const cleanDesc = sanitizePublicationText(product.description || '', [product.supplierName], product.supplierPrice); const payload = { name: cleanName, modelName: cleanName, brand: brand, colorName: product.colorName || '', supplierPrice: product.supplierPrice || 0, retailPrice: product.retailPrice || 0, originalPrice: product.originalPrice || Math.round((product.retailPrice || 0) * 1.35), sizes: JSON.stringify(product.sizes || []), images: JSON.stringify(product.images || []), colors: JSON.stringify(product.colors || []), description: cleanDesc, category: product.category || 'Sneakers', inStock: product.inStock !== false, isActive: product.isActive !== false, supplierName: product.supplierName || '', supplierPhone: product.supplierPhone || '', whatsappMessageId: product.whatsappMessageId || '', }; try { const doc = await serverDatabases.createDocument(DB_ID, 'products', product.id, payload); return { ...product, name: cleanName, modelName: cleanName, description: cleanDesc }; } catch (e: any) { if (e.code === 409) { try { await serverDatabases.updateDocument(DB_ID, 'products', product.id, payload); } catch (_) {} } return { ...product, name: cleanName, modelName: cleanName, description: cleanDesc }; } } export async function deleteProductFromAppwrite(id: string): Promise { try { await serverDatabases.deleteDocument(DB_ID, 'products', id); return true; } catch (e) { return false; } } // ========================================================================= // 7. FEED WHATSAPP (POSTS PERSISTENTES EN APPWRITE) // ========================================================================= export async function getSupplierPostsFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'supplier_posts', [ Query.limit(100), Query.orderDesc('$createdAt'), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => { let rawImages: string[] = []; let extractedProduct = undefined; try { rawImages = typeof d.rawImages === 'string' ? JSON.parse(d.rawImages) : (d.rawImages || []); } catch (_) {} try { extractedProduct = typeof d.extractedProduct === 'string' ? JSON.parse(d.extractedProduct) : d.extractedProduct; } catch (_) {} return { id: d.$id, whatsappMessageId: d.whatsappMessageId || d.$id.replace('post-', ''), channelName: d.channelName || '', channelJid: d.channelJid || '', supplierName: d.supplierName || '', supplierPhone: d.supplierPhone || '', rawMessage: d.rawMessage || '', rawImages, extractedProduct, status: d.status || 'pending_review', receivedAt: d.receivedAt || d.$createdAt, }; }); } return []; } catch (e: any) { return []; } } export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise { const whatsappMessageId = post.whatsappMessageId || post.id.replace('post-', ''); const payload = { whatsappMessageId, channelName: post.channelName || 'Grupo WhatsApp', channelJid: post.channelJid || '', supplierName: post.supplierName || '', supplierPhone: post.supplierPhone || '', rawMessage: post.rawMessage || '', rawImages: JSON.stringify(post.rawImages || []), extractedProduct: JSON.stringify(post.extractedProduct || {}), status: post.status || 'pending_review', receivedAt: post.receivedAt || new Date().toISOString(), }; try { await serverDatabases.createDocument(DB_ID, 'supplier_posts', post.id, payload); return { ...post, whatsappMessageId }; } catch (e: any) { if (e.code === 409) { try { await serverDatabases.updateDocument(DB_ID, 'supplier_posts', post.id, payload); } catch (_) {} } return { ...post, whatsappMessageId }; } } export async function updateSupplierPostStatusInAppwrite( id: string, status: SupplierPost['status'] ): Promise { const cleanId = id.trim(); const whatsappMessageId = cleanId.replace('post-', ''); try { await serverDatabases.updateDocument(DB_ID, 'supplier_posts', cleanId, { status, whatsappMessageId, }); return true; } catch (e) { try { await serverDatabases.createDocument(DB_ID, 'supplier_posts', cleanId, { status, whatsappMessageId, receivedAt: new Date().toISOString(), }); return true; } catch (_) { return false; } } } // ========================================================================= // 8. GRUPOS WHATSAPP MONITOREADOS (PERSISTENCIA APPWRITE) // ========================================================================= export async function getMonitoredGroupsFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'monitored_groups', [ Query.limit(100), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => { const isBlacklisted = isGroupIgnoredOrBlacklisted(d.jid, d.name); return { id: d.$id, jid: d.jid || '', name: d.name || '', isMonitored: isBlacklisted ? false : Boolean(d.isMonitored), autoPublish: isBlacklisted ? false : d.autoPublish !== false, notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : d.notes || '', supplierName: d.name || '', }; }); } return []; } catch (e: any) { console.warn('Error getMonitoredGroupsFromAppwrite:', e.message); return []; } } export async function saveMonitoredGroupToAppwrite( group: Partial ): Promise { const isBlacklisted = isGroupIgnoredOrBlacklisted(group.jid || '', group.name); const docId = group.id || `grp-${(group.jid || '').replace(/\D/g, '').slice(-8)}` || `grp-${Date.now()}`; const payload = { jid: group.jid || '', name: group.name || '', isMonitored: isBlacklisted ? false : Boolean(group.isMonitored), autoPublish: isBlacklisted ? false : group.autoPublish !== false, notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : group.notes || '', }; try { const doc = await serverDatabases.createDocument(DB_ID, 'monitored_groups', docId, payload); return { id: doc.$id, jid: doc.jid, name: doc.name, isMonitored: doc.isMonitored, autoPublish: doc.autoPublish, notes: doc.notes, supplierName: doc.name, }; } catch (e: any) { if (e.code === 409) { try { await serverDatabases.updateDocument(DB_ID, 'monitored_groups', docId, payload); } catch (_) {} } return { id: docId, jid: payload.jid, name: payload.name, isMonitored: payload.isMonitored, autoPublish: payload.autoPublish, notes: payload.notes, supplierName: payload.name, }; } } export async function toggleMonitoredGroupInAppwrite( idOrJid: string, isMonitored: boolean ): Promise { try { const isBlacklisted = isGroupIgnoredOrBlacklisted(idOrJid); const finalState = isBlacklisted ? false : isMonitored; const res = await serverDatabases.listDocuments(DB_ID, 'monitored_groups', [Query.limit(100)]); const matchingDocs = res.documents.filter( (d: any) => d.$id === idOrJid || d.jid === idOrJid || d.name === idOrJid ); if (matchingDocs.length > 0) { await Promise.all( matchingDocs.map((doc: any) => serverDatabases.updateDocument(DB_ID, 'monitored_groups', doc.$id, { isMonitored: finalState, autoPublish: finalState, notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : doc.notes, }) ) ); return true; } return false; } catch (e) { return false; } } // ========================================================================= // 9. PLANTILLAS DE MENSAJES & NOTIFICACIONES (CRUD APPWRITE) // ========================================================================= export async function getMessageTemplatesFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'message_templates', [ Query.limit(50), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => { let vars = []; try { vars = typeof d.availableVariables === 'string' ? JSON.parse(d.availableVariables) : (d.availableVariables || []); } catch (_) {} return { id: d.$id, title: d.title || '', description: d.description || '', category: d.category || 'customer_sales', channel: d.channel || 'whatsapp', content: d.content || '', defaultContent: d.content || '', availableVariables: vars, isActive: d.isActive !== false, updatedAt: d.updatedAt || d.$updatedAt, }; }); } return []; } catch (e: any) { console.warn('Error getMessageTemplatesFromAppwrite:', e.message); return []; } } export async function updateMessageTemplateInAppwrite( id: string, updates: Partial ): Promise { try { const payload: Record = { updatedAt: new Date().toISOString(), }; if (updates.title !== undefined) payload.title = updates.title; if (updates.description !== undefined) payload.description = updates.description; if (updates.content !== undefined) payload.content = updates.content; if (updates.isActive !== undefined) payload.isActive = updates.isActive; if (updates.category !== undefined) payload.category = updates.category; if (updates.channel !== undefined) payload.channel = updates.channel; if (updates.availableVariables !== undefined) { payload.availableVariables = typeof updates.availableVariables === 'string' ? updates.availableVariables : JSON.stringify(updates.availableVariables); } const doc = await serverDatabases.updateDocument(DB_ID, 'message_templates', id, payload); let vars = []; try { vars = typeof doc.availableVariables === 'string' ? JSON.parse(doc.availableVariables) : (doc.availableVariables || []); } catch (_) {} return { id: doc.$id, title: doc.title, description: doc.description, category: doc.category, channel: doc.channel, content: doc.content, defaultContent: doc.content, availableVariables: vars, isActive: doc.isActive !== false, updatedAt: doc.updatedAt, }; } catch (e: any) { console.error('Error updateMessageTemplateInAppwrite:', e.message); return null; } } // ========================================================================= // ENCARGOS Y PEDIDOS ESPECIALES (CUSTOM REQUESTS) // ========================================================================= export async function fetchCustomRequestsFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'custom_requests', [ Query.orderDesc('$createdAt'), Query.limit(100), ]); return res.documents.map((doc: any) => ({ id: doc.$id, userId: doc.userId, customerName: doc.customerName, customerPhone: doc.customerPhone, customerEmail: doc.customerEmail, customerCity: doc.customerCity, brand: doc.brand, modelName: doc.modelName, size: doc.size, colorway: doc.colorway, targetBudget: doc.targetBudget, notes: doc.notes, imageUrl: doc.imageUrl, status: doc.status as any, quotedPrice: doc.quotedPrice, adminNotes: doc.adminNotes, createdAt: doc.createdAt || doc.$createdAt, updatedAt: doc.updatedAt || doc.$updatedAt, })); } catch (e: any) { console.warn('Error fetchCustomRequestsFromAppwrite:', e.message); return []; } } export async function createCustomRequestInAppwrite(req: CustomProductRequest): Promise { const newId = req.id && !req.id.includes(' ') ? req.id.replace(/[^a-zA-Z0-9._-]/g, '-') : ID.unique(); const payload: Record = { customerName: req.customerName, customerPhone: req.customerPhone, customerEmail: req.customerEmail || '', customerCity: req.customerCity || 'Colombia', brand: req.brand, modelName: req.modelName, size: req.size, colorway: req.colorway || '', notes: req.notes || '', imageUrl: req.imageUrl || '', status: req.status || 'pending', createdAt: req.createdAt || new Date().toISOString(), }; if (req.targetBudget !== undefined && req.targetBudget !== null) payload.targetBudget = req.targetBudget; if (req.quotedPrice !== undefined && req.quotedPrice !== null) payload.quotedPrice = req.quotedPrice; if (req.adminNotes) payload.adminNotes = req.adminNotes; if (req.userId) payload.userId = req.userId; try { const doc = await serverDatabases.createDocument(DB_ID, 'custom_requests', newId, payload); return { ...req, id: doc.$id, }; } catch (e: any) { console.error('Error createCustomRequestInAppwrite:', e.message); return req; } } export async function updateCustomRequestInAppwrite( id: string, updates: Partial ): Promise { try { const payload: Record = { updatedAt: new Date().toISOString(), }; if (updates.customerName !== undefined) payload.customerName = updates.customerName; if (updates.customerPhone !== undefined) payload.customerPhone = updates.customerPhone; if (updates.customerEmail !== undefined) payload.customerEmail = updates.customerEmail; if (updates.customerCity !== undefined) payload.customerCity = updates.customerCity; if (updates.brand !== undefined) payload.brand = updates.brand; if (updates.modelName !== undefined) payload.modelName = updates.modelName; if (updates.size !== undefined) payload.size = updates.size; if (updates.colorway !== undefined) payload.colorway = updates.colorway; if (updates.notes !== undefined) payload.notes = updates.notes; if (updates.imageUrl !== undefined) payload.imageUrl = updates.imageUrl; if (updates.status !== undefined) payload.status = updates.status; if (updates.targetBudget !== undefined) payload.targetBudget = updates.targetBudget; if (updates.quotedPrice !== undefined) payload.quotedPrice = updates.quotedPrice; if (updates.adminNotes !== undefined) payload.adminNotes = updates.adminNotes; const doc = await serverDatabases.updateDocument(DB_ID, 'custom_requests', id, payload); return { id: doc.$id, userId: doc.userId, customerName: doc.customerName, customerPhone: doc.customerPhone, customerEmail: doc.customerEmail, customerCity: doc.customerCity, brand: doc.brand, modelName: doc.modelName, size: doc.size, colorway: doc.colorway, targetBudget: doc.targetBudget, notes: doc.notes, imageUrl: doc.imageUrl, status: doc.status as any, quotedPrice: doc.quotedPrice, adminNotes: doc.adminNotes, createdAt: doc.createdAt || doc.$createdAt, updatedAt: doc.updatedAt || doc.$updatedAt, }; } catch (e: any) { console.error('Error updateCustomRequestInAppwrite:', e.message); return null; } } export async function deleteCustomRequestInAppwrite(id: string): Promise { try { await serverDatabases.deleteDocument(DB_ID, 'custom_requests', id); return true; } catch (e: any) { console.error('Error deleteCustomRequestInAppwrite:', e.message); return false; } } // ========================================================================= // 12. CUPONES Y PROMOCIONES (CRUD PERSISTENTE EN APPWRITE) // ========================================================================= export async function getCouponsFromAppwrite(): Promise { try { const res = await serverDatabases.listDocuments(DB_ID, 'coupons', [ Query.limit(100), Query.orderDesc('$createdAt'), ]); if (res.documents && res.documents.length > 0) { return res.documents.map((d: any) => { let categories: string[] = []; try { if (d.applicableCategories) { categories = typeof d.applicableCategories === 'string' ? JSON.parse(d.applicableCategories) : d.applicableCategories; } } catch (_) { categories = []; } return { id: d.$id, code: (d.code || '').toUpperCase().trim(), description: d.description || '', discountType: (d.discountType || 'percentage') as 'percentage' | 'fixed' | 'per_item', discountValue: Number(d.discountValue) || 0, minSpend: Number(d.minSpend) || 0, maxDiscount: Number(d.maxDiscount) || 0, usageLimit: Number(d.usageLimit) || 0, usageCount: Number(d.usageCount) || 0, expiresAt: d.expiresAt || undefined, startDate: d.startDate || undefined, isActive: d.isActive !== false, applicableCategories: categories, createdAt: d.$createdAt || d.createdAt, updatedAt: d.$updatedAt || d.updatedAt, }; }); } return []; } catch (e: any) { console.warn('Error getCouponsFromAppwrite:', e.message); return []; } } export async function getCouponByCodeFromAppwrite(code: string): Promise { if (!code) return null; const cleanCode = code.toUpperCase().trim(); try { const coupons = await getCouponsFromAppwrite(); const found = coupons.find((c) => c.code === cleanCode); return found || null; } catch (e: any) { console.warn('Error getCouponByCodeFromAppwrite:', e.message); return null; } } export async function createCouponInAppwrite(coupon: Partial): Promise { const cleanCode = (coupon.code || `CUPON-${Date.now()}`).toUpperCase().trim(); const newId = coupon.id || `cup-${cleanCode.toLowerCase().replace(/[^a-z0-9]/g, '')}-${Date.now().toString(36).slice(-4)}`; const payload = { code: cleanCode, description: coupon.description || '', discountType: coupon.discountType || 'percentage', discountValue: typeof coupon.discountValue === 'number' ? coupon.discountValue : 10, minSpend: typeof coupon.minSpend === 'number' ? coupon.minSpend : 0, maxDiscount: typeof coupon.maxDiscount === 'number' ? coupon.maxDiscount : 0, usageLimit: typeof coupon.usageLimit === 'number' ? coupon.usageLimit : 0, usageCount: typeof coupon.usageCount === 'number' ? coupon.usageCount : 0, expiresAt: coupon.expiresAt || '', startDate: coupon.startDate || new Date().toISOString(), isActive: coupon.isActive !== false, applicableCategories: Array.isArray(coupon.applicableCategories) ? JSON.stringify(coupon.applicableCategories) : (coupon.applicableCategories || ''), }; try { const doc = await serverDatabases.createDocument(DB_ID, 'coupons', newId, payload); return { id: doc.$id, ...payload, applicableCategories: coupon.applicableCategories || [], createdAt: doc.$createdAt, }; } catch (e: any) { console.error('Error createCouponInAppwrite:', e.message); return { id: newId, ...payload, applicableCategories: coupon.applicableCategories || [], createdAt: new Date().toISOString(), }; } } export async function updateCouponInAppwrite(id: string, updates: Partial): Promise { try { const cleanUpdates: Record = {}; if (updates.code !== undefined) cleanUpdates.code = updates.code.toUpperCase().trim(); if (updates.description !== undefined) cleanUpdates.description = updates.description; if (updates.discountType !== undefined) cleanUpdates.discountType = updates.discountType; if (updates.discountValue !== undefined) cleanUpdates.discountValue = updates.discountValue; if (updates.minSpend !== undefined) cleanUpdates.minSpend = updates.minSpend; if (updates.maxDiscount !== undefined) cleanUpdates.maxDiscount = updates.maxDiscount; if (updates.usageLimit !== undefined) cleanUpdates.usageLimit = updates.usageLimit; if (updates.usageCount !== undefined) cleanUpdates.usageCount = updates.usageCount; if (updates.expiresAt !== undefined) cleanUpdates.expiresAt = updates.expiresAt || ''; if (updates.startDate !== undefined) cleanUpdates.startDate = updates.startDate || ''; if (updates.isActive !== undefined) cleanUpdates.isActive = updates.isActive; if (updates.applicableCategories !== undefined) { cleanUpdates.applicableCategories = Array.isArray(updates.applicableCategories) ? JSON.stringify(updates.applicableCategories) : updates.applicableCategories; } const doc = await serverDatabases.updateDocument(DB_ID, 'coupons', id, cleanUpdates); return { id: doc.$id, code: doc.code, description: doc.description, discountType: doc.discountType as any, discountValue: doc.discountValue, minSpend: doc.minSpend, maxDiscount: doc.maxDiscount, usageLimit: doc.usageLimit, usageCount: doc.usageCount, expiresAt: doc.expiresAt, startDate: doc.startDate, isActive: doc.isActive !== false, applicableCategories: updates.applicableCategories || [], createdAt: doc.$createdAt, updatedAt: doc.$updatedAt, }; } catch (e: any) { console.error('Error updateCouponInAppwrite:', e.message); return null; } } export async function deleteCouponInAppwrite(id: string): Promise { try { await serverDatabases.deleteDocument(DB_ID, 'coupons', id); return true; } catch (e: any) { console.error('Error deleteCouponInAppwrite:', e.message); return false; } } export async function incrementCouponUsage(idOrCode: string): Promise { if (!idOrCode) return; try { const coupons = await getCouponsFromAppwrite(); const clean = idOrCode.toUpperCase().trim(); const matched = coupons.find((c) => c.id === idOrCode || c.code.toUpperCase() === clean); if (matched) { await updateCouponInAppwrite(matched.id, { usageCount: (matched.usageCount || 0) + 1, }); } } catch (e: any) { console.warn('Error incrementCouponUsage:', e.message); } }