almeira-web/dripzone-app/src/lib/server-appwrite.ts

1392 lines
53 KiB
TypeScript

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<City[]> {
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<City>): Promise<City> {
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<City>): Promise<City | null> {
try {
const cleanUpdates: Record<string, any> = {};
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<boolean> {
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<PaymentMethod[]> {
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<PaymentMethod>): Promise<PaymentMethod> {
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<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: 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<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;
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<boolean> {
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<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)
// =========================================================================
export async function getOrdersFromAppwrite(): Promise<Order[]> {
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<Order>): Promise<Order> {
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<Order>): Promise<Order | null> {
try {
const cleanUpdates: Record<string, any> = {};
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<boolean> {
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<Supplier[]> {
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<Supplier>): Promise<Supplier> {
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<Supplier>): Promise<Supplier | null> {
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<boolean> {
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<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 || '',
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<StoreSettings>): Promise<StoreSettings> {
try {
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,
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<Product[]> {
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<Product> {
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<boolean> {
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<SupplierPost[]> {
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<SupplierPost> {
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<boolean> {
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<MonitoredWhatsAppGroup[]> {
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<MonitoredWhatsAppGroup>
): Promise<MonitoredWhatsAppGroup> {
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<boolean> {
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<MessageTemplate[]> {
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<MessageTemplate>
): Promise<MessageTemplate | null> {
try {
const payload: Record<string, any> = {
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<CustomProductRequest[]> {
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<CustomProductRequest> {
const newId = req.id && !req.id.includes(' ') ? req.id.replace(/[^a-zA-Z0-9._-]/g, '-') : ID.unique();
const payload: Record<string, any> = {
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<CustomProductRequest>
): Promise<CustomProductRequest | null> {
try {
const payload: Record<string, any> = {
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<boolean> {
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<Coupon[]> {
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<Coupon | null> {
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<Coupon>): Promise<Coupon> {
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<Coupon>): Promise<Coupon | null> {
try {
const cleanUpdates: Record<string, any> = {};
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<boolean> {
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<void> {
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);
}
}