178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
/**
|
|
* Motor de Precios, Políticas de Cambio y Redondeo Automático al 5.000 Siguiente
|
|
*/
|
|
|
|
export const DEFAULT_MIN_PROFIT_MARGIN_PERCENT = 35; // % Mínimo de ganancia
|
|
export const DEFAULT_MIN_PROFIT_COP = 30000; // Margen mínimo en pesos
|
|
export const DEFAULT_SEPARATION_DEPOSIT_COP = 10000; // Separación fija
|
|
export const ROUND_STEP_COP = 5000; // Redondeo siempre al múltiplo de $5.000 superior
|
|
|
|
export const EXCHANGE_POLICY = {
|
|
firstExchangeFree: true,
|
|
badgeText: '1er Cambio de Talla GRATIS',
|
|
shortNotice: 'Primer cambio de talla gratis. Cambios posteriores con flete estándar.',
|
|
fullTerms: 'Cuentas con 1er cambio de talla 100% GRATIS dentro de los primeros 5 días. Para cambios adicionales o posteriores, el cliente asume el costo logístico del flete.',
|
|
};
|
|
|
|
export function formatCOP(amount: number): string {
|
|
if (isNaN(amount)) return '$0';
|
|
return '$' + Math.round(amount).toLocaleString('es-CO');
|
|
}
|
|
|
|
export function roundUpToNearest5000(amount: number): number {
|
|
if (!amount || amount <= 0) return 0;
|
|
return Math.ceil(amount / ROUND_STEP_COP) * ROUND_STEP_COP;
|
|
}
|
|
|
|
export function calculateRetailPrice(
|
|
supplierPrice: number,
|
|
minMarginPercent: number = DEFAULT_MIN_PROFIT_MARGIN_PERCENT,
|
|
minMarginCop: number = DEFAULT_MIN_PROFIT_COP
|
|
): number {
|
|
if (!supplierPrice || supplierPrice <= 0) return 0;
|
|
const priceByPercent = supplierPrice * (1 + minMarginPercent / 100);
|
|
const priceByFixed = supplierPrice + minMarginCop;
|
|
const baseTargetPrice = Math.max(priceByPercent, priceByFixed);
|
|
return roundUpToNearest5000(baseTargetPrice);
|
|
}
|
|
|
|
export function getPricingBreakdown(supplierCost: number, minMarginPercent: number = DEFAULT_MIN_PROFIT_MARGIN_PERCENT) {
|
|
const retailPrice = calculateRetailPrice(supplierCost, minMarginPercent);
|
|
const grossProfit = retailPrice - supplierCost;
|
|
const realMarginPercent = Math.round((grossProfit / supplierCost) * 100);
|
|
|
|
return {
|
|
supplierCost,
|
|
retailPrice,
|
|
grossProfit,
|
|
realMarginPercent,
|
|
isCeiledTo5k: true,
|
|
};
|
|
}
|
|
|
|
export function calculateAnchorPrice(retailPrice: number, discountPercentage: number = 30): number {
|
|
if (!retailPrice || retailPrice <= 0) return 0;
|
|
const anchor = retailPrice / (1 - discountPercentage / 100);
|
|
return roundUpToNearest5000(anchor);
|
|
}
|
|
|
|
export function calculateSeparationBreakdown(
|
|
totalAmount: number,
|
|
depositAmount: number = DEFAULT_SEPARATION_DEPOSIT_COP
|
|
) {
|
|
const deposit = Math.min(depositAmount, totalAmount);
|
|
const balanceDue = Math.max(0, totalAmount - deposit);
|
|
|
|
return {
|
|
totalAmount,
|
|
depositAmount: deposit,
|
|
balanceDue,
|
|
summaryText: `Separas hoy con ${formatCOP(deposit)} y cancelas ${formatCOP(balanceDue)} al recibir contraentrega en tu puerta.`,
|
|
exchangeTerms: EXCHANGE_POLICY.shortNotice,
|
|
};
|
|
}
|
|
|
|
export function generateSalesCopy(params: {
|
|
productName: string;
|
|
brand: string;
|
|
retailPrice: number;
|
|
sizes: string[];
|
|
storeName?: string;
|
|
cityName?: string;
|
|
whatsappNumber?: string;
|
|
}): string {
|
|
const {
|
|
productName,
|
|
brand,
|
|
retailPrice,
|
|
sizes,
|
|
storeName = 'TIENDA DE CALZADO',
|
|
cityName = 'Cali',
|
|
whatsappNumber = '573134848203',
|
|
} = params;
|
|
|
|
const breakdown = calculateSeparationBreakdown(retailPrice);
|
|
const sizesFormatted = sizes.join(' - ');
|
|
const waLink = `https://wa.me/${whatsappNumber}?text=${encodeURIComponent(`Hola ${storeName}! Quiero pedir las zapatillas ${productName} (${brand}) en talla ___ para entrega contraentrega en ${cityName}.`)}`;
|
|
|
|
return `✨ ${storeName.toUpperCase()} • LANZAMIENTO EXCLUSIVO ✨
|
|
👟 ${productName.toUpperCase()} (${brand})
|
|
|
|
📏 Tallas Disponibles: ${sizesFormatted} (Horma Nacional)
|
|
💎 PRECIO DE VENTA: ${formatCOP(retailPrice)} COP
|
|
|
|
🚚 ¡DOMICILIO EN ${cityName.toUpperCase()} Y ENVÍOS A TODO EL PAÍS!
|
|
🤝 PAGO CONTRAENTREGA: Cancelas al recibir el producto en tus manos.
|
|
⚡ SEPARA CON SOLO ${formatCOP(breakdown.depositAmount)} COP y cancelas ${formatCOP(breakdown.balanceDue)} al recibir.
|
|
|
|
🛡️ POLÍTICA DE GARANTÍA:
|
|
✅ Calidad Premium 100% garantizada
|
|
✅ ${EXCHANGE_POLICY.badgeText} (posteriores con flete estándar)
|
|
|
|
📲 ¡Pídelas antes de que se agote tu talla!
|
|
👉 Pedir directo por WhatsApp: ${waLink}`;
|
|
}
|
|
|
|
/**
|
|
* Obtiene el precio exacto de venta de un producto según el estilo/color y la talla seleccionada.
|
|
*/
|
|
export function getProductEffectivePrice(
|
|
product: {
|
|
retailPrice: number;
|
|
sizePrices?: Record<string, number>;
|
|
},
|
|
selectedColor?: {
|
|
price?: number;
|
|
sizePrices?: Record<string, number>;
|
|
} | null,
|
|
selectedSize?: string | null
|
|
): number {
|
|
if (selectedColor && selectedSize && selectedColor.sizePrices && selectedColor.sizePrices[selectedSize]) {
|
|
return selectedColor.sizePrices[selectedSize];
|
|
}
|
|
if (selectedSize && product.sizePrices && product.sizePrices[selectedSize]) {
|
|
return product.sizePrices[selectedSize];
|
|
}
|
|
if (selectedColor && selectedColor.price && selectedColor.price > 0) {
|
|
return selectedColor.price;
|
|
}
|
|
return product.retailPrice || 120000;
|
|
}
|
|
|
|
/**
|
|
* Obtiene el rango de precios si varían según la talla o colorway
|
|
*/
|
|
export function getProductPriceRange(product: {
|
|
retailPrice: number;
|
|
sizePrices?: Record<string, number>;
|
|
colors?: Array<{ price?: number; sizePrices?: Record<string, number> }>;
|
|
}): { minPrice: number; maxPrice: number; hasVariation: boolean } {
|
|
const prices: number[] = [product.retailPrice || 120000];
|
|
|
|
if (product.sizePrices) {
|
|
Object.values(product.sizePrices).forEach((p) => {
|
|
if (typeof p === 'number' && p > 0) prices.push(p);
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(product.colors)) {
|
|
product.colors.forEach((c) => {
|
|
if (c.price && c.price > 0) prices.push(c.price);
|
|
if (c.sizePrices) {
|
|
Object.values(c.sizePrices).forEach((p) => {
|
|
if (typeof p === 'number' && p > 0) prices.push(p);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const minPrice = Math.min(...prices);
|
|
const maxPrice = Math.max(...prices);
|
|
return {
|
|
minPrice,
|
|
maxPrice,
|
|
hasVariation: minPrice !== maxPrice,
|
|
};
|
|
}
|
|
|