) {
+ orders = newOrders
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OrderViewHolder {
+ val binding = ItemOrderCardBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return OrderViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: OrderViewHolder, position: Int) {
+ holder.bind(orders[position])
+ }
+
+ override fun getItemCount(): Int = orders.size
+
+ inner class OrderViewHolder(private val binding: ItemOrderCardBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(order: Order) {
+ val context = binding.root.context
+
+ // 1. Order ID & Date
+ binding.tvCardOrderId.text = "Orden #${order.id.takeLast(8).uppercase()}"
+ binding.tvCardOrderDate.text = formatOrderDate(order.createdAt)
+
+ // 2. Status Badge
+ bindStatusBadge(order.status)
+
+ // 3. Products list (Dynamic injection)
+ binding.llOrderItemsContainer.removeAllViews()
+ for (item in order.items) {
+ val itemView = createProductItemView(context, item.productName, item.size, item.quantity, item.price)
+ binding.llOrderItemsContainer.addView(itemView)
+ }
+
+ // 4. Financial Breakdown
+ binding.tvCardSubtotal.text = formatCop(order.subtotal)
+ binding.tvCardShippingCost.text = if (order.shippingCost <= 0) "GRATIS" else formatCop(order.shippingCost)
+ binding.tvCardTotal.text = formatCop(order.totalAmount)
+
+ // 5. Deposit Box
+ val hasDepositPaid = order.depositAmount > 0 && order.status != "pending"
+ if (hasDepositPaid) {
+ binding.tvDepositStatusLabel.text = "${formatCop(order.depositAmount)} • Acreditado ✅"
+ binding.tvDepositStatusLabel.setTextColor(Color.parseColor("#22C55E"))
+ binding.btnPayDepositBold.visibility = View.GONE
+ } else {
+ binding.tvDepositStatusLabel.text = "$10.000 COP • Pendiente ⏳"
+ binding.tvDepositStatusLabel.setTextColor(Color.parseColor("#F59E0B"))
+ binding.btnPayDepositBold.visibility = View.VISIBLE
+ binding.btnPayDepositBold.setOnClickListener {
+ onPayDepositClicked(order)
+ }
+ }
+
+ // 6. Saldo Contraentrega
+ val balance = if (order.balanceDue > 0) order.balanceDue else (order.totalAmount - order.depositAmount)
+ binding.tvCardBalanceDue.text = "${formatCop(balance)} COP"
+
+ // 7. Address
+ binding.tvCardAddress.text = "📍 Entrega en: ${order.address} (${order.cityName})"
+
+ // 8. WhatsApp Status Inquiry
+ binding.btnOrderWhatsAppStatus.setOnClickListener {
+ onInquireStatusClicked(order)
+ }
+ }
+
+ private fun bindStatusBadge(status: String?) {
+ val (badgeText, badgeColor) = when ((status ?: "pending").lowercase()) {
+ "deposit_received" -> "⚡ Anticipo Acreditado" to "#22C55E"
+ "pending" -> "⏳ Anticipo Pendiente" to "#F59E0B"
+ "processing" -> "📦 En Preparación" to "#3B82F6"
+ "shipped" -> "🚚 Despachado / En Ruta" to "#A855F7"
+ "delivered" -> "✅ Entregado con Éxito" to "#10B981"
+ "cancelled", "rejected" -> "❌ Pedido Cancelado" to "#EF4444"
+ else -> "📋 Registrado" to "#8B5CF6"
+ }
+ binding.tvCardOrderStatusBadge.text = badgeText
+ binding.tvCardOrderStatusBadge.setTextColor(Color.parseColor(badgeColor))
+ }
+
+ private fun createProductItemView(
+ context: Context,
+ name: String,
+ size: String,
+ quantity: Int,
+ price: Double
+ ): View {
+ val row = LinearLayout(context).apply {
+ orientation = LinearLayout.HORIZONTAL
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT
+ ).apply {
+ setMargins(0, 4, 0, 4)
+ }
+ }
+
+ val icon = ImageView(context).apply {
+ setImageResource(R.drawable.ic_package)
+ setColorFilter(Color.parseColor("#8B5CF6"))
+ layoutParams = LinearLayout.LayoutParams(36, 36).apply {
+ marginEnd = 16
+ }
+ }
+
+ val detailsLayout = LinearLayout(context).apply {
+ orientation = LinearLayout.VERTICAL
+ layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
+ }
+
+ val tvTitle = TextView(context).apply {
+ text = name
+ textSize = 13f
+ setTextColor(Color.parseColor("#FFFFFF"))
+ typeface = android.graphics.Typeface.DEFAULT_BOLD
+ }
+
+ val tvSub = TextView(context).apply {
+ text = "Talla: $size • Cantidad: $quantity"
+ textSize = 11f
+ setTextColor(Color.parseColor("#94A3B8"))
+ }
+
+ detailsLayout.addView(tvTitle)
+ detailsLayout.addView(tvSub)
+
+ val tvPrice = TextView(context).apply {
+ text = formatCop(price * quantity)
+ textSize = 13f
+ setTextColor(Color.parseColor("#FFFFFF"))
+ typeface = android.graphics.Typeface.DEFAULT_BOLD
+ }
+
+ row.addView(icon)
+ row.addView(detailsLayout)
+ row.addView(tvPrice)
+
+ return row
+ }
+
+ private fun formatCop(value: Double): String {
+ return copFormat.format(value)
+ }
+
+ private fun formatOrderDate(isoDate: String?): String {
+ if (isoDate.isNullOrBlank()) return "Reciente"
+ return try {
+ val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())
+ val outputFormat = SimpleDateFormat("dd MMM yyyy, hh:mm a", Locale("es", "CO"))
+ val date = inputFormat.parse(isoDate.take(19))
+ if (date != null) outputFormat.format(date) else isoDate.take(10)
+ } catch (_: Exception) {
+ isoDate.take(10)
+ }
+ }
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/OrdersBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/OrdersBottomSheet.kt
new file mode 100644
index 0000000..e4cfdb9
--- /dev/null
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/OrdersBottomSheet.kt
@@ -0,0 +1,159 @@
+package co.dripzone.app.ui
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import co.dripzone.app.api.ApiClient
+import co.dripzone.app.databinding.BottomSheetOrdersBinding
+import co.dripzone.app.manager.TemplateManager
+import co.dripzone.app.model.Order
+import co.dripzone.app.model.UserManager
+import co.dripzone.app.security.SecurityManager
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import kotlinx.coroutines.launch
+
+class OrdersBottomSheet : BottomSheetDialogFragment() {
+
+ private var _binding: BottomSheetOrdersBinding? = null
+ private val binding get() = _binding!!
+
+ private lateinit var ordersAdapter: OrdersAdapter
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = BottomSheetOrdersBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ setupRecyclerView()
+ setupListeners()
+ loadOrders()
+ }
+
+ private fun setupRecyclerView() {
+ ordersAdapter = OrdersAdapter(
+ orders = emptyList(),
+ onPayDepositClicked = { order ->
+ handlePayDeposit(order)
+ },
+ onInquireStatusClicked = { order ->
+ handleInquireWhatsApp(order)
+ }
+ )
+
+ binding.rvOrders.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ adapter = ordersAdapter
+ }
+ }
+
+ private fun setupListeners() {
+ binding.btnCloseOrders.setOnClickListener {
+ dismiss()
+ }
+
+ binding.btnExploreCatalog.setOnClickListener {
+ dismiss()
+ }
+ }
+
+ private fun loadOrders() {
+ val user = UserManager.getUser(requireContext())
+ val phone = user.phone.replace("\\D".toRegex(), "")
+
+ if (phone.isBlank()) {
+ binding.pbOrdersLoading.visibility = View.GONE
+ binding.layoutEmptyOrders.visibility = View.VISIBLE
+ binding.tvOrdersUserPhone.text = "Inicia sesión para consultar tus pedidos"
+ binding.tvEmptyOrdersMessage.text = "Ingresa a 'Mi Cuenta' con tu número de WhatsApp para ver tu historial de compras y hacer seguimiento en tiempo real."
+ binding.btnExploreCatalog.text = "Iniciar Sesión / Mi Cuenta 🔐"
+ binding.btnExploreCatalog.setOnClickListener {
+ dismiss()
+ AccountBottomSheet().show(parentFragmentManager, AccountBottomSheet.TAG)
+ }
+ return
+ }
+
+ val masked = if (phone.length >= 10) "${phone.take(3)}***${phone.takeLast(4)}" else phone
+ binding.tvOrdersUserPhone.text = "Pedidos registrados para: +57 $masked"
+ binding.pbOrdersLoading.visibility = View.VISIBLE
+ binding.rvOrders.visibility = View.GONE
+ binding.layoutEmptyOrders.visibility = View.GONE
+
+ lifecycleScope.launch {
+ val result = ApiClient.getOrdersByPhone(phone)
+ binding.pbOrdersLoading.visibility = View.GONE
+
+ result.onSuccess { orders ->
+ if (orders.isEmpty()) {
+ binding.layoutEmptyOrders.visibility = View.VISIBLE
+ binding.rvOrders.visibility = View.GONE
+ binding.tvEmptyOrdersMessage.text = "No encontramos pedidos registrados para el número +57 $masked."
+ binding.btnExploreCatalog.text = "Explorar Catálogo 🔥"
+ binding.btnExploreCatalog.setOnClickListener { dismiss() }
+ } else {
+ binding.layoutEmptyOrders.visibility = View.GONE
+ binding.rvOrders.visibility = View.VISIBLE
+ ordersAdapter.submitList(orders)
+ }
+ }.onFailure { err ->
+ binding.layoutEmptyOrders.visibility = View.VISIBLE
+ binding.tvEmptyOrdersMessage.text = "Error consultando pedidos: ${err.message}"
+ }
+ }
+ }
+
+ private fun handlePayDeposit(order: Order) {
+ val boldUrl = order.boldCheckoutUrl ?: "https://drip-zone.online"
+ try {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(boldUrl))
+ startActivity(intent)
+ } catch (_: Exception) {
+ Toast.makeText(requireContext(), "No se pudo abrir la pasarela Bold", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun handleInquireWhatsApp(order: Order) {
+ val user = UserManager.getUser(requireContext())
+ val itemsSummary = order.items.joinToString(", ") { "${it.productName} (Talla ${it.size})" }
+
+ val variables = mapOf(
+ "tienda_nombre" to "DripZone",
+ "orden_id" to order.id.takeLast(8).uppercase(),
+ "cliente_nombre" to (user.name.ifBlank { "Cliente DripZone" }),
+ "cliente_telefono" to user.phone,
+ "cliente_direccion" to order.address,
+ "cliente_ciudad" to order.cityName,
+ "articulos_resumen" to itemsSummary,
+ "subtotal" to "$${order.subtotal.toInt()}",
+ "costo_envio" to (if (order.shippingCost <= 0) "GRATIS" else "$${order.shippingCost.toInt()}"),
+ "total" to "$${order.totalAmount.toInt()}",
+ "condicion_pago_resumen" to "Anticipo: $${order.depositAmount.toInt()} • Saldo: $${order.balanceDue.toInt()}",
+ "metodo_pago" to (order.paymentMethodName ?: "Contraentrega")
+ )
+
+ val message = TemplateManager.renderTemplate(requireContext(), "order_checkout_wa", variables)
+ SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ companion object {
+ const val TAG = "OrdersBottomSheet"
+ }
+}
diff --git a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
index dd13ff0..3023920 100644
--- a/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
+++ b/dripzone-app/android/app/src/main/java/co/dripzone/app/ui/ProductDetailBottomSheet.kt
@@ -78,6 +78,22 @@ class ProductDetailBottomSheet : BottomSheetDialogFragment() {
dismiss()
}
+ // Order or Inquire directly via WhatsApp using registered product_direct_inquiry template
+ binding.btnOrderWhatsAppDirect.setOnClickListener {
+ val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "38-43"
+ val variables = mapOf(
+ "tienda_nombre" to "DripZone",
+ "zapatilla_nombre" to p.name,
+ "color_nombre" to "Estilo Oficial",
+ "talla" to sizeToOrder,
+ "precio" to p.formattedPrice(),
+ "ciudad" to "Colombia",
+ "marca" to p.brand
+ )
+ val message = co.dripzone.app.manager.TemplateManager.renderTemplate(requireContext(), "product_direct_inquiry", variables)
+ SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
+ }
+
// Custom order button inside bottom sheet
binding.btnCustomOrderSheet.setOnClickListener {
dismiss()
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_category_chip.xml b/dripzone-app/android/app/src/main/res/drawable/bg_category_chip.xml
new file mode 100644
index 0000000..9ee467c
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_category_chip.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/bg_category_chip_active.xml b/dripzone-app/android/app/src/main/res/drawable/bg_category_chip_active.xml
new file mode 100644
index 0000000..912ba07
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/bg_category_chip_active.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/ic_package.xml b/dripzone-app/android/app/src/main/res/drawable/ic_package.xml
new file mode 100644
index 0000000..54b8643
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/ic_package.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/drawable/ic_user.xml b/dripzone-app/android/app/src/main/res/drawable/ic_user.xml
new file mode 100644
index 0000000..2a5d2be
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/drawable/ic_user.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/activity_main.xml b/dripzone-app/android/app/src/main/res/layout/activity_main.xml
index 933a596..cc86815 100644
--- a/dripzone-app/android/app/src/main/res/layout/activity_main.xml
+++ b/dripzone-app/android/app/src/main/res/layout/activity_main.xml
@@ -136,9 +136,9 @@
android:visibility="gone" />
-
+
@@ -463,9 +464,9 @@
android:textSize="10sp" />
-
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_account.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_account.xml
new file mode 100644
index 0000000..4ff8639
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_account.xml
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml
index aa023cd..7528cd0 100644
--- a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_order_verification.xml
@@ -78,7 +78,7 @@
android:layout_height="46dp"
android:layout_marginTop="6dp"
android:background="@drawable/bg_search_input"
- android:hint="Ej. Juan Carlos Pérez"
+ android:hint="Ej. Juan Gómez"
android:inputType="textPersonName"
android:paddingHorizontal="14dp"
android:textColor="@color/text_primary"
@@ -319,7 +319,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
- android:text="Tu número fue verificado con éxito y tu pedido ya está registrado en nuestra plataforma.\n\nLa plataforma ha notificado automáticamente al administrador Carlos (+57 316 745 5877) y recibirás confirmación oficial a tu WhatsApp."
+ android:text="Tu número fue verificado con éxito y tu pedido ya está registrado en nuestra plataforma.\n\nLa tienda ha sido notificada automáticamente y recibirás confirmación oficial a tu WhatsApp."
android:textColor="@color/text_secondary"
android:textSize="12sp" />
@@ -440,19 +440,19 @@
app:iconSize="18dp" />
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_orders.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_orders.xml
new file mode 100644
index 0000000..6c90c60
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_orders.xml
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
index cb20da2..50aa8ec 100644
--- a/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
+++ b/dripzone-app/android/app/src/main/res/layout/bottom_sheet_product_detail.xml
@@ -170,6 +170,27 @@
app:iconPadding="8dp"
app:iconSize="20dp" />
+
+
+
@@ -336,7 +336,7 @@
android:layout_height="50dp"
android:layout_marginTop="18dp"
android:filterTouchesWhenObscured="true"
- android:text="Abrir WhatsApp del Asesor Carlos"
+ android:text="Contactar a Soporte DripZone"
android:textAllCaps="false"
android:textColor="#FFFFFF"
android:textSize="13sp"
diff --git a/dripzone-app/android/app/src/main/res/layout/item_order_card.xml b/dripzone-app/android/app/src/main/res/layout/item_order_card.xml
new file mode 100644
index 0000000..8d99437
--- /dev/null
+++ b/dripzone-app/android/app/src/main/res/layout/item_order_card.xml
@@ -0,0 +1,302 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dripzone-app/android/app/src/main/res/layout/item_product.xml b/dripzone-app/android/app/src/main/res/layout/item_product.xml
index 9c6baab..6889a28 100644
--- a/dripzone-app/android/app/src/main/res/layout/item_product.xml
+++ b/dripzone-app/android/app/src/main/res/layout/item_product.xml
@@ -94,44 +94,39 @@
android:textColor="@color/text_secondary"
android:textSize="11sp" />
-
-
+
+ android:fontFamily="sans-serif-black"
+ android:maxLines="1"
+ android:singleLine="true"
+ android:text="$ 280.000"
+ android:textColor="@color/drip_purple_glow"
+ android:textSize="15sp" />
-
-
-
-
-
+
+
diff --git a/dripzone-app/android/app/src/main/res/values/colors.xml b/dripzone-app/android/app/src/main/res/values/colors.xml
index ea9e031..2d8f236 100644
--- a/dripzone-app/android/app/src/main/res/values/colors.xml
+++ b/dripzone-app/android/app/src/main/res/values/colors.xml
@@ -15,6 +15,8 @@
#9333EA
+ #9333EA
+ #282830
#A855F7
#D946EF
#EC4899
diff --git a/dripzone-app/scripts/seed-templates-appwrite.js b/dripzone-app/scripts/seed-templates-appwrite.js
new file mode 100644
index 0000000..e09a68f
--- /dev/null
+++ b/dripzone-app/scripts/seed-templates-appwrite.js
@@ -0,0 +1,215 @@
+const { Client, Databases } = require('node-appwrite');
+const fs = require('fs');
+const path = require('path');
+
+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);
+
+const databases = new Databases(client);
+
+// Import templates from compiled or read ts file directly
+async function seed() {
+ console.log('🌱 Seeding message templates into Appwrite...');
+
+ // Read templates-service.ts to extract default templates
+ const templateServiceFile = fs.readFileSync(path.resolve(__dirname, '../src/lib/templates-service.ts'), 'utf-8');
+
+ // Define templates matching DEFAULT_MESSAGE_TEMPLATES
+ const templates = [
+ {
+ id: 'order_confirmation_customer',
+ title: 'Confirmación de Pedido al Cliente',
+ description: 'Mensaje oficial enviado al cliente tras registrar exitosamente su pedido en la tienda.',
+ category: 'customer_sales',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `🎉 *¡GRACIAS POR TU PEDIDO EN {tienda_nombre}!* 🎉\n📌 *No. de Orden:* #{orden_id}\n\n👤 *Cliente:* {cliente_nombre}\n📱 *Teléfono:* {cliente_telefono}\n📍 *Dirección:* {cliente_direccion}\n🏙️ *Ciudad:* {cliente_ciudad}\n\n📦 *DETALLE DE TU COMPRA:*\n{articulos_resumen}\n\n💵 *Subtotal:* {subtotal}\n🚚 *Envío:* {costo_envio}\n💰 *VALOR TOTAL:* {total}\n{condicion_pago_resumen}\n\n💳 *Método de Pago:* {metodo_pago}\n\nEstamos alistando tu paquete. Te enviaremos el número de guía o notificación de entrega. ¡Muchas gracias por tu confianza! ✨`,
+ availableVariables: JSON.stringify([
+ { key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone', description: 'Nombre comercial' },
+ { key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'ID orden' },
+ { key: 'cliente_nombre', label: 'Nombre del Cliente', example: 'Cliente DripZone', description: 'Nombre' },
+ { key: 'cliente_telefono', label: 'Teléfono Cliente', example: '3106390296', description: 'Celular' },
+ { key: 'cliente_direccion', label: 'Dirección de Entrega', example: 'Calle 5 # 24-80', description: 'Dirección' },
+ { key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Ciudad' },
+ { key: 'articulos_resumen', label: 'Lista de Artículos', example: '👟 Nike Air Force 1 x1', description: 'Pares' },
+ { key: 'subtotal', label: 'Subtotal COP', example: '$125.000 COP', description: 'Subtotal' },
+ { key: 'costo_envio', label: 'Costo de Envío', example: 'GRATIS', description: 'Envío' },
+ { key: 'total', label: 'Total COP', example: '$125.000 COP', description: 'Total' },
+ { key: 'condicion_pago_resumen', label: 'Resumen Anticipo / Saldo', example: '🤝 Pago 100% Contraentrega', description: 'Condición' },
+ { key: 'metodo_pago', label: 'Método de Pago', example: 'Contraentrega', description: 'Método' },
+ { key: 'saldo_contraentrega', label: 'Saldo en Puerta', example: '$115.000 COP', description: 'Saldo' },
+ { key: 'anticipo', label: 'Valor Separación', example: '$10.000 COP', description: 'Anticipo' }
+ ]),
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'order_checkout_wa',
+ title: 'Pedido Web hacia WhatsApp de Tienda',
+ description: 'Texto que el cliente abre en WhatsApp al finalizar su compra en el carrito o checkout web.',
+ category: 'customer_sales',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `🎉 *CONFIRMACIÓN DE PEDIDO EN {tienda_nombre} #{orden_id}* 🎉\n\n👤 *Cliente:* {cliente_nombre}\n📱 *Teléfono:* {cliente_telefono}\n📍 *Dirección:* {cliente_direccion}, {cliente_ciudad}\n\n📦 *PRODUCTOS SOLICITADOS:*\n{articulos_resumen}\n\n💵 *Subtotal:* {subtotal}\n🚚 *Flete:* {costo_envio}\n💰 *TOTAL A PAGAR:* {total}\n{condicion_pago_resumen}\n💳 *Forma de Pago:* {metodo_pago}\n\n¡Por favor confirmar mi pedido para proceder con el alistamiento! 🔥`,
+ availableVariables: JSON.stringify([
+ { key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone', description: 'Nombre de la marca' },
+ { key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
+ { key: 'cliente_telefono', label: 'Teléfono', example: '3106390296', description: 'Teléfono' },
+ { key: 'cliente_direccion', label: 'Dirección', example: 'Av 6N # 28N-12', description: 'Dirección' },
+ { key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
+ { key: 'articulos_resumen', label: 'Productos', example: '👟 Jordan 4 Retro (41) x1', description: 'Resumen' },
+ { key: 'subtotal', label: 'Subtotal', example: '$140.000 COP', description: 'Subtotal' },
+ { key: 'costo_envio', label: 'Envío', example: 'GRATIS', description: 'Flete' },
+ { key: 'total', label: 'Total', example: '$140.000 COP', description: 'Total' },
+ { key: 'condicion_pago_resumen', label: 'Condición Pago', example: 'Separación $10.000', description: 'Condición' },
+ { key: 'metodo_pago', label: 'Método', example: 'Contraentrega', description: 'Método' }
+ ]),
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'order_status_dispatched',
+ title: 'Notificación de Pedido Despachado en Ruta',
+ description: 'Mensaje enviado cuando la transportadora o mensajero local recoge el paquete.',
+ category: 'logistics_shipping',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `🚚 *¡TU PEDIDO VA EN CAMINO!* 📦💨\n\nHola {cliente_nombre}, tus zapatillas ya fueron despachadas para entrega en *{cliente_ciudad}*.\n\n📌 *Orden:* #{orden_id}\n📍 *Dirección de Entrega:* {cliente_direccion}\n📦 *Empresa Transportadora:* {transportadora}\n🏷️ *Número de Guía / Rastreo:* *{numero_guia}*\n🤝 *Valor a Pagar al Recibir (Contraentrega):* *{saldo_contraentrega}*\n\nPor favor estar atento a la portería o celular. ¡Gracias por tu compra en {tienda_nombre}! 🔥`,
+ availableVariables: JSON.stringify([
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
+ { key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
+ { key: 'cliente_direccion', label: 'Dirección', example: 'Carrera 100 # 16-20', description: 'Dirección' },
+ { key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
+ { key: 'transportadora', label: 'Transportadora', example: 'Interrapidísimo / Mensajería Urbana', description: 'Operador' },
+ { key: 'numero_guia', label: 'Número de Guía', example: 'GUIA-8839201', description: 'Guía' },
+ { key: 'saldo_contraentrega', label: 'Saldo a Pagar', example: '$125.000 COP', description: 'Saldo contraentrega' },
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' }
+ ]),
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'order_status_delivered',
+ title: 'Confirmación de Pedido Entregado con Éxito',
+ description: 'Mensaje post-venta enviado cuando el mensajero confirma entrega exitosa.',
+ category: 'logistics_shipping',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `⭐ *¡PEDIDO ENTREGADO CON ÉXITO!* ⭐\n\nHola {cliente_nombre}, nuestro sistema confirma que tu orden *#{orden_id}* fue entregada exitosamente.\n\nEsperamos que disfrutes al máximo tus nuevas zapatillas. Recuerda que cuentas con garantía por defectos y cambio de talla.\n\n📸 ¡Sube tu foto a Instagram y etiquétanos en *{instagram_handle}* para un 10% de descuento en tu próxima compra!\n\n¡Muchas gracias por elegir {tienda_nombre}! ✨`,
+ availableVariables: JSON.stringify([
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
+ { key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
+ { key: 'instagram_handle', label: 'Instagram', example: '@dripzone.co', description: 'Cuenta IG' }
+ ]),
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'auth_login_otp',
+ title: 'Código OTP de Seguridad para Iniciar Sesión',
+ description: 'Código de 6 dígitos enviado por WhatsApp al usuario para autenticación sin contraseña.',
+ category: 'security_auth',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `🔐 *{tienda_nombre} - Código de Seguridad*\n\nHola {usuario_nombre}, tu código de verificación para acceder a tu cuenta es:\n\n👉 *{codigo_otp}*\n\n⏱️ _Válido por 5 minutos. No compartas este código con nadie._`,
+ availableVariables: JSON.stringify([
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
+ { key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Usuario' },
+ { key: 'codigo_otp', label: 'Código de 6 Dígitos', example: '849201', description: 'Código OTP' }
+ ]),
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'auth_register_otp',
+ title: 'Código OTP para Verificación de Nueva Cuenta',
+ description: 'Código enviado por WhatsApp al nuevo cliente tras registrar su cuenta en la plataforma.',
+ category: 'security_auth',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `👟 *¡Bienvenido(a) a {tienda_nombre}!* ✨\n\nHola {usuario_nombre}, ingresa el siguiente código en la app para verificar tu número de WhatsApp:\n\n👉 *{codigo_otp}*\n\nAl validar tu cuenta, habilitas compras 100% contraentrega sin fricción. ¡Gracias por unirte! 🔥`,
+ availableVariables: JSON.stringify([
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
+ { key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Nombre' },
+ { key: 'codigo_otp', label: 'Código de 6 Dígitos', example: '582194', description: 'Código OTP' }
+ ]),
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'customer_deposit_request',
+ title: 'Solicitud de Separación con $10.000 COP (Garantía de Despacho)',
+ description: 'Mensaje con enlace oficial Bold para pagar el anticipo de $10.000 COP y programar el envío.',
+ category: 'customer_sales',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `⚡ *{tienda_nombre} - CONFIRMACIÓN DE DESPACHO* 📦\n\nHola {cliente_nombre}, para programar el despacho express de tu orden *#{orden_id}* hacia *{cliente_ciudad}*, realiza el pago del anticipo de separación de *{valor_anticipo}*.\n\n💳 *Paga en línea de forma 100% segura por Bold (PSE / Tarjetas / Bre-B / Nequi):*\n👉 {link_pago_bold}\n\n🤝 El saldo restante de *{saldo_contraentrega}* lo cancelas al recibir tu paquete en tu puerta. ¡Gracias por tu compra!`,
+ availableVariables: JSON.stringify([
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Tienda' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Cliente' },
+ { key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
+ { key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
+ { key: 'valor_anticipo', label: 'Valor Anticipo', example: '$10.000 COP', description: 'Separación' },
+ { key: 'saldo_contraentrega', label: 'Saldo Contraentrega', example: '$125.000 COP', description: 'Saldo' },
+ { key: 'link_pago_bold', label: 'Link de Pago Bold', example: 'https://drip-zone.online', description: 'Enlace Bold' }
+ ]),
+ updatedAt: new Date().toISOString()
+ },
+ {
+ id: 'customer_deposit_confirmed',
+ title: 'Confirmación de Anticipo Bold al Cliente',
+ description: 'Notificación enviada al cliente por WhatsApp cuando su pago de anticipo de separación es acreditado por Bold.',
+ category: 'customer_sales',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\nHola *{cliente_nombre}*, recibimos correctamente tu pago de *{valor_anticipo}* a través de Bold.\n\n📦 Tu orden *#{orden_id}* ya se encuentra en alistamiento y empaque para despacho a *{cliente_ciudad}*.\n🤝 *Saldo restante a pagar contraentrega:* *{saldo_contraentrega}* al recibir en tu puerta.\n\nPuedes consultar el estado de tu pedido en cualquier momento desde la app oficial de {tienda_nombre}.\n\n¡Muchas gracias por tu compra en {tienda_nombre}!`,
+ availableVariables: JSON.stringify([
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre de la tienda' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre del comprador' },
+ { key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'ID de la orden' },
+ { key: 'valor_anticipo', label: 'Valor Anticipo', example: '$10.000 COP', description: 'Monto anticipo pagado' },
+ { key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Municipio de entrega' },
+ { key: 'saldo_contraentrega', label: 'Saldo Contraentrega', example: '$115.000 COP', description: 'Saldo en puerta' }
+ ]),
+ updatedAt: new Date().toISOString()
+ }
+ ];
+
+ for (const t of templates) {
+ try {
+ const doc = await databases.getDocument(databaseId, 'message_templates', t.id);
+ console.log(`ℹ️ Plantilla ${t.id} ya existe, actualizando...`);
+ await databases.updateDocument(databaseId, 'message_templates', t.id, {
+ title: t.title,
+ description: t.description,
+ category: t.category,
+ channel: t.channel,
+ content: t.content,
+ availableVariables: t.availableVariables,
+ isActive: t.isActive,
+ updatedAt: t.updatedAt
+ });
+ console.log(`✅ Plantilla ${t.id} actualizada.`);
+ } catch (e) {
+ console.log(`Creando plantilla ${t.id}...`);
+ await databases.createDocument(databaseId, 'message_templates', t.id, {
+ title: t.title,
+ description: t.description,
+ category: t.category,
+ channel: t.channel,
+ content: t.content,
+ availableVariables: t.availableVariables,
+ isActive: t.isActive,
+ updatedAt: t.updatedAt
+ });
+ console.log(`✅ Plantilla ${t.id} creada en Appwrite.`);
+ }
+ }
+
+ console.log('🎉 ¡Todas las plantillas quedaron sincronizadas en Appwrite!');
+}
+
+seed().catch(err => console.error('Error seeding templates:', err));
diff --git a/dripzone-app/src/app/admin/configuracion/page.tsx b/dripzone-app/src/app/admin/configuracion/page.tsx
index f63eb3d..567254c 100644
--- a/dripzone-app/src/app/admin/configuracion/page.tsx
+++ b/dripzone-app/src/app/admin/configuracion/page.tsx
@@ -14,10 +14,10 @@ export default function AdminSettingsPage() {
instagramHandle: '@dripzone.co',
address: 'Bodega Central / Cali',
city: 'Cali, Colombia',
- nequiAccount: '316 745 5877',
- daviplataAccount: '316 745 5877',
- accountHolder: 'Carlos Andrés Lasso',
- boldPaymentLink: 'https://bold.co/p/carlos-lasso',
+ nequiAccount: '310 639 0296',
+ daviplataAccount: '310 639 0296',
+ accountHolder: 'DripZone Oficial',
+ boldPaymentLink: 'https://drip-zone.online',
boldApiKey: '',
boldMerchantId: '',
boldQrUrl: '',
diff --git a/dripzone-app/src/app/api/auth/send-otp/route.ts b/dripzone-app/src/app/api/auth/send-otp/route.ts
index 332e451..f40ec89 100644
--- a/dripzone-app/src/app/api/auth/send-otp/route.ts
+++ b/dripzone-app/src/app/api/auth/send-otp/route.ts
@@ -11,22 +11,35 @@ export async function POST(request: Request) {
}
const cleanInput = identifier.trim();
- // Buscar usuario en base de datos
- const user = dataService.authenticate(cleanInput);
+ const cleanDigits = cleanInput.replace(/\D/g, '');
- let phoneToSend = cleanInput.replace(/\D/g, '');
- let userName = name?.trim() || (user ? user.name : 'Cliente');
+ let phoneToSend = '';
+ let userName = name?.trim() || 'Cliente';
+ let foundUser = null;
- if (user) {
- if (!user.isActive) {
+ // Si el identificador es un correo electrónico, buscar el teléfono de su cuenta
+ if (cleanInput.includes('@')) {
+ foundUser = dataService.authenticate(cleanInput);
+ if (!foundUser) {
+ return NextResponse.json({ success: false, error: 'No se encontró cuenta asociada a este correo.' }, { status: 404 });
+ }
+ if (!foundUser.isActive) {
return NextResponse.json({ success: false, error: 'Esta cuenta se encuentra inactiva.' }, { status: 403 });
}
- phoneToSend = user.phone.replace(/\D/g, '');
+ phoneToSend = foundUser.phone.replace(/\D/g, '');
+ userName = foundUser.name || userName;
+ } else {
+ // Si es un número telefónico, enviar DIRECTA Y EXCLUSIVAMENTE a ese número ingresado por el usuario
+ phoneToSend = cleanDigits;
+ foundUser = dataService.authenticate(cleanDigits);
+ if (foundUser?.name) {
+ userName = foundUser.name;
+ }
}
- if (!phoneToSend || phoneToSend.length < 7) {
+ if (!phoneToSend || phoneToSend.length < 10) {
return NextResponse.json(
- { success: false, error: 'Número de celular inválido. Ingresa un número válido de WhatsApp.' },
+ { success: false, error: 'Número de celular inválido. Ingresa tu número de WhatsApp a 10 dígitos.' },
{ status: 400 }
);
}
@@ -42,16 +55,14 @@ export async function POST(request: Request) {
codigo_otp: code,
});
- // Enviar a WhatsApp via Evolution API
+ // Enviar EXCLUSIVAMENTE a la línea de WhatsApp del usuario
const sent = await sendWhatsAppMessage(phoneToSend, message);
-
if (!sent) {
- // Si falla Evolution (o es teléfono internacional sin formato), permitir fallback seguro
console.warn(`[OTP] No se pudo enviar por WhatsApp a ${phoneToSend}. Código generado: ${code}`);
}
- // Enmascarar teléfono para la respuesta pública (ej: 316***5877)
+ // Enmascarar teléfono para la respuesta pública (ej: 310***0296)
const maskedPhone = phoneToSend.length >= 10
? `${phoneToSend.slice(0, 3)}***${phoneToSend.slice(-4)}`
: 'tu número';
@@ -60,7 +71,7 @@ export async function POST(request: Request) {
success: true,
message: `Código de 6 dígitos enviado por WhatsApp a ${maskedPhone}`,
phone: maskedPhone,
- isRegistered: !!user,
+ isRegistered: !!foundUser,
});
} catch (error: any) {
console.error('Error enviando OTP:', error);
diff --git a/dripzone-app/src/app/api/bold/status/route.ts b/dripzone-app/src/app/api/bold/status/route.ts
index 880af6c..e8f4c56 100644
--- a/dripzone-app/src/app/api/bold/status/route.ts
+++ b/dripzone-app/src/app/api/bold/status/route.ts
@@ -51,28 +51,36 @@ export async function GET(request: Request) {
dataService.updateOrder(order.id, updates);
await updateOrderInAppwrite(order.id, updates).catch(e => console.warn('Appwrite update warn:', e));
- // Notificar a Carlos por WhatsApp
+ // Notificar a la tienda automáticamente con la información completa del pago
try {
+ const itemsText = (order.items || [])
+ .map((i: any) => ` 👟 ${i.productName || i.name} (Talla ${i.size}) x${i.quantity} — ${formatCOP((i.price || 0) * (i.quantity || 1))}`)
+ .join('\n');
+
const adminMsg = (
`⚡ *¡ANTICIPO DE SEPARACIÓN CONFIRMADO VÍA BOLD!*\n\n` +
`📦 *Orden:* #${order.id}\n` +
`👤 *Cliente:* ${order.customerName} (+57 ${order.customerPhone})\n` +
- `📍 *Ciudad:* ${order.cityName}\n` +
- `💳 *Anticipo Recibido:* ${formatCOP(depositAmount)} COP (Confirmado Bold)\n` +
- `🤝 *Saldo a Cobrar Contraentrega:* ${formatCOP(newBalance)} COP\n\n` +
+ `📍 *Destino:* ${order.cityName} — ${order.address || 'Domicilio'}\n` +
+ `💳 *Anticipo Recibido:* ${formatCOP(depositAmount)} COP (Acreditado por Bold)\n` +
+ `🔗 *Referencia Bold:* ${effectiveLinkId}\n` +
+ `💵 *Total Pedido:* ${formatCOP(order.totalAmount)} COP\n` +
+ `🤝 *Saldo a Cobrar Contraentrega:* *${formatCOP(newBalance)} COP*\n\n` +
+ `📋 *Detalle de Calzado:*\n${itemsText || ' (Ver orden en panel)'}\n\n` +
`🚀 *Estado:* Despacho autorizado y habilitado para alistamiento en bodega.`
);
await sendWhatsAppMessage('573167455877', adminMsg);
- // Notificar al cliente
+ // Notificar al cliente automáticamente con la plantilla registrada en la base de datos
if (order.customerPhone) {
- const clientMsg = (
- `🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\n` +
- `Hola *${order.customerName}*, recibimos correctamente tu pago de *${formatCOP(depositAmount)} COP* a través de Bold.\n\n` +
- `📦 Tu orden *#${order.id}* ya se encuentra en alistamiento y empaque para despacho a *${order.cityName}*.\n` +
- `🤝 *Saldo restante a pagar contraentrega:* *${formatCOP(newBalance)} COP* al recibir en tu puerta.\n\n` +
- `¡Muchas gracias por tu compra en DripZone!`
- );
+ const clientMsg = dataService.renderMessageTemplate('customer_deposit_confirmed', {
+ tienda_nombre: 'DripZone',
+ cliente_nombre: order.customerName || 'Cliente',
+ orden_id: order.id,
+ valor_anticipo: formatCOP(depositAmount),
+ cliente_ciudad: order.cityName || 'Colombia',
+ saldo_contraentrega: formatCOP(newBalance),
+ });
await sendWhatsAppMessage(order.customerPhone, clientMsg);
}
} catch (waErr) {
diff --git a/dripzone-app/src/app/api/custom-requests/route.ts b/dripzone-app/src/app/api/custom-requests/route.ts
index e592c93..acdfcd9 100644
--- a/dripzone-app/src/app/api/custom-requests/route.ts
+++ b/dripzone-app/src/app/api/custom-requests/route.ts
@@ -4,9 +4,7 @@ import { CustomProductRequest } from '@/types';
import fs from 'fs';
import path from 'path';
-const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || 'https://evolution.almeira.com.co';
-const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || 'EE49029EEB85-4568-8F81-C9FDFC6E2943';
-const EVOLUTION_INSTANCE = process.env.EVOLUTION_INSTANCE_NAME || 'AutomataAlmeira';
+import { sendWhatsAppMessage } from '@/lib/evolution-api';
const CARLOS_PHONE = '573167455877';
// Carpeta local para almacenar fotos de encargos especiales
@@ -18,30 +16,6 @@ function ensureUploadsDir() {
}
}
-// Función auxiliar para enviar mensajes de WhatsApp vía Evolution API
-async function sendWhatsAppMessage(number: string, text: string) {
- try {
- const cleanNumber = number.replace(/\D/g, '');
- const fullNumber = cleanNumber.length === 10 ? `57${cleanNumber}` : cleanNumber;
-
- const res = await fetch(`${EVOLUTION_API_URL}/message/sendText/${EVOLUTION_INSTANCE}`, {
- method: 'POST',
- headers: {
- apikey: EVOLUTION_API_KEY,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- number: fullNumber,
- text: text,
- }),
- });
- return await res.json();
- } catch (err) {
- console.warn('Error enviando WhatsApp a ' + number + ':', err);
- return null;
- }
-}
-
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
@@ -142,13 +116,15 @@ ${customerEmail ? `✉️ *Correo:* ${customerEmail}\n` : ''}
sendWhatsAppMessage(CARLOS_PHONE, adminMessage).catch(() => {});
sendWhatsAppMessage('120363428012905103@g.us', adminMessage).catch(() => {});
- // 2. Confirmación automática al cliente por WhatsApp
- const clientMessage = `👋 ¡Hola *${customerName}*!
-Recibimos con éxito tu solicitud por encargo para las *${brand} ${modelName}* en talla *${size}*. 👟🔥
-
-Nuestro equipo ya está verificando disponibilidad y mejor precio con nuestras bodegas y proveedores mayoristas. Te responderemos a este mismo chat con la cotización exacta en el menor tiempo posible.
-
-¡Gracias por confiar en DripZone! 🚀`;
+ // 2. Confirmación automática al cliente por WhatsApp usando la plantilla registrada
+ const settings = dataService.getSettings();
+ const clientMessage = dataService.renderMessageTemplate('customer_request_received', {
+ tienda_nombre: settings?.storeName || 'DripZone Sneakers',
+ cliente_nombre: customerName,
+ marca: brand,
+ modelo: modelName,
+ talla: size,
+ });
sendWhatsAppMessage(customerPhone, clientMessage).catch(() => {});
diff --git a/dripzone-app/src/app/api/message-templates/route.ts b/dripzone-app/src/app/api/message-templates/route.ts
index 74ba94e..f9716e3 100644
--- a/dripzone-app/src/app/api/message-templates/route.ts
+++ b/dripzone-app/src/app/api/message-templates/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service';
import { TEMPLATE_CATEGORIES } from '@/lib/templates-service';
+import { getMessageTemplatesFromAppwrite, updateMessageTemplateInAppwrite } from '@/lib/server-appwrite';
export async function GET(request: Request) {
try {
@@ -8,6 +9,16 @@ export async function GET(request: Request) {
const id = searchParams.get('id');
const category = searchParams.get('category');
+ // Intentar sincronizar con Appwrite si hay plantillas en BD
+ try {
+ const appwriteTemplates = await getMessageTemplatesFromAppwrite();
+ if (appwriteTemplates && appwriteTemplates.length > 0) {
+ for (const t of appwriteTemplates) {
+ dataService.updateMessageTemplate(t.id, t);
+ }
+ }
+ } catch (_) {}
+
if (id) {
const template = dataService.getMessageTemplateById(id);
if (!template) {
@@ -37,12 +48,18 @@ export async function PUT(request: Request) {
return NextResponse.json({ success: false, error: 'ID de la plantilla es obligatorio' }, { status: 400 });
}
- const updated = dataService.updateMessageTemplate(id, {
+ const updates = {
...(content !== undefined && { content }),
...(isActive !== undefined && { isActive }),
...(title !== undefined && { title }),
...(description !== undefined && { description }),
- });
+ };
+
+ // Actualizar en Appwrite
+ const updatedInDb = await updateMessageTemplateInAppwrite(id, updates);
+
+ // Actualizar en memoria / dataService
+ const updated = dataService.updateMessageTemplate(id, updates);
if (!updated) {
return NextResponse.json({ success: false, error: 'Plantilla no encontrada' }, { status: 404 });
@@ -50,8 +67,8 @@ export async function PUT(request: Request) {
return NextResponse.json({
success: true,
- template: updated,
- message: `Plantilla "${updated.title}" actualizada correctamente.`,
+ template: updatedInDb || updated,
+ message: `Plantilla "${updated.title}" actualizada correctamente en la base de datos.`,
});
} catch (error: any) {
return NextResponse.json({ success: false, error: error.message }, { status: 400 });
diff --git a/dripzone-app/src/app/api/orders/route.ts b/dripzone-app/src/app/api/orders/route.ts
index 8fe5d18..6d05be6 100644
--- a/dripzone-app/src/app/api/orders/route.ts
+++ b/dripzone-app/src/app/api/orders/route.ts
@@ -12,12 +12,38 @@ import {
} from '@/lib/evolution-api';
import { dataService } from '@/lib/data-service';
-export async function GET() {
+export async function GET(request: Request) {
try {
- const orders = await getOrdersFromAppwrite();
+ const { searchParams } = new URL(request.url);
+ const phone = searchParams.get('phone');
+
+ let orders = await getOrdersFromAppwrite();
+ if (!orders || orders.length === 0) {
+ orders = dataService.getOrders();
+ }
+
+ if (phone) {
+ const cleanPhone = phone.replace(/\D/g, '');
+ orders = orders.filter((o: any) => {
+ const oPhone = (o.customerPhone || '').replace(/\D/g, '');
+ return oPhone.endsWith(cleanPhone) || cleanPhone.endsWith(oPhone);
+ });
+ }
+
return NextResponse.json({ success: true, orders });
} catch (error: any) {
- const fallback = dataService.getOrders();
+ let fallback = dataService.getOrders();
+ try {
+ const { searchParams } = new URL(request.url);
+ const phone = searchParams.get('phone');
+ if (phone) {
+ const cleanPhone = phone.replace(/\D/g, '');
+ fallback = fallback.filter((o: any) => {
+ const oPhone = (o.customerPhone || '').replace(/\D/g, '');
+ return oPhone.endsWith(cleanPhone) || cleanPhone.endsWith(oPhone);
+ });
+ }
+ } catch (_) {}
return NextResponse.json({ success: true, orders: fallback });
}
}
diff --git a/dripzone-app/src/app/checkout/page.tsx b/dripzone-app/src/app/checkout/page.tsx
index 39f9a6f..b386fbe 100644
--- a/dripzone-app/src/app/checkout/page.tsx
+++ b/dripzone-app/src/app/checkout/page.tsx
@@ -123,8 +123,8 @@ export default function CheckoutPage() {
const depositAmount = isMandatoryDeposit ? 10000 : 0;
const balanceDue = Math.max(0, totalAmount - depositAmount);
- const officialWhatsapp = storeSettings?.whatsappNumber || '573167455877';
- const boldLink = storeSettings?.boldPaymentLink || 'https://bold.co/p/carlos-lasso';
+ const officialWhatsapp = storeSettings?.whatsappNumber || '573106390296';
+ const boldLink = storeSettings?.boldPaymentLink || 'https://drip-zone.online';
const processOrderSubmission = async () => {
setIsSubmitting(true);
@@ -407,7 +407,7 @@ export default function CheckoutPage() {
📱 Transferencia Directa:
- Nequi: {storeSettings?.nequiAccount || '316 745 5877'}
+ Nequi: {storeSettings?.nequiAccount || '310 639 0296'}
diff --git a/dripzone-app/src/lib/data-service.ts b/dripzone-app/src/lib/data-service.ts
index 611f16b..de0e1d0 100644
--- a/dripzone-app/src/lib/data-service.ts
+++ b/dripzone-app/src/lib/data-service.ts
@@ -128,8 +128,8 @@ export const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
description: 'Paga de forma rápida y 100% segura con tu cuenta Bold. Acepta PSE, Tarjetas Visa/Mastercard y QR de cualquier banco.',
requiresDeposit: false,
depositAmount: 0,
- boldPaymentLink: 'https://bold.co/p/carlos-lasso',
- accountInfo: 'Pasarela Oficial Bold.co • PSE / Tarjetas / QR',
+ boldPaymentLink: 'https://drip-zone.online',
+ accountInfo: 'Pasarela Oficial Bold.co • PSE / Tarjetas / QR Bre-B',
isActive: true,
},
{
@@ -139,8 +139,8 @@ export const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
description: 'Asegura tu talla con solo $10.000 COP vía Bold/Nequi/Daviplata y cancelas el saldo restante en efectivo o transferencia cuando el mensajero llegue a tu casa.',
requiresDeposit: true,
depositAmount: 10000,
- accountInfo: 'Nequi / Daviplata Oficial: 316 745 5877 (Carlos Andrés Lasso)',
- boldPaymentLink: 'https://bold.co/p/carlos-lasso',
+ accountInfo: 'Nequi / Daviplata Oficial DripZone: 310 639 0296',
+ boldPaymentLink: 'https://drip-zone.online',
isActive: true,
},
{
@@ -159,7 +159,7 @@ export const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
description: 'Paga el 100% anticipado y recibe prioridad de despacho express en 24 horas.',
requiresDeposit: false,
depositAmount: 0,
- accountInfo: 'Nequi / Daviplata: 316 745 5877 (Carlos Andrés Lasso)',
+ accountInfo: 'Nequi / Daviplata Oficial DripZone: 310 639 0296',
isActive: true,
},
];
@@ -172,10 +172,10 @@ export const INITIAL_ORDERS: Order[] = [];
export const INITIAL_USERS: User[] = [
{
- id: 'usr-admin-carlos',
- name: 'Carlos Lasso',
- email: 'carlos.andres.lasso@gmail.com',
- phone: '573167455877',
+ id: 'usr-admin-dripzone',
+ name: 'Administrador DripZone',
+ email: 'info@drip-zone.online',
+ phone: '573106390296',
city: 'Cali',
address: 'Sede Principal / Cali',
role: 'admin',
@@ -210,53 +210,84 @@ function getPathModule() {
return null;
}
-function getPersistedFilePath(): string | null {
+function getPersistedFilePath(fileName: string = 'persisted_products.json'): string | null {
const pathMod = getPathModule();
if (!pathMod) return null;
- return pathMod.join(process.cwd(), 'data', 'persisted_products.json');
+ return pathMod.join(process.cwd(), 'data', fileName);
}
-function loadPersistedProducts(): Product[] {
+function loadPersistedData(fileName: string, defaultValue: T): T {
const fsMod = getFsModule();
- const filePath = getPersistedFilePath();
+ const filePath = getPersistedFilePath(fileName);
if (fsMod && filePath && fsMod.existsSync(filePath)) {
try {
const content = fsMod.readFileSync(filePath, 'utf-8');
const parsed = JSON.parse(content);
- if (Array.isArray(parsed) && parsed.length > 0) {
+ if (parsed !== undefined && parsed !== null) {
return parsed;
}
} catch (e) {
- console.warn('[DataService] Error leyendo archivo de productos persistidos:', e);
+ console.warn(`[DataService] Error leyendo ${fileName}:`, e);
}
}
- return [...INITIAL_PRODUCTS];
+ return defaultValue;
}
-function savePersistedProducts(products: Product[]): void {
+function savePersistedData(fileName: string, data: T): void {
const fsMod = getFsModule();
const pathMod = getPathModule();
- const filePath = getPersistedFilePath();
+ const filePath = getPersistedFilePath(fileName);
if (fsMod && pathMod && filePath) {
try {
const dir = pathMod.dirname(filePath);
if (!fsMod.existsSync(dir)) {
fsMod.mkdirSync(dir, { recursive: true });
}
- fsMod.writeFileSync(filePath, JSON.stringify(products, null, 2), 'utf-8');
+ fsMod.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
} catch (e) {
- console.warn('[DataService] Error guardando productos en archivo persistente:', e);
+ console.warn(`[DataService] Error guardando ${fileName}:`, e);
}
}
}
+function loadPersistedProducts(): Product[] {
+ return loadPersistedData('persisted_products.json', [...INITIAL_PRODUCTS]);
+}
+
+function savePersistedProducts(products: Product[]): void {
+ savePersistedData('persisted_products.json', products);
+}
+
+export const DEFAULT_STORE_SETTINGS: StoreSettings = {
+ storeName: 'DripZone',
+ slogan: 'Exclusive Streetwear & Sneakers Oficial',
+ email: 'info@drip-zone.online',
+ phone: '3106390296',
+ whatsappNumber: '573106390296',
+ instagramHandle: '@dripzone.co',
+ address: 'Bodega Central / Cali',
+ city: 'Cali, Colombia',
+ nequiAccount: '310 639 0296',
+ daviplataAccount: '310 639 0296',
+ accountHolder: 'DripZone Oficial',
+ boldPaymentLink: 'https://drip-zone.online',
+ boldApiKey: '',
+ boldMerchantId: '',
+ boldQrUrl: '',
+ boldEnabled: true,
+ defaultProfitMargin: 35,
+ defaultDepositAmount: 10000,
+};
+
class DataService {
private products: Product[] = loadPersistedProducts();
- private cities: City[] = [...INITIAL_CITIES];
- private paymentMethods: PaymentMethod[] = [...INITIAL_PAYMENT_METHODS];
- private suppliers: Supplier[] = [...INITIAL_SUPPLIERS];
- private orders: Order[] = [...INITIAL_ORDERS];
+ private cities: City[] = loadPersistedData('persisted_cities.json', [...INITIAL_CITIES]);
+ private paymentMethods: PaymentMethod[] = loadPersistedData('persisted_payment_methods.json', [...INITIAL_PAYMENT_METHODS]);
+ private suppliers: Supplier[] = loadPersistedData('persisted_suppliers.json', [...INITIAL_SUPPLIERS]);
+ private orders: Order[] = loadPersistedData('persisted_orders.json', [...INITIAL_ORDERS]);
private supplierPosts: SupplierPost[] = [...INITIAL_SUPPLIER_POSTS];
+ private settings: StoreSettings = loadPersistedData('persisted_settings.json', DEFAULT_STORE_SETTINGS);
+ private messageTemplates: MessageTemplate[] = loadPersistedData('persisted_templates.json', [...DEFAULT_MESSAGE_TEMPLATES]);
getProducts(): Product[] {
if (this.products.length === 0) {
@@ -661,6 +692,7 @@ class DataService {
addCity(city: City): City {
this.cities.push(city);
+ savePersistedData('persisted_cities.json', this.cities);
return city;
}
@@ -668,12 +700,14 @@ class DataService {
const idx = this.cities.findIndex((c) => c.id === id);
if (idx === -1) return null;
this.cities[idx] = { ...this.cities[idx], ...updates };
+ savePersistedData('persisted_cities.json', this.cities);
return this.cities[idx];
}
deleteCity(id: string): boolean {
const initialLen = this.cities.length;
this.cities = this.cities.filter((c) => c.id !== id);
+ savePersistedData('persisted_cities.json', this.cities);
return this.cities.length < initialLen;
}
@@ -687,6 +721,7 @@ class DataService {
addPaymentMethod(method: PaymentMethod): PaymentMethod {
this.paymentMethods.push(method);
+ savePersistedData('persisted_payment_methods.json', this.paymentMethods);
return method;
}
@@ -694,12 +729,14 @@ class DataService {
const idx = this.paymentMethods.findIndex((p) => p.id === id);
if (idx === -1) return null;
this.paymentMethods[idx] = { ...this.paymentMethods[idx], ...updates };
+ savePersistedData('persisted_payment_methods.json', this.paymentMethods);
return this.paymentMethods[idx];
}
deletePaymentMethod(id: string): boolean {
const initialLen = this.paymentMethods.length;
this.paymentMethods = this.paymentMethods.filter((p) => p.id !== id);
+ savePersistedData('persisted_payment_methods.json', this.paymentMethods);
return this.paymentMethods.length < initialLen;
}
@@ -713,6 +750,7 @@ class DataService {
addOrder(order: Order): Order {
this.orders.unshift(order);
+ savePersistedData('persisted_orders.json', this.orders);
return order;
}
@@ -737,6 +775,7 @@ class DataService {
createdAt: orderData.createdAt || new Date().toISOString(),
};
this.orders.unshift(newOrder);
+ savePersistedData('persisted_orders.json', this.orders);
return newOrder;
}
@@ -756,12 +795,14 @@ class DataService {
depositAmount: deposit,
balanceDue,
};
+ savePersistedData('persisted_orders.json', this.orders);
return this.orders[idx];
}
deleteOrder(id: string): boolean {
const initialLen = this.orders.length;
this.orders = this.orders.filter((o) => o.id !== id);
+ savePersistedData('persisted_orders.json', this.orders);
return this.orders.length < initialLen;
}
@@ -850,33 +891,13 @@ class DataService {
}
// Configuración de la Tienda
- private settings: StoreSettings = {
- storeName: 'DripZone',
- slogan: 'Luxury Streetwear & Collectibles',
- email: 'info@drip-zone.online',
- phone: '3106390296',
- whatsappNumber: '573106390296',
- instagramHandle: '@dripzone.co',
- address: 'Bodega Central / Cali',
- city: 'Cali, Colombia',
- nequiAccount: '316 745 5877',
- daviplataAccount: '316 745 5877',
- accountHolder: 'Carlos Andrés Lasso',
- boldPaymentLink: 'https://bold.co/p/carlos-lasso',
- boldApiKey: '',
- boldMerchantId: '',
- boldQrUrl: '',
- boldEnabled: true,
- defaultProfitMargin: 35,
- defaultDepositAmount: 10000,
- };
-
getSettings(): StoreSettings {
return this.settings;
}
updateSettings(updates: Partial): StoreSettings {
this.settings = { ...this.settings, ...updates };
+ savePersistedData('persisted_settings.json', this.settings);
return this.settings;
}
@@ -912,9 +933,15 @@ class DataService {
authenticate(emailOrPhone: string): User | null {
const q = emailOrPhone.trim().toLowerCase();
- const user = this.users.find(
- (u) => u.email.toLowerCase() === q || u.phone === q || u.phone.includes(q)
- );
+ const cleanDigits = q.replace(/\D/g, '');
+ const user = this.users.find((u) => {
+ if (u.email.toLowerCase() === q) return true;
+ const uDigits = u.phone.replace(/\D/g, '');
+ if (cleanDigits.length >= 7 && uDigits.length >= 7) {
+ return uDigits === cleanDigits || uDigits.endsWith(cleanDigits) || cleanDigits.endsWith(uDigits);
+ }
+ return false;
+ });
return user || null;
}
@@ -941,8 +968,6 @@ class DataService {
// ==========================================
// SISTEMA DE PLANTILLAS CATEGORIZADAS
// ==========================================
- private messageTemplates: MessageTemplate[] = [...DEFAULT_MESSAGE_TEMPLATES];
-
getMessageTemplates(category?: string): MessageTemplate[] {
if (!category || category === 'all') {
return this.messageTemplates;
@@ -962,6 +987,7 @@ class DataService {
...updates,
updatedAt: new Date().toISOString(),
};
+ savePersistedData('persisted_templates.json', this.messageTemplates);
return this.messageTemplates[idx];
}
@@ -971,6 +997,7 @@ class DataService {
const idx = this.messageTemplates.findIndex((t) => t.id === id);
if (idx === -1) {
this.messageTemplates.push({ ...defaultTpl, updatedAt: new Date().toISOString() });
+ savePersistedData('persisted_templates.json', this.messageTemplates);
return defaultTpl;
}
this.messageTemplates[idx] = {
@@ -979,6 +1006,7 @@ class DataService {
isActive: true,
updatedAt: new Date().toISOString(),
};
+ savePersistedData('persisted_templates.json', this.messageTemplates);
return this.messageTemplates[idx];
}
@@ -988,6 +1016,7 @@ class DataService {
content: t.defaultContent,
updatedAt: new Date().toISOString(),
}));
+ savePersistedData('persisted_templates.json', this.messageTemplates);
return this.messageTemplates;
}
@@ -1007,7 +1036,7 @@ class DataService {
{
id: 'req-1',
customerName: 'Santiago Mejía',
- customerPhone: '573167455877',
+ customerPhone: '573105559876',
customerCity: 'Cali',
brand: 'Jordan',
modelName: 'Air Jordan 4 Retro Travis Scott Cactus Jack',
diff --git a/dripzone-app/src/lib/otp-store.ts b/dripzone-app/src/lib/otp-store.ts
index db688bb..c2e49ec 100644
--- a/dripzone-app/src/lib/otp-store.ts
+++ b/dripzone-app/src/lib/otp-store.ts
@@ -31,7 +31,7 @@ export const otpStore = {
const key = identifier.trim().toLowerCase();
const record = otpCache.get(key);
- // Emergency Master PIN fallback for Carlos Lasso
+ // Emergency Master PIN fallback
if (code === '1978' || code === '197824') {
otpCache.delete(key);
return { valid: true };
diff --git a/dripzone-app/src/lib/server-appwrite.ts b/dripzone-app/src/lib/server-appwrite.ts
index 1a8785b..8f694fa 100644
--- a/dripzone-app/src/lib/server-appwrite.ts
+++ b/dripzone-app/src/lib/server-appwrite.ts
@@ -789,3 +789,83 @@ export async function toggleMonitoredGroupInAppwrite(
}
}
+// =========================================================================
+// 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;
+ }
+}
+
+
diff --git a/dripzone-app/src/lib/templates-service.ts b/dripzone-app/src/lib/templates-service.ts
index 076a98d..af8a5de 100644
--- a/dripzone-app/src/lib/templates-service.ts
+++ b/dripzone-app/src/lib/templates-service.ts
@@ -94,7 +94,7 @@ Estamos alistando tu paquete. Te enviaremos el número de guía o notificación
availableVariables: [
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone Luxury', description: 'Nombre comercial configurado' },
{ key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'Identificador único del pedido' },
- { key: 'cliente_nombre', label: 'Nombre del Cliente', example: 'Carlos Lasso', description: 'Nombre registrado' },
+ { key: 'cliente_nombre', label: 'Nombre del Cliente', example: 'Cliente DripZone', description: 'Nombre registrado' },
{ key: 'cliente_telefono', label: 'Teléfono Cliente', example: '3167455877', description: 'Celular del comprador' },
{ key: 'cliente_direccion', label: 'Dirección de Entrega', example: 'Calle 5 # 24-80, Apto 302', description: 'Dirección física' },
{ key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Municipio de despacho' },
@@ -150,7 +150,7 @@ Estamos alistando tu paquete. Te enviaremos el número de guía o notificación
availableVariables: [
{ key: 'tienda_nombre', label: 'Nombre de Tienda', example: 'DripZone', description: 'Nombre de la marca' },
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
- { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
{ key: 'cliente_telefono', label: 'Teléfono', example: '3167455877', description: 'Teléfono' },
{ key: 'cliente_direccion', label: 'Dirección', example: 'Av 6N # 28N-12', description: 'Dirección' },
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
@@ -270,7 +270,7 @@ Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tie
Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tienda_nombre}! 🔥`,
availableVariables: [
- { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID de la orden' },
{ key: 'cliente_direccion', label: 'Dirección', example: 'Carrera 100 # 16-20', description: 'Dirección' },
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
@@ -301,7 +301,7 @@ Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tie
¡Muchas gracias por elegir {tienda_nombre}! ✨`,
availableVariables: [
- { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Marca' },
],
@@ -324,7 +324,7 @@ Por favor estar atento a la portería o celular. ¡Gracias por tu compra en {tie
🤝 ¿Nos confirmas en qué horario o con qué persona podemos coordinar el reintento de entrega hoy? Quedamos muy atentos.`,
availableVariables: [
- { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Nombre' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
{ key: 'cliente_direccion', label: 'Dirección', example: 'Calle 9 # 44-12', description: 'Dirección' },
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Cali', description: 'Ciudad' },
@@ -465,7 +465,7 @@ Hola {usuario_nombre}, tu código de verificación para iniciar sesión es:
⏱️ _Válido por 5 minutos. No compartas este código con nadie._`,
availableVariables: [
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone Sneakers', description: 'Marca' },
- { key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Carlos Lasso', description: 'Usuario' },
+ { key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Usuario' },
{ key: 'codigo_otp', label: 'Código de 6 Dígitos', example: '849201', description: 'Código OTP' },
],
},
@@ -506,7 +506,7 @@ Hola {usuario_nombre}! Tu código de seguridad para validar tu cuenta es:
content: `Hola {usuario_nombre}! Te escribimos del equipo de {tienda_nombre} 🟣`,
defaultContent: `Hola {usuario_nombre}! Te escribimos del equipo de {tienda_nombre} 🟣`,
availableVariables: [
- { key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Carlos Lasso', description: 'Nombre del usuario' },
+ { key: 'usuario_nombre', label: 'Nombre Usuario', example: 'Cliente DripZone', description: 'Nombre del usuario' },
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre' },
{ key: 'ciudad', label: 'Ciudad Usuario', example: 'Cali', description: 'Ciudad' },
],
@@ -538,7 +538,7 @@ Hola {cliente_nombre}, para procesar tu orden #{orden_id} hacia {cliente_ciudad}
¡Una vez confirmado tu comprobante, tu despacho saldrá de inmediato!`,
availableVariables: [
{ key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Tienda' },
- { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Carlos Lasso', description: 'Cliente' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Cliente' },
{ key: 'orden_id', label: 'No. Orden', example: 'ORD-9842', description: 'ID orden' },
{ key: 'cliente_ciudad', label: 'Ciudad', example: 'Bogotá D.C.', description: 'Ciudad' },
{ key: 'costo_envio', label: 'Costo Flete', example: '$12.000 COP', description: 'Valor del flete' },
@@ -572,6 +572,41 @@ Cualquier duda o asesoría adicional, ¡estamos para servirte con todo el gusto!
{ key: 'ciudad_tienda', label: 'Ciudad Sede', example: 'Cali', description: 'Ciudad' },
],
},
+ {
+ id: 'customer_request_received',
+ title: 'Confirmación de Encargo Especial al Cliente',
+ description: 'Notificación enviada al cliente por WhatsApp cuando registra una solicitud de calzado por encargo.',
+ category: 'customer_sales',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `👋 ¡Hola *{cliente_nombre}*!\n\nRecibimos con éxito tu solicitud por encargo para las *{marca} {modelo}* en talla *{talla}*. 👟🔥\n\nNuestro equipo en {tienda_nombre} ya está verificando disponibilidad y mejor precio con nuestras bodegas y proveedores mayoristas. Te responderemos a este mismo chat con la cotización exacta en el menor tiempo posible.\n\n¡Gracias por confiar en {tienda_nombre}! 🚀`,
+ defaultContent: `👋 ¡Hola *{cliente_nombre}*!\n\nRecibimos con éxito tu solicitud por encargo para las *{marca} {modelo}* en talla *{talla}*. 👟🔥\n\nNuestro equipo en {tienda_nombre} ya está verificando disponibilidad y mejor precio con nuestras bodegas y proveedores mayoristas. Te responderemos a este mismo chat con la cotización exacta en el menor tiempo posible.\n\n¡Gracias por confiar en {tienda_nombre}! 🚀`,
+ availableVariables: [
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre' },
+ { key: 'marca', label: 'Marca', example: 'Nike', description: 'Marca calzado' },
+ { key: 'modelo', label: 'Modelo', example: 'Air Jordan 4', description: 'Modelo' },
+ { key: 'talla', label: 'Talla', example: '41', description: 'Talla' },
+ ],
+ },
+ {
+ id: 'customer_deposit_confirmed',
+ title: 'Confirmación de Anticipo Bold al Cliente',
+ description: 'Notificación enviada al cliente por WhatsApp cuando su pago de anticipo de separación es acreditado por Bold.',
+ category: 'customer_sales',
+ channel: 'whatsapp',
+ isActive: true,
+ content: `🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\nHola *{cliente_nombre}*, recibimos correctamente tu pago de *{valor_anticipo}* a través de Bold.\n\n📦 Tu orden *#{orden_id}* ya se encuentra en alistamiento y empaque para despacho a *{cliente_ciudad}*.\n🤝 *Saldo restante a pagar contraentrega:* *{saldo_contraentrega}* al recibir en tu puerta.\n\nPuedes consultar el estado de tu pedido en cualquier momento desde la app oficial de {tienda_nombre}.\n\n¡Muchas gracias por tu compra en {tienda_nombre}!`,
+ defaultContent: `🎉 *¡Tu anticipo de separación fue confirmado con éxito!*\n\nHola *{cliente_nombre}*, recibimos correctamente tu pago de *{valor_anticipo}* a través de Bold.\n\n📦 Tu orden *#{orden_id}* ya se encuentra en alistamiento y empaque para despacho a *{cliente_ciudad}*.\n🤝 *Saldo restante a pagar contraentrega:* *{saldo_contraentrega}* al recibir en tu puerta.\n\nPuedes consultar el estado de tu pedido en cualquier momento desde la app oficial de {tienda_nombre}.\n\n¡Muchas gracias por tu compra en {tienda_nombre}!`,
+ availableVariables: [
+ { key: 'tienda_nombre', label: 'Nombre Tienda', example: 'DripZone', description: 'Nombre de la tienda' },
+ { key: 'cliente_nombre', label: 'Nombre Cliente', example: 'Cliente DripZone', description: 'Nombre del comprador' },
+ { key: 'orden_id', label: 'No. de Orden', example: 'ORD-9842', description: 'ID de la orden' },
+ { key: 'valor_anticipo', label: 'Valor Anticipo', example: '$10.000 COP', description: 'Monto anticipo pagado' },
+ { key: 'cliente_ciudad', label: 'Ciudad Destino', example: 'Cali', description: 'Municipio de entrega' },
+ { key: 'saldo_contraentrega', label: 'Saldo Contraentrega', example: '$115.000 COP', description: 'Saldo en puerta' },
+ ],
+ },
];
/**
diff --git a/dripzone-app/src/types/index.ts b/dripzone-app/src/types/index.ts
index a027982..8fdfb85 100644
--- a/dripzone-app/src/types/index.ts
+++ b/dripzone-app/src/types/index.ts
@@ -239,7 +239,7 @@ export type MessageTemplateCategory =
export interface MessageTemplateVariable {
key: string; // e.g. 'cliente_nombre'
label: string; // e.g. 'Nombre del Cliente'
- example: string; // e.g. 'Carlos Lasso'
+ example: string; // e.g. 'Cliente DripZone'
description?: string; // e.g. 'Nombre completo registrado por el cliente en el pedido'
}
diff --git a/scripts/check_vps.py b/scripts/check_vps.py
index 9535283..e4ef01a 100644
--- a/scripts/check_vps.py
+++ b/scripts/check_vps.py
@@ -1,6 +1,10 @@
import paramiko
import sys
+if sys.platform == 'win32':
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
+
HOST = "161.97.93.156"
USER = "carlos"
PASSWORD = "Innova1978+*?"
diff --git a/scripts/deploy_vps.py b/scripts/deploy_vps.py
index ef50515..002850c 100644
--- a/scripts/deploy_vps.py
+++ b/scripts/deploy_vps.py
@@ -101,7 +101,9 @@ def main():
# Copiar APK a contenedores y permisos
cmd_copy = f"""
docker cp {REMOTE_BASE}/public/DripZone.apk dripzone-production:/app/public/DripZone.apk || true
- docker cp {REMOTE_BASE}/public/DripZone.apk bfmnoga3lb0ckomhdmxe0rco-021053217798:/app/public/DripZone.apk || true
+ for c in $(docker ps -q --filter "name=bfmnoga3"); do
+ docker cp {REMOTE_BASE}/public/DripZone.apk $c:/app/public/DripZone.apk || true
+ done
docker exec dripzone-production ls -lh /app/public/DripZone.apk || true
"""
stdin, stdout, stderr = ssh.exec_command(cmd_copy)
@@ -121,22 +123,23 @@ def main():
print("[5/5] Enviando notificación detallada y archivo APK al WhatsApp de Carlos (+57 316 745 5877)...")
message_text = (
- "🚀 *DripZone v1.6 - Línea Oficial Exclusiva & Instancia DripZone Conectada*\n\n"
- "Hola Carlos, se ha implementado en producción la división operativa solicitada:\n\n"
- "📱 *1. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
- "• Conectada exitosamente en Evolution API bajo la instancia *DripZone* (Token: `83B5BD230796-4324-AE73-3508F69E279D`).\n"
- "• Todos los envíos de códigos de seguridad OTP, confirmaciones de pedidos, recibos de abono Bold y chats de soporte con clientes ahora se emiten de forma exclusiva desde esta línea oficial.\n\n"
- "📥 *2. Recepción de Productos y Publicaciones (Tu WhatsApp Personal):*\n"
- "• Se mantiene conectada tu instancia personal *AutomataAlmeira* para la lectura de los grupos de proveedores mayoristas donde recibes el calzado.\n"
- "• Ninguna publicación se interrumpe y la catalogación inteligente sigue operando con tus grupos personales.\n\n"
- "🌐 *3. Dominio Oficial drip-zone.online:*\n"
- "• Configurado el servidor de producción (Traefik y Docker) para responder en *drip-zone.online*, *www.drip-zone.online* y el alias `dripzone.almeira.com.co`.\n\n"
- "💳 *4. Pasarela Bold & QR Bre-B en Vivo:*\n"
- "• Generación dinámica de QR Bre-B para el cobro de la separación de $10.000 COP y acreditación automática en tiempo real.\n\n"
+ "🚀 *DripZone v1.6 - Dominio drip-zone.online & Correo Oficial info@drip-zone.online Configurados*\n\n"
+ "Hola Carlos, se ha completado el despliegue de las nuevas configuraciones en producción:\n\n"
+ "🌐 *1. Dominio Oficial & Correo Electrónico:*\n"
+ "• URL Principal: *https://drip-zone.online*\n"
+ "• Correo Oficial: *info@drip-zone.online*\n"
+ "• Certificados SSL y Traefik operativos y validados.\n\n"
+ "📱 *2. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
+ "• Instancia Evolution API *DripZone* activa (`83B5BD230796-4324-AE73-3508F69E279D`).\n"
+ "• Los códigos OTP, confirmaciones de compras, soporte y enlaces Bold se emiten desde esta línea oficial.\n\n"
+ "📥 *3. Publicaciones y Catálogo (Tu WhatsApp Personal):*\n"
+ "• La instancia *AutomataAlmeira* sigue activa recibiendo los mensajes de los grupos de proveedores mayoristas para sincronización automática de calzado.\n\n"
+ "💳 *4. Pasarela Bold & QR Bre-B en Producción:*\n"
+ "• Cobro de anticipo de $10.000 COP con generación automática de QR Bre-B y acreditación en vivo.\n\n"
"📲 *5. App Nativa Android v1.6 Sincronizada:*\n"
- "• Compilada con la nueva línea de atención oficial (+57 310 639 0296) y soporte dinámico para `drip-zone.online`.\n\n"
- f"🔗 *Descarga Directa:* {APK_URL}\n\n"
- "Te adjunto el APK compilado para que puedas instalarlo y probar la experiencia de usuario."
+ "• Compilada con el nuevo dominio y la línea oficial de atención.\n\n"
+ f"🔗 *Descarga Directa del APK:* {APK_URL}\n\n"
+ "Te adjunto el APK compilado para que puedas actualizar la aplicación en tu celular."
)
req_text = urllib.request.Request(
@@ -156,8 +159,8 @@ def main():
"number": TARGET_PHONE,
"mediatype": "document",
"mimetype": "application/vnd.android.package-archive",
- "fileName": "DripZone-Native-v1.5.apk",
- "caption": "📲 *DripZone Native Kotlin v1.5 - Pasarela Bold & QR Bre-B*",
+ "fileName": "DripZone-v1.6-Official.apk",
+ "caption": "📲 *DripZone Native v1.6 - Dominio drip-zone.online & Correo info@drip-zone.online*",
"media": APK_URL
}).encode("utf-8"),
headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
diff --git a/scripts/send_official_whatsapp.py b/scripts/send_official_whatsapp.py
new file mode 100644
index 0000000..62c04cb
--- /dev/null
+++ b/scripts/send_official_whatsapp.py
@@ -0,0 +1,69 @@
+import urllib.request
+import json
+import ssl
+import sys
+
+if sys.platform == 'win32':
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
+
+EVOLUTION_API_URL = "https://evolution.almeira.com.co"
+EVOLUTION_API_KEY = "83B5BD230796-4324-AE73-3508F69E279D"
+INSTANCE_NAME = "DripZone"
+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
+
+message_text = (
+ "🚀 *DripZone v1.6 - Dominio drip-zone.online & Correo Oficial info@drip-zone.online Configurados*\n\n"
+ "Hola Carlos, se ha completado el despliegue de las nuevas configuraciones en producción:\n\n"
+ "🌐 *1. Dominio Oficial & Correo Electrónico:*\n"
+ "• URL Principal: *https://drip-zone.online*\n"
+ "• Correo Oficial: *info@drip-zone.online*\n"
+ "• Servidor Traefik y Coolify enlazados con SSL Let's Encrypt activo.\n\n"
+ "📱 *2. Línea Celular Exclusiva DripZone (+57 310 639 0296):*\n"
+ "• Instancia Evolution API *DripZone* activa (`83B5BD230796-4324-AE73-3508F69E279D`).\n"
+ "• Códigos OTP, confirmaciones de compras, soporte y pasarela Bold se emiten desde esta línea oficial.\n\n"
+ "📥 *3. Publicaciones y Catálogo (Tu WhatsApp Personal):*\n"
+ "• La instancia *AutomataAlmeira* continúa recibiendo los mensajes de los grupos de proveedores mayoristas para sincronización inteligente de calzado.\n\n"
+ "💳 *4. Pasarela Bold & QR Bre-B en Producción:*\n"
+ "• Cobro de anticipo de $10.000 COP con generación automática de QR Bre-B y acreditación en vivo.\n\n"
+ "📲 *5. App Nativa Android v1.6 Sincronizada:*\n"
+ "• Compilada con el nuevo dominio y la línea oficial de atención.\n\n"
+ f"🔗 *Descarga Directa del APK:* {APK_URL}\n\n"
+ "Te adjunto el APK compilado para que puedas actualizar la aplicación en tu celular."
+)
+
+print("[1/2] Enviando mensaje de texto oficial a Carlos...")
+req_text = urllib.request.Request(
+ f"{EVOLUTION_API_URL}/message/sendText/{INSTANCE_NAME}",
+ data=json.dumps({"number": TARGET_PHONE, "text": message_text, "delay": 500}).encode("utf-8"),
+ headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
+)
+try:
+ with urllib.request.urlopen(req_text, context=ctx, timeout=30) as resp:
+ print(" [OK] Mensaje de texto enviado a WhatsApp (Status:", resp.status, ")")
+except Exception as e:
+ print(" [Error texto WhatsApp]:", e)
+
+print("[2/2] Enviando archivo APK compilado a WhatsApp...")
+req_media = urllib.request.Request(
+ f"{EVOLUTION_API_URL}/message/sendMedia/{INSTANCE_NAME}",
+ data=json.dumps({
+ "number": TARGET_PHONE,
+ "mediatype": "document",
+ "mimetype": "application/vnd.android.package-archive",
+ "fileName": "DripZone-v1.6-Official.apk",
+ "caption": "📲 *DripZone Native v1.6 - Dominio drip-zone.online & Correo info@drip-zone.online*",
+ "media": APK_URL
+ }).encode("utf-8"),
+ headers={"apikey": EVOLUTION_API_KEY, "Content-Type": "application/json"}
+)
+try:
+ with urllib.request.urlopen(req_media, context=ctx, timeout=180) as resp:
+ print(" [OK] Documento APK enviado a WhatsApp (Status:", resp.status, ")")
+except Exception as e:
+ print(" [Error envío documento APK]:", e)