feat: gestion completa de Encargos y Pedidos Especiales (editar, eliminar, crear, sincronizar con Appwrite)

This commit is contained in:
AI Bot 2026-09-10 15:57:46 -05:00
parent ef90fb55af
commit e4820668cb
7 changed files with 1439 additions and 239 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,12 @@ import { dataService } from '@/lib/data-service';
import { CustomProductRequest } from '@/types';
import fs from 'fs';
import path from 'path';
import {
fetchCustomRequestsFromAppwrite,
createCustomRequestInAppwrite,
updateCustomRequestInAppwrite,
deleteCustomRequestInAppwrite,
} from '@/lib/server-appwrite';
import { sendWhatsAppMessage } from '@/lib/evolution-api';
const CARLOS_PHONE = '573167455877';
@ -20,7 +26,26 @@ export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const userId = searchParams.get('userId') || undefined;
const requests = dataService.getCustomRequests(userId);
// Intentar leer de Appwrite primero
let requests: CustomProductRequest[] = [];
try {
const appwriteRequests = await fetchCustomRequestsFromAppwrite();
if (appwriteRequests && appwriteRequests.length > 0) {
requests = appwriteRequests;
dataService.setCustomRequests(requests);
}
} catch (err: any) {
console.warn('[GET /api/custom-requests] Error leyendo de Appwrite:', err?.message);
}
// Fallback a DataService local si Appwrite aún no tiene registros
if (requests.length === 0) {
requests = dataService.getCustomRequests(userId);
} else if (userId) {
requests = requests.filter((r) => r.userId === userId);
}
return NextResponse.json({ success: true, requests });
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
@ -57,7 +82,7 @@ export async function POST(request: NextRequest) {
let finalImageUrl = body.imageUrl || '';
// Si el usuario envió una imagen en base64, guardarla en disco
// Si se envió una imagen en base64, guardarla en disco
if (imageBase64 && imageBase64.startsWith('data:image')) {
try {
ensureUploadsDir();
@ -75,12 +100,12 @@ export async function POST(request: NextRequest) {
}
}
// Crear la solicitud en DataService
// 1. Guardar en DataService local
const createdRequest = dataService.addCustomRequest({
userId,
customerName,
customerPhone,
customerEmail,
customerEmail: customerEmail || '',
customerCity: customerCity || 'Colombia',
brand,
modelName,
@ -89,12 +114,21 @@ export async function POST(request: NextRequest) {
targetBudget: targetBudget ? parseInt(targetBudget, 10) : undefined,
notes: notes || '',
imageUrl: finalImageUrl,
status: 'pending',
status: body.status || 'pending',
quotedPrice: body.quotedPrice ? parseInt(body.quotedPrice, 10) : undefined,
adminNotes: body.adminNotes || '',
});
// 1. Notificación inmediata a Carlos (+57 316 745 5877)
// 2. Persistir inmediatamente en Appwrite Cloud
try {
await createCustomRequestInAppwrite(createdRequest);
} catch (appwriteErr: any) {
console.warn('Aviso: No se pudo guardar encargo en Appwrite:', appwriteErr?.message);
}
// 3. Notificación inmediata a Carlos (+57 316 745 5877)
const budgetFormatted = createdRequest.targetBudget
? `$${createdRequest.targetBudget.toLocaleString('es-CO')}`
? `$${createdRequest.targetBudget.toLocaleString('es-CO')} COP`
: 'A cotizar por bodega';
const adminMessage = `🚨 *NUEVA SOLICITUD DE ENCARGO ESPECIAL* 👟
@ -115,7 +149,7 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
// 2. Confirmación automática al cliente por WhatsApp usando la plantilla registrada
// 4. Confirmación automática al cliente por WhatsApp usando la plantilla registrada (desde línea DripZone)
const settings = dataService.getSettings();
const clientMessage = dataService.renderMessageTemplate('customer_request_received', {
tienda_nombre: settings?.storeName || 'DripZone Sneakers',
@ -129,7 +163,7 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
return NextResponse.json({
success: true,
message: '¡Solicitud de encargo recibida con éxito! Nuestro equipo te contactará por WhatsApp.',
message: '¡Solicitud de encargo recibida y guardada con éxito!',
request: createdRequest,
});
} catch (error: any) {
@ -141,21 +175,57 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
export async function PUT(request: NextRequest) {
try {
const body = await request.json();
const { id, updates, notifyCustomer } = body;
const { id, updates = {}, notifyCustomer } = body;
if (!id) {
return NextResponse.json({ success: false, error: 'ID de solicitud requerido' }, { status: 400 });
}
const updated = dataService.updateCustomRequest(id, updates);
const cleanUpdates: Partial<CustomProductRequest> = { ...updates };
// Si se envió una nueva imagen en base64 en la edición, guardarla en disco
if (updates.imageBase64 && typeof updates.imageBase64 === 'string' && updates.imageBase64.startsWith('data:image')) {
try {
ensureUploadsDir();
const matches = updates.imageBase64.match(/^data:image\/([a-zA-Z0-9]+);base64,(.+)$/);
if (matches) {
const ext = matches[1] === 'jpeg' ? 'jpg' : matches[1];
const buffer = Buffer.from(matches[2], 'base64');
const filename = `encargo-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;
const filePath = path.join(UPLOADS_DIR, filename);
fs.writeFileSync(filePath, buffer);
cleanUpdates.imageUrl = `/uploads/custom-requests/${filename}`;
}
} catch (saveErr) {
console.warn('No se pudo guardar la imagen editada del encargo:', saveErr);
}
}
// Parsear números si vienen como string
if (cleanUpdates.quotedPrice !== undefined && cleanUpdates.quotedPrice !== null) {
cleanUpdates.quotedPrice = parseInt(String(cleanUpdates.quotedPrice), 10) || undefined;
}
if (cleanUpdates.targetBudget !== undefined && cleanUpdates.targetBudget !== null) {
cleanUpdates.targetBudget = parseInt(String(cleanUpdates.targetBudget), 10) || undefined;
}
// 1. Actualizar en DataService local
const updated = dataService.updateCustomRequest(id, cleanUpdates);
if (!updated) {
return NextResponse.json({ success: false, error: 'Solicitud no encontrada' }, { status: 404 });
}
// Si el admin activó la notificación al cliente (por ejemplo al cotizar el producto)
// 2. Actualizar en Appwrite Cloud
try {
await updateCustomRequestInAppwrite(id, cleanUpdates);
} catch (appwriteErr: any) {
console.warn('Aviso: Error actualizando en Appwrite:', appwriteErr?.message);
}
// 3. Notificar al cliente si se solicita
if (notifyCustomer && updated.customerPhone && updated.quotedPrice) {
const quoteMessage = `👋 ¡Hola *${updated.customerName}*!
¡Buenas noticias! Encontramos tu par solicitado: *${updated.brand} ${updated.modelName}* en talla *${updated.size}*. 👟✨
¡Buenas noticias de *DripZone*! Encontramos tu par solicitado: *${updated.brand} ${updated.modelName}* en talla *${updated.size}*. 👟✨
💰 *Precio Cotizado:* $${updated.quotedPrice.toLocaleString('es-CO')} COP
🚚 *Envío:* Despacho directo a tu dirección con pago contraentrega disponible.
@ -167,6 +237,7 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ success: true, request: updated });
} catch (error: any) {
console.error('Error en PUT /api/custom-requests:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}
@ -178,9 +249,20 @@ export async function DELETE(request: NextRequest) {
if (!id) {
return NextResponse.json({ success: false, error: 'ID requerido' }, { status: 400 });
}
// 1. Eliminar en DataService local
const deleted = dataService.deleteCustomRequest(id);
return NextResponse.json({ success: deleted });
// 2. Eliminar en Appwrite Cloud
try {
await deleteCustomRequestInAppwrite(id);
} catch (appwriteErr: any) {
console.warn('Aviso: Error eliminando de Appwrite:', appwriteErr?.message);
}
return NextResponse.json({ success: true, deleted: true, id });
} catch (error: any) {
console.error('Error en DELETE /api/custom-requests:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@ -22,4 +22,5 @@ export const COLLECTIONS = {
ORDERS: 'orders',
SETTINGS: 'settings',
SUPPLIER_POSTS: 'supplier_posts',
CUSTOM_REQUESTS: 'custom_requests',
};

View File

@ -1170,49 +1170,63 @@ class DataService {
// =========================================================================
// ENCARGOS / PEDIDOS ESPECIALES DE CLIENTES (CUSTOM REQUESTS)
// =========================================================================
private customRequests: CustomProductRequest[] = [
{
id: 'req-1',
customerName: 'Santiago Mejía',
customerPhone: '573105559876',
customerCity: 'Cali',
brand: 'Jordan',
modelName: 'Air Jordan 4 Retro Travis Scott Cactus Jack',
size: '41',
colorway: 'University Blue / Black-Varsity Red',
targetBudget: 220000,
notes: 'Las busco en calidad 1.1 o G5 con caja y llavero si es posible.',
imageUrl: 'https://images.unsplash.com/photo-1552346154-21d32810aba3?auto=format&fit=crop&w=800&q=80',
status: 'searching',
quotedPrice: 195000,
createdAt: '2026-09-02T16:20:00Z',
},
{
id: 'req-2',
customerName: 'Valeria Osorio',
customerPhone: '573001234567',
customerCity: 'Medellín',
brand: 'Adidas',
modelName: 'Wales Bonner x Samba Pony Tonal',
size: '38',
colorway: 'Cream White / Mystery Brown',
targetBudget: 180000,
notes: 'Quiero la versión con lengüeta doblada.',
imageUrl: 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
status: 'pending',
createdAt: '2026-09-03T11:15:00Z',
},
];
private customRequests: CustomProductRequest[] = loadPersistedData<CustomProductRequest[]>(
'persisted_custom_requests.json',
[
{
id: 'req-1',
customerName: 'Santiago Mejía',
customerPhone: '573105559876',
customerCity: 'Cali',
brand: 'Jordan',
modelName: 'Air Jordan 4 Retro Travis Scott Cactus Jack',
size: '41',
colorway: 'University Blue / Black-Varsity Red',
targetBudget: 220000,
notes: 'Las busco en calidad 1.1 o G5 con caja y llavero si es posible.',
imageUrl: 'https://images.unsplash.com/photo-1552346154-21d32810aba3?auto=format&fit=crop&w=800&q=80',
status: 'searching',
quotedPrice: 195000,
createdAt: '2026-09-02T16:20:00Z',
},
{
id: 'req-2',
customerName: 'Valeria Osorio',
customerPhone: '573001234567',
customerCity: 'Medellín',
brand: 'Adidas',
modelName: 'Wales Bonner x Samba Pony Tonal',
size: '38',
colorway: 'Cream White / Mystery Brown',
targetBudget: 180000,
notes: 'Quiero la versión con lengüeta doblada.',
imageUrl: 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80',
status: 'pending',
createdAt: '2026-09-03T11:15:00Z',
},
]
);
getCustomRequests(userId?: string): CustomProductRequest[] {
if (!this.customRequests || this.customRequests.length === 0) {
this.customRequests = loadPersistedData<CustomProductRequest[]>(
'persisted_custom_requests.json',
[]
);
}
if (userId) {
return this.customRequests.filter((r) => r.userId === userId);
}
return [...this.customRequests];
}
setCustomRequests(requests: CustomProductRequest[]): void {
this.customRequests = requests;
savePersistedData('persisted_custom_requests.json', this.customRequests);
}
getCustomRequestById(id: string): CustomProductRequest | undefined {
return this.customRequests.find((r) => r.id === id);
return this.getCustomRequests().find((r) => r.id === id);
}
addCustomRequest(
@ -1225,6 +1239,7 @@ class DataService {
createdAt: data.createdAt || new Date().toISOString(),
};
this.customRequests.unshift(newRequest);
savePersistedData('persisted_custom_requests.json', this.customRequests);
return newRequest;
}
@ -1236,13 +1251,18 @@ class DataService {
...updates,
updatedAt: new Date().toISOString(),
};
savePersistedData('persisted_custom_requests.json', this.customRequests);
return this.customRequests[idx];
}
deleteCustomRequest(id: string): boolean {
const prev = this.customRequests.length;
this.customRequests = this.customRequests.filter((r) => r.id !== id);
return this.customRequests.length < prev;
const deleted = this.customRequests.length < prev;
if (deleted) {
savePersistedData('persisted_custom_requests.json', this.customRequests);
}
return deleted;
}
}

View File

@ -9,6 +9,7 @@ import {
SupplierPost,
MonitoredWhatsAppGroup,
MessageTemplate,
CustomProductRequest,
} from '@/types';
import { INITIAL_CITIES, INITIAL_PAYMENT_METHODS, INITIAL_SUPPLIERS } from './data-service';
import { DEFAULT_MESSAGE_TEMPLATES } from './templates-service';
@ -868,4 +869,133 @@ export async function updateMessageTemplateInAppwrite(
}
}
// =========================================================================
// 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;
}
}

View File

@ -0,0 +1,138 @@
import paramiko
import os
import urllib.request
import json
import ssl
import sys
import io
import time
import subprocess
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
HOST = "161.97.93.156"
USER = "carlos"
PASSWORD = "Innova1978+*?"
REMOTE_BASE = "/home/carlos/apps/dripzone-app"
LOCAL_APK = r"c:\Users\Carlos\dev\almeira-web\dripzone-app\public\DripZone.apk"
REMOTE_APK = f"{REMOTE_BASE}/public/DripZone.apk"
EVOLUTION_API_URL = "https://evolution.almeira.com.co"
STORE_INSTANCE_NAME = "DripZone"
STORE_API_KEY = "83B5BD230796-4324-AE73-3508F69E279D"
TARGET_PHONE = "573167455877"
APK_URL = "https://drip-zone.online/DripZone.apk"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def main():
print("[1/5] Verificando APK local...")
if not os.path.exists(LOCAL_APK):
print(f" [ERROR] El archivo {LOCAL_APK} no existe.")
sys.exit(1)
size_mb = os.path.getsize(LOCAL_APK) / (1024 * 1024)
print(f" -> APK local verificado: {LOCAL_APK} ({size_mb:.2f} MB)")
print("[2/5] Guardando cambios y pusheando a Gitea...")
try:
subprocess.run(["git", "add", "."], cwd=r"c:\Users\Carlos\dev\almeira-web", check=False)
msg = "feat: gestion completa de Encargos y Pedidos Especiales (editar, eliminar, crear, sincronizar con Appwrite)"
subprocess.run(["git", "commit", "-m", msg], cwd=r"c:\Users\Carlos\dev\almeira-web", check=False)
push_res = subprocess.run(["git", "push", "origin", "main"], cwd=r"c:\Users\Carlos\dev\almeira-web", capture_output=True, text=True)
print(" -> Git push:", push_res.stdout.strip() or push_res.stderr.strip())
except Exception as e:
print(" [WARN Git]:", e)
print("[3/5] Subiendo APK a VPS 161.97.93.156 vía SFTP...")
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=HOST, username=USER, password=PASSWORD, timeout=30)
sftp = ssh.open_sftp()
try:
sftp.mkdir(f"{REMOTE_BASE}/public")
except:
pass
sftp.put(LOCAL_APK, REMOTE_APK)
sftp.close()
print(" [OK] Archivo APK subido por SFTP al VPS.")
print("[4/5] Copiando APK dentro de los contenedores Docker en VPS...")
cmd = f"""
docker cp {REMOTE_APK} dripzone-production:/app/public/DripZone.apk || true
docker cp {REMOTE_APK} dripzone-production:/app/public/dripzone.apk || true
docker cp {REMOTE_APK} dripzone-production:/app/DripZone.apk || true
for c in $(docker ps -q --filter "name=bfmnoga3"); do
docker cp {REMOTE_APK} $c:/app/public/DripZone.apk || true
docker cp {REMOTE_APK} $c:/app/public/dripzone.apk || true
docker cp {REMOTE_APK} $c:/app/DripZone.apk || true
done
docker exec dripzone-production ls -lh /app/public/DripZone.apk || true
"""
stdin, stdout, stderr = ssh.exec_command(cmd)
out = stdout.read().decode('utf-8', errors='replace')
print(" -> Salida Docker:\n", out.strip())
ssh.close()
time.sleep(2)
try:
req = urllib.request.Request(APK_URL, method='HEAD')
with urllib.request.urlopen(req, context=ctx, timeout=15) as res:
live_size_mb = int(res.headers.get('Content-Length', 0)) / (1024 * 1024)
print(f" [OK] APK online verificado en {APK_URL} (HTTP {res.status}, {live_size_mb:.2f} MB)")
except Exception as e:
print(f" [WARN] Verificación HEAD APK: {e}")
print("[5/5] Enviando notificación al WhatsApp de Carlos (+57 316 745 5877)...")
message_text = (
"👟 *DripZone - Módulo de Encargos Especiales Habilitado al 100%*\n\n"
"Hola Carlos, ya puedes *editar, eliminar, actualizar y crear* encargos y pedidos especiales:\n\n"
"🛠️ *Nuevas Funcionalidades en el Panel Administrativo:*\n"
"• ✏️ *Editar Todo:* Ahora puedes modificar cualquier dato del encargo (cliente, WhatsApp, ciudad, marca, modelo exacto, talla, colorway, presupuesto, notas, precio cotizado y estado).\n"
"• 🖼️ *Cambiar o Subir Foto:* Permite reemplazar o subir una nueva foto para el encargo directamente desde tu computador.\n"
"• 🗑️ *Eliminar con Confirmación:* Botón de papelera en cada tarjeta y en el modal para descartar solicitudes no deseadas o de prueba.\n"
"• ➕ *Crear Encargo Manual:* Botón '+ Nuevo Encargo' para registrar solicitudes recibidas directamente por llamada o chat.\n"
"• ☁️ *Persistencia en la Nube (Appwrite):* Creada y conectada la colección `custom_requests` en Appwrite Cloud, garantizando que nunca se pierdan los cambios al reiniciar el servidor.\n\n"
"👉 *Pruébalo ahora en el panel:* https://drip-zone.online/admin/encargos\n"
f"📲 *APK Android v2.5:* {APK_URL}"
)
req_text = urllib.request.Request(
f"{EVOLUTION_API_URL}/message/sendText/{STORE_INSTANCE_NAME}",
data=json.dumps({"number": TARGET_PHONE, "text": message_text, "delay": 500}).encode("utf-8"),
headers={"apikey": STORE_API_KEY, "Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req_text, context=ctx, timeout=20) as resp:
print(f" [OK] Notificación enviada a WhatsApp (Status: {resp.status})")
except Exception as e:
print(f" [WARN] Fallo enviando texto: {e}")
req_media = urllib.request.Request(
f"{EVOLUTION_API_URL}/message/sendMedia/{STORE_INSTANCE_NAME}",
data=json.dumps({
"number": TARGET_PHONE,
"mediatype": "document",
"mimetype": "application/vnd.android.package-archive",
"fileName": "DripZone-v2.5-Encargos-Fix.apk",
"caption": "📲 *DripZone Android v2.5 - Encargos y Pedidos Especiales CRUD*",
"media": APK_URL
}).encode("utf-8"),
headers={"apikey": STORE_API_KEY, "Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req_media, context=ctx, timeout=180) as resp:
print(f" [OK] Archivo APK enviado como documento (Status: {resp.status})")
except Exception as e:
print(f" [WARN] Fallo enviando documento APK: {e}")
print("\n[DESPLIEGUE Y NOTIFICACIÓN COMPLETADOS CON ÉXITO]")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,95 @@
import urllib.request
import urllib.error
import json
import ssl
import time
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
endpoint = 'https://appwrite.almeira.com.co/v1'
project = '69b84df5003bd8957c5c'
key = 'standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3'
db = 'sneakers_multistore_db'
col_id = 'custom_requests'
headers = {
'X-Appwrite-Project': project,
'X-Appwrite-Key': key,
'Content-Type': 'application/json'
}
# 1. Crear colección si no existe
check_url = f'{endpoint}/databases/{db}/collections/{col_id}'
try:
req = urllib.request.Request(check_url, headers=headers)
with urllib.request.urlopen(req, context=ctx, timeout=10) as r:
print('[OK] La colección custom_requests ya existe en Appwrite.')
except urllib.error.HTTPError as e:
if e.code == 404:
print('Creando colección custom_requests...')
url = f'{endpoint}/databases/{db}/collections'
body = json.dumps({
'collectionId': col_id,
'name': 'Encargos y Pedidos Especiales',
'permissions': ['read("any")', 'create("any")', 'update("any")', 'delete("any")']
}).encode('utf-8')
req = urllib.request.Request(url, data=body, headers=headers)
with urllib.request.urlopen(req, context=ctx, timeout=10) as r:
print('[OK] Colección creada:', r.read().decode())
else:
print('Error consultando colección:', e.code, e.read().decode())
# 2. Atributos
attributes = [
{'type': 'string', 'key': 'customerName', 'size': 255, 'required': True},
{'type': 'string', 'key': 'customerPhone', 'size': 64, 'required': True},
{'type': 'string', 'key': 'customerEmail', 'size': 255, 'required': False},
{'type': 'string', 'key': 'customerCity', 'size': 128, 'required': False},
{'type': 'string', 'key': 'brand', 'size': 128, 'required': True},
{'type': 'string', 'key': 'modelName', 'size': 255, 'required': True},
{'type': 'string', 'key': 'size', 'size': 32, 'required': True},
{'type': 'string', 'key': 'colorway', 'size': 255, 'required': False},
{'type': 'integer', 'key': 'targetBudget', 'required': False, 'min': 0, 'max': 100000000},
{'type': 'string', 'key': 'notes', 'size': 2000, 'required': False},
{'type': 'string', 'key': 'imageUrl', 'size': 1000, 'required': False},
{'type': 'string', 'key': 'status', 'size': 32, 'required': True},
{'type': 'integer', 'key': 'quotedPrice', 'required': False, 'min': 0, 'max': 100000000},
{'type': 'string', 'key': 'adminNotes', 'size': 2000, 'required': False},
{'type': 'string', 'key': 'createdAt', 'size': 64, 'required': False},
{'type': 'string', 'key': 'updatedAt', 'size': 64, 'required': False},
{'type': 'string', 'key': 'userId', 'size': 128, 'required': False},
]
attr_base_url = f'{endpoint}/databases/{db}/collections/{col_id}/attributes'
for attr in attributes:
attr_type = attr['type']
key_name = attr['key']
url = f'{attr_base_url}/{attr_type}'
payload = {
'key': key_name,
'required': attr['required']
}
if attr_type == 'string':
payload['size'] = attr['size']
elif attr_type == 'integer':
if 'min' in attr:
payload['min'] = attr['min']
if 'max' in attr:
payload['max'] = attr['max']
req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers=headers)
try:
with urllib.request.urlopen(req, context=ctx, timeout=10) as r:
print(f' [+] Atributo {key_name} ({attr_type}) creado.')
except urllib.error.HTTPError as e:
if e.code == 409:
print(f' [=] Atributo {key_name} ya existe.')
else:
print(f' [!] Error en {key_name}:', e.code, e.read().decode())
time.sleep(0.3)
print('[FIN] Colección y atributos configurados en Appwrite.')