almeira-web/sneaky-app/src/lib/pricing-engine.ts

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,
};
}