Fix product persistence in Appwrite/disk, remove direct WA button, cart separation breakdown and Bold payment flow, include product image in WhatsApp notifications
This commit is contained in:
parent
b7f4d600fc
commit
3c5c67e4eb
@ -8,7 +8,8 @@ data class UserProfile(
|
||||
val phone: String = "",
|
||||
val city: String = "Cali",
|
||||
val address: String = "",
|
||||
val isPhoneVerified: Boolean = false
|
||||
val isPhoneVerified: Boolean = false,
|
||||
val totalOrders: Int = 0
|
||||
)
|
||||
|
||||
object UserManager {
|
||||
@ -18,6 +19,7 @@ object UserManager {
|
||||
private const val KEY_USER_CITY = "user_profile_city"
|
||||
private const val KEY_USER_ADDRESS = "user_profile_address"
|
||||
private const val KEY_PHONE_VERIFIED = "user_phone_verified"
|
||||
private const val KEY_TOTAL_ORDERS = "user_total_orders"
|
||||
|
||||
fun getUser(context: Context): UserProfile {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
@ -26,7 +28,8 @@ object UserManager {
|
||||
phone = prefs.getString(KEY_USER_PHONE, "") ?: "",
|
||||
city = prefs.getString(KEY_USER_CITY, "Cali") ?: "Cali",
|
||||
address = prefs.getString(KEY_USER_ADDRESS, "") ?: "",
|
||||
isPhoneVerified = prefs.getBoolean(KEY_PHONE_VERIFIED, false)
|
||||
isPhoneVerified = prefs.getBoolean(KEY_PHONE_VERIFIED, false),
|
||||
totalOrders = prefs.getInt(KEY_TOTAL_ORDERS, 0)
|
||||
)
|
||||
}
|
||||
|
||||
@ -36,16 +39,26 @@ object UserManager {
|
||||
phone: String,
|
||||
city: String,
|
||||
address: String,
|
||||
isVerified: Boolean
|
||||
isVerified: Boolean,
|
||||
totalOrders: Int? = null
|
||||
) {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
prefs.edit()
|
||||
val editor = prefs.edit()
|
||||
.putString(KEY_USER_NAME, name.trim())
|
||||
.putString(KEY_USER_PHONE, phone.trim())
|
||||
.putString(KEY_USER_CITY, city.trim())
|
||||
.putString(KEY_USER_ADDRESS, address.trim())
|
||||
.putBoolean(KEY_PHONE_VERIFIED, isVerified)
|
||||
.apply()
|
||||
if (totalOrders != null) {
|
||||
editor.putInt(KEY_TOTAL_ORDERS, totalOrders)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun incrementCompletedOrders(context: Context) {
|
||||
val prefs = SecurityManager.getSecurePreferences(context)
|
||||
val current = prefs.getInt(KEY_TOTAL_ORDERS, 0)
|
||||
prefs.edit().putInt(KEY_TOTAL_ORDERS, current + 1).apply()
|
||||
}
|
||||
|
||||
fun setPhoneVerified(context: Context, isVerified: Boolean) {
|
||||
|
||||
@ -56,7 +56,7 @@ class CartBottomSheet : BottomSheetDialogFragment() {
|
||||
)
|
||||
binding.rvCartItems.adapter = cartAdapter
|
||||
|
||||
binding.btnCheckoutWhatsApp.setOnClickListener {
|
||||
binding.btnCartAction.setOnClickListener {
|
||||
val cartList = CartManager.getItems()
|
||||
if (cartList.isEmpty()) return@setOnClickListener
|
||||
|
||||
@ -101,7 +101,13 @@ class CartBottomSheet : BottomSheetDialogFragment() {
|
||||
val subtotal = CartManager.getSubtotal()
|
||||
val shipping = selectedCity.shippingCost
|
||||
val total = subtotal + shipping
|
||||
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
|
||||
|
||||
val user = co.dripzone.app.model.UserManager.getUser(requireContext())
|
||||
// Regla de Negocio: Primer pedido requiere separación de $10.000 COP.
|
||||
// Si el cliente ya completó su primera compra (totalOrders > 0), paga 100% contraentrega sin separación.
|
||||
val requiresSeparation = user.totalOrders == 0
|
||||
val separationAmount = if (requiresSeparation) 10000.0 else 0.0
|
||||
val balanceDue = if (total > separationAmount) total - separationAmount else 0.0
|
||||
|
||||
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
|
||||
format.maximumFractionDigits = 0
|
||||
@ -109,7 +115,28 @@ class CartBottomSheet : BottomSheetDialogFragment() {
|
||||
binding.tvSubtotal.text = format.format(subtotal) + " COP"
|
||||
binding.tvShippingTotal.text = selectedCity.formattedShipping()
|
||||
binding.tvCartTotalAmount.text = format.format(total) + " COP"
|
||||
binding.tvSeparationBreakdown.text = "Separas tu pedido con $10.000 COP y cancelas el saldo restante (${format.format(balanceDue)} COP) en efectivo al recibir."
|
||||
|
||||
if (requiresSeparation) {
|
||||
binding.tvLabelSeparation.text = "⚡ Separación (1er Pedido):"
|
||||
binding.tvSeparationAmount.text = "$ 10.000 COP"
|
||||
binding.tvBalanceDueAmount.text = format.format(balanceDue) + " COP"
|
||||
binding.tvSeparationBreakdown.text = "Separas tu pedido con $10.000 COP y cancelas el saldo restante (${format.format(balanceDue)} COP) en efectivo al recibir."
|
||||
binding.btnCartAction.text = "Pagar Separación ($10.000 COP)"
|
||||
binding.btnCartAction.backgroundTintList = android.content.res.ColorStateList.valueOf(
|
||||
androidx.core.content.ContextCompat.getColor(requireContext(), co.dripzone.app.R.color.drip_purple)
|
||||
)
|
||||
binding.btnCartAction.setIconResource(co.dripzone.app.R.drawable.ic_sparkle)
|
||||
} else {
|
||||
binding.tvLabelSeparation.text = "⭐ Separación (Cliente Frecuente):"
|
||||
binding.tvSeparationAmount.text = "$ 0 COP (Exento)"
|
||||
binding.tvBalanceDueAmount.text = format.format(total) + " COP"
|
||||
binding.tvSeparationBreakdown.text = "¡Cliente Frecuente! Pagas el 100% del pedido (${format.format(total)} COP) contraentrega al recibir en tu puerta."
|
||||
binding.btnCartAction.text = "Enviar Pedido (100% Contraentrega)"
|
||||
binding.btnCartAction.backgroundTintList = android.content.res.ColorStateList.valueOf(
|
||||
androidx.core.content.ContextCompat.getColor(requireContext(), co.dripzone.app.R.color.accent_emerald)
|
||||
)
|
||||
binding.btnCartAction.setIconResource(co.dripzone.app.R.drawable.ic_truck)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
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
|
||||
@ -215,7 +217,16 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
val subtotal = items.sumOf { it.itemTotal }
|
||||
val total = subtotal + selectedCity.shippingCost
|
||||
val balanceDue = if (total > 10000.0) total - 10000.0 else 0.0
|
||||
|
||||
val user = UserManager.getUser(requireContext())
|
||||
// Regla: Para el primer pedido se cobra la separación ($10.000 COP).
|
||||
// Tras haber realizado la primera compra culminada satisfactoriamente (totalOrders > 0), el cliente paga 100% contraentrega.
|
||||
val requiresDeposit = user.totalOrders == 0
|
||||
val depositAmount = if (requiresDeposit) 10000.0 else 0.0
|
||||
val balanceDue = if (total > depositAmount) total - depositAmount else 0.0
|
||||
|
||||
val format = NumberFormat.getCurrencyInstance(Locale("es", "CO"))
|
||||
format.maximumFractionDigits = 0
|
||||
|
||||
val itemsPayload = items.map {
|
||||
mapOf(
|
||||
@ -239,9 +250,9 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
"subtotal" to subtotal,
|
||||
"shippingCost" to selectedCity.shippingCost,
|
||||
"totalAmount" to total,
|
||||
"depositAmount" to 10000.0,
|
||||
"depositAmount" to depositAmount,
|
||||
"balanceDue" to balanceDue,
|
||||
"paymentMethodName" to "Pago Contraentrega",
|
||||
"paymentMethodName" to if (requiresDeposit) "Separación $10.000 + Contraentrega" else "100% Pago Contraentrega",
|
||||
"notes" to "Pedido registrado desde App Android Nativa (WhatsApp OTP Verificado)"
|
||||
)
|
||||
|
||||
@ -251,6 +262,8 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
|
||||
orderResult.onSuccess { orderId ->
|
||||
createdOrderId = orderId
|
||||
UserManager.incrementCompletedOrders(requireContext())
|
||||
|
||||
// Limpiar carrito
|
||||
if (!isDirectProductOrder) {
|
||||
CartManager.clear(requireContext())
|
||||
@ -261,7 +274,30 @@ class OrderVerificationBottomSheet : BottomSheetDialogFragment() {
|
||||
binding.layoutStepOtp.visibility = View.GONE
|
||||
binding.layoutStepSuccess.visibility = View.VISIBLE
|
||||
binding.tvSuccessOrderId.text = "Orden #$orderId"
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\nLa plataforma notificó automáticamente al administrador Carlos (+57 316 745 5877) para preparar tu paquete."
|
||||
|
||||
if (requiresDeposit) {
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\n⚠️ REQUISITO PARA DESPACHO:\nPara poder programar el despacho de tu paquete, realiza el pago de la separación de $10.000 COP a través de la pasarela oficial Bold. Cancelarás el saldo restante (${format.format(balanceDue)} COP) en efectivo al recibir."
|
||||
binding.btnPaySeparationBold.visibility = View.VISIBLE
|
||||
binding.btnPaySeparationBold.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://bold.co/p/carlos-lasso"))
|
||||
startActivity(intent)
|
||||
}
|
||||
binding.btnOpenWhatsAppChat.text = "Notificar Pago a Carlos en WhatsApp"
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
val message = "¡Hola Carlos! Acabo de registrar mi pedido en DripZone con la orden *$createdOrderId*. Voy a cancelar la separación de $10.000 COP para coordinar el despacho a ${selectedCity.name}. Saldo al recibir: ${format.format(balanceDue)} COP."
|
||||
SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
|
||||
dismiss()
|
||||
}
|
||||
} else {
|
||||
binding.tvSuccessMessage.text = "¡Tu número de WhatsApp (+57 $customerPhone) fue validado y tu pedido fue registrado en la plataforma!\n\n⭐ BENEFICIO CLIENTE FRECUENTE:\nComo ya realizaste compras satisfactoriamente, estás exento de separación y pagarás el 100% (${format.format(total)} COP) contraentrega al recibir tu paquete."
|
||||
binding.btnPaySeparationBold.visibility = View.GONE
|
||||
binding.btnOpenWhatsAppChat.text = "Coordinar Entrega por WhatsApp"
|
||||
binding.btnOpenWhatsAppChat.setOnClickListener {
|
||||
val message = "¡Hola Carlos! Acabo de registrar mi orden *$createdOrderId* en DripZone con 100% Contraentrega a ${selectedCity.name}. ¿Me confirmas la programación del despacho?"
|
||||
SecurityManager.openWhatsApp(requireContext(), SecurityManager.WHATSAPP_STORE_PHONE, message)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}.onFailure { err ->
|
||||
binding.btnVerifyOtpAndSubmit.isEnabled = true
|
||||
Toast.makeText(requireContext(), "Error al registrar orden: ${err.message}", Toast.LENGTH_LONG).show()
|
||||
|
||||
@ -78,26 +78,6 @@ class ProductDetailBottomSheet : BottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Direct Order Checkout Button with Verification
|
||||
binding.btnOrderWhatsApp.setOnClickListener {
|
||||
val sizeToOrder = if (selectedSize.isNotBlank()) selectedSize else sizeAdapter?.getSelectedSize() ?: "39"
|
||||
val directItem = co.dripzone.app.model.CartItem(
|
||||
product = p,
|
||||
selectedSize = sizeToOrder,
|
||||
quantity = 1
|
||||
)
|
||||
val city = (activity as? co.dripzone.app.MainActivity)?.selectedCity ?: co.dripzone.app.model.City.defaultCities().first()
|
||||
val verificationSheet = OrderVerificationBottomSheet.newInstance(
|
||||
city = city,
|
||||
isDirect = true,
|
||||
directItem = directItem
|
||||
)
|
||||
parentFragmentManager.let {
|
||||
verificationSheet.show(it, OrderVerificationBottomSheet.TAG)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Custom order button inside bottom sheet
|
||||
binding.btnCustomOrderSheet.setOnClickListener {
|
||||
dismiss()
|
||||
|
||||
@ -259,41 +259,92 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="TOTAL A PAGAR:"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="Valor Total de la Orden:"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp" />
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvCartTotalAmount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="$ 0 COP"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="15sp" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- Valor de Separación -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLabelSeparation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:text="⚡ Separación (1er Pedido):"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSeparationAmount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:fontFamily="sans-serif-bold"
|
||||
android:text="$ 10.000 COP"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="13sp" />
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- Saldo Restante Contraentrega -->
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="🤝 SALDO CONTRAENTREGA:"
|
||||
android:textColor="@color/text_primary"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvBalanceDueAmount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:fontFamily="sans-serif-black"
|
||||
android:text="$ 0 COP"
|
||||
android:textColor="@color/drip_purple_glow"
|
||||
android:textSize="18sp" />
|
||||
android:textColor="@color/accent_emerald"
|
||||
android:textSize="16sp" />
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- WhatsApp Checkout Button with Tapjacking Protection -->
|
||||
<!-- Dynamic Checkout / Pay Separation Action Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCheckoutWhatsApp"
|
||||
android:id="@+id/btnCartAction"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="54dp"
|
||||
android:layout_marginTop="18dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Pedir Carrito por WhatsApp (Contraentrega)"
|
||||
android:text="Pagar Separación ($10.000 COP)"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="16dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:icon="@drawable/ic_sparkle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="22dp" />
|
||||
app:iconSize="20dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@ -323,13 +323,32 @@
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<!-- Separation Payment Button (Pasarela Bold) -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPaySeparationBold"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Pagar Separación con Bold ($10.000 COP)"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/drip_purple"
|
||||
app:cornerRadius="14dp"
|
||||
app:icon="@drawable/ic_sparkle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="18dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnOpenWhatsAppChat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Abrir Chat con el Asesor en WhatsApp"
|
||||
android:text="Notificar Pago por WhatsApp"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="13sp"
|
||||
|
||||
@ -170,25 +170,6 @@
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="20dp" />
|
||||
|
||||
<!-- WhatsApp Checkout Action Button with Tapjacking Protection -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnOrderWhatsApp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="54dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:text="Pedir Directo por WhatsApp (Contraentrega)"
|
||||
android:textAllCaps="false"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
app:backgroundTint="@color/accent_whatsapp"
|
||||
app:cornerRadius="16dp"
|
||||
app:icon="@drawable/ic_whatsapp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="8dp"
|
||||
app:iconSize="22dp" />
|
||||
|
||||
<!-- Secondary Custom Request Button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCustomOrderSheet"
|
||||
|
||||
@ -10,12 +10,36 @@ export async function GET(request: Request) {
|
||||
if (phone) {
|
||||
const cleanPhone = phone.replace(/\D/g, '');
|
||||
const user = dataService.getUsers().find((u) => u.phone.replace(/\D/g, '') === cleanPhone);
|
||||
const existingOrders = dataService.getOrders().filter(
|
||||
(o) => o.customerPhone.replace(/\D/g, '') === cleanPhone
|
||||
);
|
||||
const hasCompletedPreviousOrder = Boolean(
|
||||
(user && (user.totalOrders || 0) > 0) ||
|
||||
existingOrders.some((o) => o.status === 'dispatched' || o.status === 'deposit_received' || o.status === 'supplier_notified')
|
||||
);
|
||||
const requiresShippingDeposit = !hasCompletedPreviousOrder;
|
||||
const depositAmount = requiresShippingDeposit ? 10000 : 0;
|
||||
const depositReason = requiresShippingDeposit
|
||||
? 'Garantía de separación requerida para primer pedido'
|
||||
: 'Cliente distinguido exento de anticipo (100% Contraentrega)';
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
requiresShippingDeposit: user ? user.requiresShippingDeposit === true : false,
|
||||
depositReason: user?.depositReason,
|
||||
isFirstPurchase: !hasCompletedPreviousOrder,
|
||||
requiresShippingDeposit,
|
||||
depositAmount,
|
||||
depositReason,
|
||||
phoneVerified: user ? user.phoneVerified === true : false,
|
||||
user: user ? { id: user.id, name: user.name, phoneVerified: user.phoneVerified, requiresShippingDeposit: user.requiresShippingDeposit, depositReason: user.depositReason } : null,
|
||||
user: user
|
||||
? {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
phoneVerified: user.phoneVerified,
|
||||
totalOrders: user.totalOrders || existingOrders.length,
|
||||
requiresShippingDeposit,
|
||||
depositReason,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,25 @@ function ensureCacheDir() {
|
||||
// Fallback image buffer (placeholder sneaker)
|
||||
const FALLBACK_IMAGE_URL = 'https://images.unsplash.com/photo-1595950653106-6c9ebd614d3a?auto=format&fit=crop&w=800&q=80';
|
||||
|
||||
async function serveFallbackImage() {
|
||||
try {
|
||||
const res = await fetch(FALLBACK_IMAGE_URL);
|
||||
const arrayBuf = await res.arrayBuffer();
|
||||
return new Response(Buffer.from(arrayBuf), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/jpeg',
|
||||
'Cache-Control': 'public, max-age=86400',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
return new Response(Buffer.alloc(0), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/jpeg' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
@ -26,7 +45,7 @@ export async function GET(request: NextRequest) {
|
||||
const quality = searchParams.get('q') || 'high';
|
||||
|
||||
if (!messageId) {
|
||||
return NextResponse.redirect(FALLBACK_IMAGE_URL);
|
||||
return serveFallbackImage();
|
||||
}
|
||||
|
||||
const cleanId = messageId.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
@ -94,8 +113,8 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
if (!messageRecord || !messageRecord.message?.imageMessage) {
|
||||
// Si el mensaje no existe o no tiene imagen, redirigir a imagen fallback
|
||||
return NextResponse.redirect(FALLBACK_IMAGE_URL);
|
||||
// Si el mensaje no existe o no tiene imagen, servir fallback directo
|
||||
return serveFallbackImage();
|
||||
}
|
||||
|
||||
// 3. Solicitar el medio en Alta Resolución Full HD a Evolution API
|
||||
@ -148,9 +167,9 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.redirect(FALLBACK_IMAGE_URL);
|
||||
return serveFallbackImage();
|
||||
} catch (error: any) {
|
||||
console.error('Error en GET /api/whatsapp-media:', error);
|
||||
return NextResponse.redirect(FALLBACK_IMAGE_URL);
|
||||
return serveFallbackImage();
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,8 +54,8 @@ export default function CheckoutPage() {
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [orderCompleted, setOrderCompleted] = useState<Order | null>(null);
|
||||
const [userRequiresDeposit, setUserRequiresDeposit] = useState<boolean>(false);
|
||||
const [userDepositReason, setUserDepositReason] = useState<string>('');
|
||||
const [userRequiresDeposit, setUserRequiresDeposit] = useState<boolean>(true);
|
||||
const [userDepositReason, setUserDepositReason] = useState<string>('Garantía de separación requerida para el primer pedido');
|
||||
const [showOtpModal, setShowOtpModal] = useState<boolean>(false);
|
||||
const [otpCode, setOtpCode] = useState<string>('');
|
||||
const [otpLoading, setOtpLoading] = useState<boolean>(false);
|
||||
@ -80,7 +80,7 @@ export default function CheckoutPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setUserRequiresDeposit(user.requiresShippingDeposit === true);
|
||||
setUserRequiresDeposit((user.totalOrders || 0) === 0 || user.requiresShippingDeposit === true);
|
||||
setUserDepositReason(user.depositReason || '');
|
||||
}
|
||||
}, [user]);
|
||||
@ -92,7 +92,7 @@ export default function CheckoutPage() {
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
setUserRequiresDeposit(data.requiresShippingDeposit === true);
|
||||
setUserRequiresDeposit(data.requiresShippingDeposit !== false);
|
||||
setUserDepositReason(data.depositReason || '');
|
||||
}
|
||||
})
|
||||
@ -107,15 +107,10 @@ export default function CheckoutPage() {
|
||||
const shippingCost = activeCity.shippingCost;
|
||||
const totalAmount = subtotal + shippingCost;
|
||||
|
||||
// Valor de garantía por flete según la ciudad seleccionada (o $10.000 COP si flete local es 0)
|
||||
const cityShippingFee = activeCity.shippingCost > 0 ? activeCity.shippingCost : 10000;
|
||||
|
||||
// Por defecto NO se cobra anticipo a clientes nuevos
|
||||
// Solo se activa si el cliente tiene un pedido rechazado previo o si un admin lo activó
|
||||
// Regla: Para el primer pedido se cobra separación fija ($10.000 COP) sin importar cuántos productos lleve.
|
||||
// Tras la primera compra culminada satisfactoriamente, el cliente paga 100% contraentrega.
|
||||
const isMandatoryDeposit = userRequiresDeposit;
|
||||
const depositAmount = isMandatoryDeposit
|
||||
? cityShippingFee
|
||||
: (activePayment.requiresDeposit ? activePayment.depositAmount : 0);
|
||||
const depositAmount = isMandatoryDeposit ? 10000 : 0;
|
||||
const balanceDue = Math.max(0, totalAmount - depositAmount);
|
||||
|
||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573167455877';
|
||||
@ -544,40 +539,35 @@ export default function CheckoutPage() {
|
||||
{shippingCost === 0 ? <span className="text-emerald-400 font-bold uppercase">Gratis</span> : formatCOP(shippingCost)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-base font-black text-white pt-2 border-t border-neutral-800">
|
||||
<span>Total a Pagar:</span>
|
||||
<span className={`text-transparent bg-clip-text bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-lg`}>
|
||||
<div className="flex justify-between text-sm font-bold text-white pt-2 border-t border-neutral-800">
|
||||
<span>Total de la Orden:</span>
|
||||
<span className="font-bold text-white">
|
||||
{formatCOP(totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Garantía de Flete / Despacho (Solo visible si está activo para el usuario) */}
|
||||
{isMandatoryDeposit ? (
|
||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-2xl p-3.5 space-y-2 text-xs">
|
||||
<div className="flex items-center space-x-2 text-amber-300 font-bold">
|
||||
<ShieldCheck className="w-4 h-4 text-amber-400 shrink-0" />
|
||||
<span>Garantía de Despacho & Reserva ({activeCity.name})</span>
|
||||
</div>
|
||||
<p className="text-neutral-300 text-[11px] leading-relaxed">
|
||||
Para asegurar tu turno prioritario de entrega con la transportadora en <strong>{activeCity.name}</strong>, se realiza un anticipo de <strong>{formatCOP(depositAmount)}</strong> por concepto de flete, el cual es <strong>100% descontable</strong> del valor total al recibir tu calzado en tu domicilio.
|
||||
</p>
|
||||
<div className="flex justify-between items-center pt-1.5 border-t border-amber-500/20 text-[11px]">
|
||||
<span className="text-amber-200">Saldo a pagar en efectivo al recibir:</span>
|
||||
<span className="font-mono font-black text-white text-xs">{formatCOP(balanceDue)}</span>
|
||||
</div>
|
||||
{/* Discriminación de Separación y Saldo Contraentrega */}
|
||||
<div className="bg-neutral-950 border border-white/10 rounded-2xl p-3.5 space-y-2 text-xs">
|
||||
<div className="flex justify-between items-center text-xs">
|
||||
<span className="text-purple-300 font-bold">
|
||||
{isMandatoryDeposit ? '⚡ Valor Separación (1er Pedido):' : '⭐ Separación (Cliente Frecuente):'}
|
||||
</span>
|
||||
<span className="font-black text-white font-mono">
|
||||
{isMandatoryDeposit ? formatCOP(depositAmount) : '$0 COP (Exento)'}
|
||||
</span>
|
||||
</div>
|
||||
) : depositAmount > 0 ? (
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-2.5 text-xs text-white space-y-1">
|
||||
<div className="flex justify-between font-bold text-white">
|
||||
<span>Anticipo de Separación:</span>
|
||||
<span>{formatCOP(depositAmount)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-black text-white pt-1 border-t border-white/10">
|
||||
<span>Saldo a pagar al recibir:</span>
|
||||
<span>{formatCOP(balanceDue)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2 border-t border-white/10 text-xs">
|
||||
<span className="text-emerald-400 font-black">🤝 Saldo a Pagar Contraentrega:</span>
|
||||
<span className="font-mono font-black text-emerald-400 text-sm">{formatCOP(balanceDue)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="text-[10px] text-neutral-400 pt-1 leading-relaxed">
|
||||
{isMandatoryDeposit
|
||||
? 'Para tu primer pedido aseguras el despacho con solo $10.000 COP vía Bold/Nequi y cancelas el saldo restante en efectivo al recibir tu paquete.'
|
||||
: '¡Gracias por tu confianza! Como cliente frecuente, cancelas el 100% del pedido contraentrega al recibir.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-2.5 text-xs text-neutral-300 space-y-1">
|
||||
<div className="flex items-center space-x-1.5 font-bold text-white">
|
||||
@ -599,9 +589,14 @@ export default function CheckoutPage() {
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span>Procesando Pedido...</span>
|
||||
) : isMandatoryDeposit ? (
|
||||
<>
|
||||
<span>Pagar Separación ($10.000 COP)</span>
|
||||
<CreditCard className="w-4 h-4" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Confirmar Pedido Contraentrega</span>
|
||||
<span>Enviar Pedido (100% Contraentrega)</span>
|
||||
<Truck className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Sparkles, Upload, X, CheckCircle2, MessageCircle, AlertCircle, ArrowRight, Camera } from 'lucide-react';
|
||||
import { Sparkles, Upload, X, CheckCircle2, MessageCircle, AlertCircle, ArrowRight, Camera, ShieldCheck } from 'lucide-react';
|
||||
|
||||
interface CustomRequestModalProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@ -212,22 +212,30 @@ export const CartDrawer: React.FC<CartDrawerProps> = ({
|
||||
{shippingCost === 0 ? <span className="text-emerald-400 uppercase font-black">Gratis</span> : formatCOP(shippingCost)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm font-black text-white pt-1 border-t border-slate-800">
|
||||
<div className="flex justify-between text-sm font-bold text-white pt-1 border-t border-slate-800">
|
||||
<span>Total de la Orden:</span>
|
||||
<span className={`text-transparent bg-clip-text bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-base`}>
|
||||
<span className="font-bold text-white">
|
||||
{formatCOP(totalAmount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-purple-300 font-semibold pt-1">
|
||||
<span>⚡ Separación (1er Pedido):</span>
|
||||
<span className="font-bold font-mono">$10.000 COP</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-emerald-400 font-black pt-1 border-t border-white/5">
|
||||
<span>🤝 Saldo Restante Contraentrega:</span>
|
||||
<span className="font-mono">{formatCOP(Math.max(0, totalAmount - 10000))}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separation Highlight Banner */}
|
||||
<div className="bg-white/5 border border-white/10 rounded-xl p-2.5 text-xs space-y-1">
|
||||
<p className="font-bold text-white flex items-center gap-1">
|
||||
<Zap className="w-3.5 h-3.5 fill-white" />
|
||||
Opción de Separación con $10.000 COP
|
||||
Separación con $10.000 COP (1er Pedido)
|
||||
</p>
|
||||
<p className="text-slate-300 text-[11px]">
|
||||
Separas con <strong>{formatCOP(separation.depositAmount)}</strong> y pagas <strong>{formatCOP(separation.balanceDue)}</strong> al recibir contraentrega en tu casa.
|
||||
Separas con <strong>$10.000 COP</strong> y cancelas el saldo restante (<strong>{formatCOP(Math.max(0, totalAmount - 10000))}</strong>) en efectivo o transferencia al recibir en tu puerta.
|
||||
</p>
|
||||
<p className="text-[10px] text-emerald-400 font-semibold pt-0.5 border-t border-white/5">
|
||||
🛡️ {selectedCity.firstExchangeFree
|
||||
@ -236,25 +244,16 @@ export const CartDrawer: React.FC<CartDrawerProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
{/* Action Button */}
|
||||
<div className="pt-1">
|
||||
<Link
|
||||
href="/checkout"
|
||||
onClick={onClose}
|
||||
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-sm py-3 px-4 rounded-xl flex items-center justify-center space-x-2 shadow-lg active:scale-95 transition-all text-center`}
|
||||
className={`w-full bg-gradient-to-r ${currentBrand.colors.primaryGradient} text-white font-black text-sm py-3.5 px-4 rounded-xl flex items-center justify-center space-x-2 shadow-lg active:scale-95 transition-all text-center`}
|
||||
>
|
||||
<span>Proceder al Pago Contraentrega</span>
|
||||
<span>Continuar al Pago Contraentrega</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/checkout"
|
||||
onClick={onClose}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-xs py-2.5 px-3 rounded-xl flex items-center justify-center space-x-1.5 transition-all cursor-pointer"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 fill-white" />
|
||||
<span>Completar Pedido con Verificación WhatsApp</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -402,24 +402,15 @@ export const ProductModal: React.FC<ProductModalProps> = ({
|
||||
{/* Action Buttons */}
|
||||
<div className="pt-4 border-t border-neutral-800 flex flex-col gap-2.5 mt-4">
|
||||
{isSizeInStock ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleWhatsAppDirect}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-sm py-3.5 px-4 rounded-2xl flex items-center justify-center space-x-2 shadow-lg shadow-emerald-600/20 active:scale-95 transition-all cursor-pointer"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4 fill-white" />
|
||||
<span>Pedir por WhatsApp (Contraentrega Talla {selectedSize} • {selectedColor.colorName})</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={added}
|
||||
className={`w-full font-bold text-sm py-3.5 px-4 rounded-2xl flex items-center justify-center space-x-2 transition-all cursor-pointer ${
|
||||
added
|
||||
? 'bg-neutral-800 text-white border border-white/20'
|
||||
: `bg-gradient-to-r ${currentBrand.colors.primaryGradient} hover:opacity-95 text-white shadow-lg active:scale-95`
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={added}
|
||||
className={`w-full font-bold text-sm py-4 px-4 rounded-2xl flex items-center justify-center space-x-2 transition-all cursor-pointer ${
|
||||
added
|
||||
? 'bg-neutral-800 text-white border border-white/20'
|
||||
: `bg-gradient-to-r ${currentBrand.colors.primaryGradient} hover:opacity-95 text-white shadow-lg active:scale-95`
|
||||
}`}
|
||||
>
|
||||
{added ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 text-white" />
|
||||
@ -432,7 +423,6 @@ export const ProductModal: React.FC<ProductModalProps> = ({
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<div className="bg-amber-500/10 border border-amber-500/30 rounded-2xl p-3 text-xs text-amber-200 flex items-center gap-2">
|
||||
|
||||
@ -188,8 +188,70 @@ export const INITIAL_USERS: User[] = [
|
||||
|
||||
export const INITIAL_MONITORED_GROUPS: MonitoredWhatsAppGroup[] = [];
|
||||
|
||||
function getFsModule() {
|
||||
if (typeof window === 'undefined') {
|
||||
try {
|
||||
return require('fs');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPathModule() {
|
||||
if (typeof window === 'undefined') {
|
||||
try {
|
||||
return require('path');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getPersistedFilePath(): string | null {
|
||||
const pathMod = getPathModule();
|
||||
if (!pathMod) return null;
|
||||
return pathMod.join(process.cwd(), 'data', 'persisted_products.json');
|
||||
}
|
||||
|
||||
function loadPersistedProducts(): Product[] {
|
||||
const fsMod = getFsModule();
|
||||
const filePath = getPersistedFilePath();
|
||||
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) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[DataService] Error leyendo archivo de productos persistidos:', e);
|
||||
}
|
||||
}
|
||||
return [...INITIAL_PRODUCTS];
|
||||
}
|
||||
|
||||
function savePersistedProducts(products: Product[]): void {
|
||||
const fsMod = getFsModule();
|
||||
const pathMod = getPathModule();
|
||||
const filePath = getPersistedFilePath();
|
||||
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');
|
||||
} catch (e) {
|
||||
console.warn('[DataService] Error guardando productos en archivo persistente:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DataService {
|
||||
private products: Product[] = [...INITIAL_PRODUCTS];
|
||||
private products: Product[] = loadPersistedProducts();
|
||||
private cities: City[] = [...INITIAL_CITIES];
|
||||
private paymentMethods: PaymentMethod[] = [...INITIAL_PAYMENT_METHODS];
|
||||
private suppliers: Supplier[] = [...INITIAL_SUPPLIERS];
|
||||
@ -197,9 +259,17 @@ class DataService {
|
||||
private supplierPosts: SupplierPost[] = [...INITIAL_SUPPLIER_POSTS];
|
||||
|
||||
getProducts(): Product[] {
|
||||
if (this.products.length === 0) {
|
||||
this.products = loadPersistedProducts();
|
||||
}
|
||||
return groupProductsByModel(this.products);
|
||||
}
|
||||
|
||||
setProducts(products: Product[]): void {
|
||||
this.products = products;
|
||||
savePersistedProducts(this.products);
|
||||
}
|
||||
|
||||
getProductById(id: string): Product | undefined {
|
||||
const allGrouped = groupProductsByModel(this.products);
|
||||
return allGrouped.find((p) => p.id === id || p.colors?.some((c) => c.id === id));
|
||||
@ -274,6 +344,7 @@ class DataService {
|
||||
if (!existing.description || existing.description.length < 50) {
|
||||
existing.description = cleanDescription;
|
||||
}
|
||||
savePersistedProducts(this.products);
|
||||
return existing;
|
||||
}
|
||||
|
||||
@ -306,6 +377,7 @@ class DataService {
|
||||
};
|
||||
|
||||
this.products.unshift(formattedProduct);
|
||||
savePersistedProducts(this.products);
|
||||
return formattedProduct;
|
||||
}
|
||||
|
||||
@ -313,12 +385,14 @@ class DataService {
|
||||
const idx = this.products.findIndex((p) => p.id === id);
|
||||
if (idx === -1) return null;
|
||||
this.products[idx] = { ...this.products[idx], ...updates };
|
||||
savePersistedProducts(this.products);
|
||||
return this.products[idx];
|
||||
}
|
||||
|
||||
deleteProduct(id: string): boolean {
|
||||
const initialLen = this.products.length;
|
||||
this.products = this.products.filter((p) => p.id !== id);
|
||||
savePersistedProducts(this.products);
|
||||
return this.products.length < initialLen;
|
||||
}
|
||||
|
||||
|
||||
@ -113,18 +113,33 @@ export async function notifyNewProductPublished(
|
||||
`━━━━━━━━━━━━━━━━━━━━━━`
|
||||
);
|
||||
|
||||
// Resolver URL absoluta de la imagen para garantizar que Evolution API la descargue con éxito
|
||||
let resolvedMediaUrl = mediaUrl;
|
||||
if (!resolvedMediaUrl && product?.images && product.images.length > 0) {
|
||||
const rawImg = product.images[0];
|
||||
if (rawImg.startsWith('http://') || rawImg.startsWith('https://')) {
|
||||
resolvedMediaUrl = rawImg;
|
||||
} else if (rawImg.startsWith('/')) {
|
||||
resolvedMediaUrl = `https://dripzone.almeira.com.co${rawImg}`;
|
||||
} else {
|
||||
resolvedMediaUrl = `https://dripzone.almeira.com.co/${rawImg}`;
|
||||
}
|
||||
} else if (resolvedMediaUrl && !resolvedMediaUrl.startsWith('http')) {
|
||||
resolvedMediaUrl = `https://dripzone.almeira.com.co${resolvedMediaUrl.startsWith('/') ? '' : '/'}${resolvedMediaUrl}`;
|
||||
}
|
||||
|
||||
const dispatchedTo: string[] = [];
|
||||
|
||||
for (const target of NOTIFICATION_TARGETS) {
|
||||
try {
|
||||
let sent = false;
|
||||
|
||||
// Si tenemos URL de imagen completa y válida, intentamos enviar con imagen
|
||||
if (mediaUrl && (mediaUrl.startsWith('http://') || mediaUrl.startsWith('https://'))) {
|
||||
sent = await sendWhatsAppMedia(target, mediaUrl, notificationCaption);
|
||||
// 1. Enviar prioritariamente con la foto del calzado en alta definición
|
||||
if (resolvedMediaUrl && (resolvedMediaUrl.startsWith('http://') || resolvedMediaUrl.startsWith('https://'))) {
|
||||
sent = await sendWhatsAppMedia(target, resolvedMediaUrl, notificationCaption);
|
||||
}
|
||||
|
||||
// Si no hay imagen o falló el envío multimedia, enviar como texto formateado de alta visibilidad
|
||||
// 2. Si no hay imagen o falló el envío multimedia, enviar como texto formateado
|
||||
if (!sent) {
|
||||
sent = await sendWhatsAppMessage(target, notificationCaption);
|
||||
}
|
||||
@ -180,7 +195,8 @@ export function buildOrderConfirmationMessage(order: any): string {
|
||||
|
||||
/**
|
||||
* Notifica inmediatamente al administrador Carlos (+57 316 745 5877)
|
||||
* y al canal de notificaciones cuando entra un nuevo pedido en la plataforma (web o app móvil).
|
||||
* y al canal de notificaciones cuando entra un nuevo pedido en la plataforma (web o app móvil),
|
||||
* INCLUYENDO la fotografía del calzado en la notificación oficial.
|
||||
*/
|
||||
export async function notifyAdminNewOrder(order: any): Promise<boolean> {
|
||||
const CARLOS_PHONE = '573167455877';
|
||||
@ -221,24 +237,40 @@ export async function notifyAdminNewOrder(order: any): Promise<boolean> {
|
||||
`👉 https://dripzone.almeira.com.co/admin/pedidos`
|
||||
);
|
||||
|
||||
// Extraer y resolver la URL absoluta de la fotografía del calzado
|
||||
const firstItem = order.items?.[0];
|
||||
const rawImage = firstItem?.image || (Array.isArray(firstItem?.images) ? firstItem.images[0] : null);
|
||||
let resolvedImageUrl: string | null = null;
|
||||
if (rawImage && typeof rawImage === 'string') {
|
||||
if (rawImage.startsWith('http://') || rawImage.startsWith('https://')) {
|
||||
resolvedImageUrl = rawImage;
|
||||
} else if (rawImage.startsWith('/')) {
|
||||
resolvedImageUrl = `https://dripzone.almeira.com.co${rawImage}`;
|
||||
} else {
|
||||
resolvedImageUrl = `https://dripzone.almeira.com.co/${rawImage}`;
|
||||
}
|
||||
}
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
// 1. Notificación principal al WhatsApp personal de Carlos
|
||||
const resCarlos = await sendWhatsAppMessage(CARLOS_PHONE, adminMessage);
|
||||
if (resCarlos) success = true;
|
||||
|
||||
// 2. Si el producto principal tiene imagen web, enviar foto como soporte
|
||||
const firstImage = order.items?.[0]?.image || (Array.isArray(order.items?.[0]?.images) ? order.items[0].images[0] : null);
|
||||
if (firstImage && (firstImage.startsWith('http://') || firstImage.startsWith('https://'))) {
|
||||
await sendWhatsAppMedia(
|
||||
CARLOS_PHONE,
|
||||
firstImage,
|
||||
`👟 Foto de referencia: ${order.items[0]?.productName || order.items[0]?.name || 'Producto DripZone'}`
|
||||
).catch(() => {});
|
||||
// 1. Notificación principal al WhatsApp personal de Carlos CON LA FOTO DEL PRODUCTO
|
||||
let sentCarlos = false;
|
||||
if (resolvedImageUrl) {
|
||||
sentCarlos = await sendWhatsAppMedia(CARLOS_PHONE, resolvedImageUrl, adminMessage);
|
||||
}
|
||||
if (!sentCarlos) {
|
||||
sentCarlos = await sendWhatsAppMessage(CARLOS_PHONE, adminMessage);
|
||||
}
|
||||
if (sentCarlos) success = true;
|
||||
|
||||
// 3. Notificación al canal de alertas
|
||||
await sendWhatsAppMessage(NOTIFICATION_GROUP, adminMessage).catch(() => {});
|
||||
// 2. Notificación al canal de alertas (grupo) CON LA FOTO DEL PRODUCTO
|
||||
let sentGroup = false;
|
||||
if (resolvedImageUrl) {
|
||||
sentGroup = await sendWhatsAppMedia(NOTIFICATION_GROUP, resolvedImageUrl, adminMessage).catch(() => false);
|
||||
}
|
||||
if (!sentGroup) {
|
||||
await sendWhatsAppMessage(NOTIFICATION_GROUP, adminMessage).catch(() => {});
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error enviando notificación de pedido a Carlos:', err.message);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user