feat: sistema de cupones de descuento configurables por caducidad y limite de usos en admin, checkout y app android
This commit is contained in:
parent
c7827cbbf1
commit
7a3aa95126
@ -46,7 +46,7 @@ export default function AdminCuponesPage() {
|
||||
// Form Fields
|
||||
const [code, setCode] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [discountType, setDiscountType] = useState<'percentage' | 'fixed'>('percentage');
|
||||
const [discountType, setDiscountType] = useState<'percentage' | 'fixed' | 'per_item'>('percentage');
|
||||
const [discountValue, setDiscountValue] = useState<number>(10);
|
||||
const [minSpend, setMinSpend] = useState<number>(0);
|
||||
const [maxDiscount, setMaxDiscount] = useState<number>(0);
|
||||
@ -526,20 +526,24 @@ export default function AdminCuponesPage() {
|
||||
{/* Discount Value Badge */}
|
||||
<div className="bg-neutral-950/80 border border-neutral-800/80 rounded-2xl p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center font-black">
|
||||
{coupon.discountType === 'percentage' ? '%' : '$'}
|
||||
<div className="w-8 h-8 rounded-xl bg-purple-500/20 text-purple-400 flex items-center justify-center font-black text-xs">
|
||||
{coupon.discountType === 'percentage' ? '%' : coupon.discountType === 'per_item' ? '📦' : '$'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-black text-white">
|
||||
{coupon.discountType === 'percentage'
|
||||
? `${coupon.discountValue}% de Descuento`
|
||||
: coupon.discountType === 'per_item'
|
||||
? `${formatCOP(coupon.discountValue)} / cada producto`
|
||||
: `${formatCOP(coupon.discountValue)} de Descuento`}
|
||||
</p>
|
||||
<p className="text-[10px] text-neutral-400">
|
||||
{coupon.minSpend && coupon.minSpend > 0
|
||||
{coupon.discountType === 'per_item'
|
||||
? `Descuenta ${formatCOP(coupon.discountValue)} por cada producto al saldo contraentrega`
|
||||
: coupon.minSpend && coupon.minSpend > 0
|
||||
? `Compra mín: ${formatCOP(coupon.minSpend)}`
|
||||
: 'Sin monto mínimo'}
|
||||
{coupon.discountType === 'percentage' && coupon.maxDiscount && coupon.maxDiscount > 0
|
||||
{coupon.maxDiscount && coupon.maxDiscount > 0
|
||||
? ` • Tope: ${formatCOP(coupon.maxDiscount)}`
|
||||
: ''}
|
||||
</p>
|
||||
@ -733,20 +737,20 @@ export default function AdminCuponesPage() {
|
||||
{/* Tipo de Descuento */}
|
||||
<div className="space-y-2">
|
||||
<label className="font-bold text-neutral-300 block">Tipo de Descuento *</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDiscountType('percentage');
|
||||
if (discountValue > 100) setDiscountValue(10);
|
||||
}}
|
||||
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-2 border transition-all cursor-pointer ${
|
||||
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-1.5 border transition-all cursor-pointer text-xs ${
|
||||
discountType === 'percentage'
|
||||
? 'bg-purple-600 text-white border-purple-400 shadow-md'
|
||||
: 'bg-neutral-950 text-neutral-400 border-neutral-800 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Percent className="w-4 h-4" />
|
||||
<Percent className="w-3.5 h-3.5" />
|
||||
<span>Porcentaje (%)</span>
|
||||
</button>
|
||||
|
||||
@ -756,22 +760,57 @@ export default function AdminCuponesPage() {
|
||||
setDiscountType('fixed');
|
||||
if (discountValue <= 100) setDiscountValue(20000);
|
||||
}}
|
||||
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-2 border transition-all cursor-pointer ${
|
||||
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-1.5 border transition-all cursor-pointer text-xs ${
|
||||
discountType === 'fixed'
|
||||
? 'bg-purple-600 text-white border-purple-400 shadow-md'
|
||||
: 'bg-neutral-950 text-neutral-400 border-neutral-800 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<DollarSign className="w-4 h-4" />
|
||||
<span>Monto Fijo (COP $)</span>
|
||||
<DollarSign className="w-3.5 h-3.5" />
|
||||
<span>Monto Fijo Global</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setDiscountType('per_item');
|
||||
if (discountValue <= 100) setDiscountValue(10000);
|
||||
}}
|
||||
className={`py-2.5 px-3 rounded-xl font-bold flex items-center justify-center gap-1.5 border transition-all cursor-pointer text-xs ${
|
||||
discountType === 'per_item'
|
||||
? 'bg-gradient-to-r from-emerald-600 to-teal-500 text-white border-emerald-400 shadow-md'
|
||||
: 'bg-neutral-950 text-neutral-400 border-neutral-800 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm">📦</span>
|
||||
<span>Por Cada Producto</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{discountType === 'per_item' && (
|
||||
<div className="bg-emerald-950/30 border border-emerald-500/20 rounded-xl p-2.5 text-[11px] text-emerald-300 space-y-1">
|
||||
<p className="font-bold flex items-center gap-1 text-emerald-400">
|
||||
<span>⚡ Descuento Progresivo por Unidades:</span>
|
||||
</p>
|
||||
<p className="text-neutral-300 leading-relaxed">
|
||||
Si el cliente compra <strong>1 par</strong> descuenta {formatCOP(discountValue || 10000)}, si compra <strong>2 pares</strong> descuenta {formatCOP((discountValue || 10000) * 2)}, si compra <strong>3 pares</strong> {formatCOP((discountValue || 10000) * 3)} y así sucesivamente.
|
||||
</p>
|
||||
<p className="text-emerald-400 font-semibold text-[10px]">
|
||||
🛡️ El descuento se aplica 100% sobre el saldo pendiente por pagar (contraentrega), protegiendo intacto el anticipo de separación.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Valor del Descuento y Atajos */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="font-bold text-neutral-300 block">
|
||||
Valor a Descontar * {discountType === 'percentage' ? '(1% al 100%)' : '(Pesos Colombianos COP)'}
|
||||
Valor a Descontar *{' '}
|
||||
{discountType === 'percentage'
|
||||
? '(1% al 100%)'
|
||||
: discountType === 'per_item'
|
||||
? '(Pesos COP por cada unidad/par comprado)'
|
||||
: '(Pesos Colombianos COP)'}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
@ -802,6 +841,17 @@ export default function AdminCuponesPage() {
|
||||
{v}%
|
||||
</button>
|
||||
))
|
||||
: discountType === 'per_item'
|
||||
? [5000, 10000, 15000, 20000, 25000, 30000].map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
type="button"
|
||||
onClick={() => setDiscountValue(v)}
|
||||
className="px-2 py-0.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-md text-[10px] font-mono cursor-pointer transition-colors"
|
||||
>
|
||||
{formatCOP(v)}/par
|
||||
</button>
|
||||
))
|
||||
: [10000, 15000, 20000, 30000, 50000].map((v) => (
|
||||
<button
|
||||
key={v}
|
||||
|
||||
@ -99,13 +99,28 @@ export async function POST(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Calcular descuento aplicable
|
||||
// 7. Calcular descuento aplicable (porcentaje, fijo global o por unidad de producto)
|
||||
const itemsCount = Math.max(
|
||||
1,
|
||||
Number(body.itemsCount) ||
|
||||
(Array.isArray(body.items)
|
||||
? body.items.reduce((acc: number, it: any) => acc + (it.quantity || 1), 0)
|
||||
: 1)
|
||||
);
|
||||
|
||||
let discountAmount = 0;
|
||||
if (coupon.discountType === 'percentage') {
|
||||
discountAmount = Math.round(subtotalNum * (coupon.discountValue / 100));
|
||||
if (coupon.maxDiscount && coupon.maxDiscount > 0) {
|
||||
discountAmount = Math.min(discountAmount, coupon.maxDiscount);
|
||||
}
|
||||
} else if (coupon.discountType === 'per_item') {
|
||||
// Descuento progresivo por cantidad de productos comprados (ej. $10.000 por 1, $20.000 por 2, etc.)
|
||||
discountAmount = coupon.discountValue * itemsCount;
|
||||
if (coupon.maxDiscount && coupon.maxDiscount > 0) {
|
||||
discountAmount = Math.min(discountAmount, coupon.maxDiscount);
|
||||
}
|
||||
discountAmount = Math.min(discountAmount, subtotalNum);
|
||||
} else {
|
||||
// Monto fijo en pesos COP
|
||||
discountAmount = Math.min(coupon.discountValue, subtotalNum);
|
||||
@ -114,6 +129,8 @@ export async function POST(request: Request) {
|
||||
const discountSummary =
|
||||
coupon.discountType === 'percentage'
|
||||
? `${coupon.discountValue}% OFF (${formatCOP(discountAmount)})`
|
||||
: coupon.discountType === 'per_item'
|
||||
? `${formatCOP(coupon.discountValue)} x ${itemsCount} ${itemsCount === 1 ? 'producto' : 'productos'} (${formatCOP(discountAmount)} OFF al saldo)`
|
||||
: `${formatCOP(discountAmount)} OFF`;
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@ -226,6 +226,7 @@ export default function CheckoutPage() {
|
||||
const activeCity = cities.find((c) => c.id === selectedCityId) || cities[0];
|
||||
const activePayment = paymentMethods.find((p) => p.id === selectedPaymentId) || paymentMethods[0];
|
||||
|
||||
const totalQuantity = checkoutItems.reduce((acc, it) => acc + it.quantity, 0);
|
||||
const subtotal = checkoutItems.reduce((acc, it) => acc + it.price * it.quantity, 0);
|
||||
const shippingCost = activeCity.shippingCost;
|
||||
const finalDiscount = appliedCoupon ? Math.min(discountAmount, subtotal) : 0;
|
||||
@ -244,7 +245,16 @@ export default function CheckoutPage() {
|
||||
const res = await fetch('/api/coupons/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code, subtotal }),
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
subtotal,
|
||||
itemsCount: totalQuantity,
|
||||
items: checkoutItems.map((it) => ({
|
||||
productId: it.productId,
|
||||
quantity: it.quantity,
|
||||
price: it.price,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.success && data.valid) {
|
||||
@ -278,27 +288,31 @@ export default function CheckoutPage() {
|
||||
setCouponSuccessMessage(null);
|
||||
};
|
||||
|
||||
// Revalidar el cupón si cambia el subtotal de los productos
|
||||
// Revalidar el cupón si cambia el subtotal o la cantidad de productos
|
||||
useEffect(() => {
|
||||
if (appliedCoupon && subtotal > 0) {
|
||||
handleApplyCoupon(appliedCoupon.code);
|
||||
}
|
||||
}, [subtotal]);
|
||||
}, [subtotal, totalQuantity]);
|
||||
|
||||
// Lógica de Cobro:
|
||||
// - Si el método es Bold 100% en línea (PSE, Tarjetas, Nequi, etc. sin separación): cobra el totalAmount.
|
||||
// - Si el método requiere separación (ej. $10.000 COP) o el usuario la tiene obligatoria: cobra el anticipo ($10.000 COP) y el saldo queda contraentrega.
|
||||
// - Si es 100% contraentrega en efectivo (ej. Cali): depósito $0 y saldo total contraentrega.
|
||||
// - REGLA CLAVE: El valor del cupón de descuento se aplica exclusivamente al saldo pendiente por pagar (contraentrega) y NUNCA descuenta el anticipo de separación.
|
||||
const is100PercentOnline = activePayment?.type === 'bold' && !activePayment?.requiresDeposit;
|
||||
const isSeparation = Boolean(activePayment?.requiresDeposit || userRequiresDeposit);
|
||||
const isMandatoryDeposit = isSeparation;
|
||||
|
||||
const depositAmount = is100PercentOnline
|
||||
const rawDepositAmount = is100PercentOnline
|
||||
? totalAmount
|
||||
: isSeparation
|
||||
? (activePayment?.depositAmount || 10000)
|
||||
: 0;
|
||||
|
||||
// El anticipo de separación se mantiene fijo en $10.000 COP (o tope totalAmount si totalAmount < 10000)
|
||||
const depositAmount = Math.min(rawDepositAmount, totalAmount);
|
||||
|
||||
// Todo el descuento del cupón se absorbe directamente en el saldo contraentrega:
|
||||
const balanceDue = Math.max(0, totalAmount - depositAmount);
|
||||
|
||||
const officialWhatsapp = storeSettings?.whatsappNumber || '573106390296';
|
||||
@ -1016,17 +1030,23 @@ export default function CheckoutPage() {
|
||||
<div className="bg-emerald-950/40 border border-emerald-500/30 rounded-xl p-2.5 flex items-center justify-between text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-lg bg-emerald-500/20 text-emerald-400 flex items-center justify-center font-bold text-xs">
|
||||
%
|
||||
{appliedCoupon.discountType === 'per_item' ? '📦' : '%'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-white flex items-center gap-1">
|
||||
<span className="font-mono text-emerald-400">{appliedCoupon.code}</span>
|
||||
<span className="text-[10px] text-neutral-400">
|
||||
({appliedCoupon.discountType === 'percentage' ? `${appliedCoupon.discountValue}% OFF` : `${formatCOP(appliedCoupon.discountValue)} OFF`})
|
||||
({appliedCoupon.discountType === 'percentage'
|
||||
? `${appliedCoupon.discountValue}% OFF`
|
||||
: appliedCoupon.discountType === 'per_item'
|
||||
? `${formatCOP(appliedCoupon.discountValue)} x par`
|
||||
: `${formatCOP(appliedCoupon.discountValue)} OFF`})
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-[10px] text-emerald-300/80">
|
||||
Ahorras {formatCOP(finalDiscount)} en esta compra
|
||||
{appliedCoupon.discountType === 'per_item'
|
||||
? `Ahorras ${formatCOP(finalDiscount)} (${formatCOP(appliedCoupon.discountValue)} x ${totalQuantity} ${totalQuantity === 1 ? 'par' : 'pares'}) aplicados al saldo contraentrega`
|
||||
: `Ahorras ${formatCOP(finalDiscount)} aplicados a tu saldo contraentrega`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -1067,7 +1087,10 @@ export default function CheckoutPage() {
|
||||
<div className="flex justify-between text-emerald-400 font-bold bg-emerald-950/30 border border-emerald-500/20 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Tag className="w-3.5 h-3.5" />
|
||||
<span>Descuento Cupón ({appliedCoupon.code}):</span>
|
||||
<span>
|
||||
Descuento Cupón ({appliedCoupon.code}
|
||||
{appliedCoupon.discountType === 'per_item' ? ` • ${totalQuantity}x` : ''}):
|
||||
</span>
|
||||
</span>
|
||||
<span className="font-mono">-{formatCOP(finalDiscount)}</span>
|
||||
</div>
|
||||
@ -1089,7 +1112,7 @@ export default function CheckoutPage() {
|
||||
<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):'}
|
||||
{isMandatoryDeposit ? '⚡ Valor Separación Fija (Garantía):' : '⭐ Separación (Cliente Frecuente):'}
|
||||
</span>
|
||||
<span className="font-black text-white font-mono">
|
||||
{isMandatoryDeposit ? formatCOP(depositAmount) : '$0 COP (Exento)'}
|
||||
@ -1101,9 +1124,20 @@ export default function CheckoutPage() {
|
||||
<span className="font-mono font-black text-emerald-400 text-sm">{formatCOP(balanceDue)}</span>
|
||||
</div>
|
||||
|
||||
{appliedCoupon && finalDiscount > 0 && (
|
||||
<div className="bg-emerald-950/40 border border-emerald-500/30 rounded-xl p-2.5 text-[11px] text-emerald-300 leading-tight space-y-1">
|
||||
<p className="font-bold text-emerald-200 flex items-center gap-1">
|
||||
<span>🎉 Descuento aplicado al saldo contraentrega:</span>
|
||||
</p>
|
||||
<p className="text-[10px] text-emerald-300/90">
|
||||
El ahorro de <strong className="text-white font-mono">{formatCOP(finalDiscount)}</strong> se descuenta directamente de lo que pagas al recibir tu paquete en casa. El valor de separación permanece fijo en <strong className="text-white font-mono">{formatCOP(depositAmount)}</strong> como flete de garantía.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-[10px] text-neutral-400 pt-1 leading-relaxed">
|
||||
{isMandatoryDeposit
|
||||
? `Para tu primer pedido aseguras el despacho de tu orden (${checkoutItems.reduce((acc, it) => acc + it.quantity, 0)} items) con una separación fija única de $10.000 COP vía Bold y cancelas el saldo restante en efectivo al recibir tu paquete.`
|
||||
? `Para tu pedido aseguras el despacho (${totalQuantity} ${totalQuantity === 1 ? 'par' : 'pares'}) con una separación fija única de $10.000 COP vía Bold y cancelas el saldo restante en efectivo o transferencia al recibir tu paquete.`
|
||||
: '¡Gracias por tu confianza! Como cliente frecuente, cancelas el 100% del pedido contraentrega al recibir.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -300,6 +300,22 @@ export const INITIAL_COUPONS: Coupon[] = [
|
||||
applicableCategories: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
id: 'cup-multidrip',
|
||||
code: 'MULTIDRIP10K',
|
||||
description: '$10.000 COP de descuento por cada producto comprado (1=$10.000, 2=$20.000, etc.)',
|
||||
discountType: 'per_item',
|
||||
discountValue: 10000,
|
||||
minSpend: 0,
|
||||
maxDiscount: 0,
|
||||
usageLimit: 100,
|
||||
usageCount: 0,
|
||||
expiresAt: new Date(Date.now() + 30 * 86400000).toISOString(),
|
||||
startDate: new Date().toISOString(),
|
||||
isActive: true,
|
||||
applicableCategories: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
export function isCarlosAdmin(identifier?: string | null): boolean {
|
||||
|
||||
@ -440,9 +440,13 @@ export function buildOrderConfirmationMessage(order: any): string {
|
||||
.join('\n\n');
|
||||
|
||||
const settings = dataService.getSettings();
|
||||
const couponSummary = (order.couponCode && order.discountAmount > 0)
|
||||
? `\n🎟️ *Cupón Aplicado (${order.couponCode}):* -${formatCOP(order.discountAmount)} (aplicado a tu saldo contraentrega)`
|
||||
: '';
|
||||
|
||||
const paymentCondition = order.depositAmount > 0
|
||||
? `⚡ *Separación / Anticipo:* ${formatCOP(order.depositAmount)}\n🤝 *Saldo a Pagar Contraentrega:* ${formatCOP(order.balanceDue)}`
|
||||
: '🤝 *Pago 100% Contraentrega al recibir en tu casa.*';
|
||||
? `⚡ *Separación / Anticipo:* ${formatCOP(order.depositAmount)} (Fijo)\n🤝 *Saldo a Pagar Contraentrega:* ${formatCOP(order.balanceDue)}${couponSummary}`
|
||||
: `🤝 *Pago 100% Contraentrega al recibir en tu casa:* ${formatCOP(order.totalAmount)}${couponSummary}`;
|
||||
|
||||
return dataService.renderMessageTemplate('order_confirmation_customer', {
|
||||
tienda_nombre: settings?.storeName || 'DripZone Luxury',
|
||||
|
||||
@ -1240,7 +1240,7 @@ export async function getCouponsFromAppwrite(): Promise<Coupon[]> {
|
||||
id: d.$id,
|
||||
code: (d.code || '').toUpperCase().trim(),
|
||||
description: d.description || '',
|
||||
discountType: (d.discountType || 'percentage') as 'percentage' | 'fixed',
|
||||
discountType: (d.discountType || 'percentage') as 'percentage' | 'fixed' | 'per_item',
|
||||
discountValue: Number(d.discountValue) || 0,
|
||||
minSpend: Number(d.minSpend) || 0,
|
||||
maxDiscount: Number(d.maxDiscount) || 0,
|
||||
|
||||
@ -166,7 +166,7 @@ export interface Coupon {
|
||||
id: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
discountType: 'percentage' | 'fixed';
|
||||
discountType: 'percentage' | 'fixed' | 'per_item';
|
||||
discountValue: number;
|
||||
minSpend?: number;
|
||||
maxDiscount?: number;
|
||||
|
||||
@ -140,13 +140,17 @@ def main():
|
||||
"• Barra de progreso visual del cupo consumido (ej. 24 / 50 canjes - 48%).\n"
|
||||
"• Al alcanzar el límite, el cupón pasa automáticamente a estado 'Agotado' y rechaza canjes adicionales.\n\n"
|
||||
"💰 *4. Tipos de Descuento & Reglas de Margen:*\n"
|
||||
"• *📦 Por Cada Producto (Progresivo):* ¡NUEVO! Descuenta $10.000 COP si compra 1 par, $20.000 COP si compra 2 pares, $30.000 COP si compra 3 pares, etc.\n"
|
||||
"• *Porcentaje (%):* Ej. 10%, 15%, 20% con opción de tope máximo en COP para proteger márgenes.\n"
|
||||
"• *Monto Fijo ($ COP):* Ej. $20.000 COP directo en el pedido.\n"
|
||||
"• *Compra Mínima:* Monto mínimo en subtotal para activar el beneficio (ej. compras superiores a $120.000).\n\n"
|
||||
"🛒 *5. Integración en Carrito & Checkout:*\n"
|
||||
"🛡️ *5. Protección Financiera de la Separación:*\n"
|
||||
"• El valor del cupón se descuenta 100% del *saldo pendiente por pagar contraentrega*.\n"
|
||||
"• La *separación fija de $10.000 COP* permanece intacta como garantía de flete de despacho.\n\n"
|
||||
"🛒 *6. Integración en Carrito & Checkout:*\n"
|
||||
"• Caja de cupón interactiva en `/checkout` con validación en tiempo real y animación de confeti.\n"
|
||||
"• Desglose transparente en el resumen de compra: Subtotal, Descuento de Cupón (-$XX.XXX) y Total.\n"
|
||||
"• Registro del cupón en la base de datos Appwrite y aumento automático del contador de usos al crear el pedido.\n"
|
||||
"• Desglose transparente en el resumen de compra: Subtotal, Descuento de Cupón (-$XX.XXX), Separación ($10.000 fija) y Saldo Contraentrega.\n"
|
||||
"• Registro del cupón en Appwrite y aumento automático del contador de usos al crear el pedido.\n"
|
||||
"• Alerta de nuevo pedido a tu WhatsApp ahora destaca el cupón utilizado por el cliente.\n\n"
|
||||
f"🔗 *Descarga Directa del APK:* {APK_URL}\n\n"
|
||||
"Te adjunto a continuación el archivo APK instalador actualizado con soporte completo para la app Android."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user