270 lines
9.5 KiB
TypeScript
270 lines
9.5 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 interface MarketPricingStrategy {
|
|
id: 'competitive' | 'balanced' | 'premium';
|
|
name: string;
|
|
badge: string;
|
|
description: string;
|
|
price: number;
|
|
profitCOP: number;
|
|
marginPercent: number;
|
|
clientSavingsPercent: number;
|
|
savingsCOP: number;
|
|
}
|
|
|
|
export function calculateMarketPricingStrategies(
|
|
supplierCost: number,
|
|
marketReferencePriceCOP?: number
|
|
): {
|
|
competitive: MarketPricingStrategy;
|
|
balanced: MarketPricingStrategy;
|
|
premium: MarketPricingStrategy;
|
|
marketReferencePrice: number;
|
|
} {
|
|
const cost = supplierCost > 0 ? supplierCost : 85000;
|
|
const marketRef =
|
|
marketReferencePriceCOP && marketReferencePriceCOP > cost * 1.5
|
|
? marketReferencePriceCOP
|
|
: Math.round(cost * 4.2); // Referencia promedio en tiendas retail oficiales de Colombia
|
|
|
|
// Estrategia 1: ⚡ Máxima Competitividad (Alta Rotación)
|
|
// Margen de ganancia de $30.000 a $35.000 COP
|
|
const competitiveTarget = cost + Math.max(30000, Math.round(cost * 0.35));
|
|
const competitivePrice = roundUpToNearest5000(competitiveTarget);
|
|
const competitiveProfit = competitivePrice - cost;
|
|
const competitiveSavingsCOP = Math.max(0, marketRef - competitivePrice);
|
|
const competitiveSavingsPct = Math.round((competitiveSavingsCOP / marketRef) * 100);
|
|
|
|
// Estrategia 2: ⚖️ Precio Recomendado DripZone (Equilibrio Margen / Ahorro Cliente)
|
|
// Margen de ganancia de $45.000 a $55.000 COP
|
|
const balancedTarget = cost + Math.max(45000, Math.round(cost * 0.52));
|
|
const balancedPrice = roundUpToNearest5000(balancedTarget);
|
|
const balancedProfit = balancedPrice - cost;
|
|
const balancedSavingsCOP = Math.max(0, marketRef - balancedPrice);
|
|
const balancedSavingsPct = Math.round((balancedSavingsCOP / marketRef) * 100);
|
|
|
|
// Estrategia 3: 💎 Máxima Ganancia / Exclusividad (Mayor Rentabilidad)
|
|
// Margen de ganancia de $70.000 a $95.000+ COP aprovechando la alta demanda o valor de mercado
|
|
const premiumTarget = Math.min(
|
|
marketRef * 0.55,
|
|
cost + Math.max(70000, Math.round(cost * 0.85))
|
|
);
|
|
const premiumPrice = roundUpToNearest5000(Math.max(premiumTarget, balancedPrice + 25000));
|
|
const premiumProfit = premiumPrice - cost;
|
|
const premiumSavingsCOP = Math.max(0, marketRef - premiumPrice);
|
|
const premiumSavingsPct = Math.round((premiumSavingsCOP / marketRef) * 100);
|
|
|
|
return {
|
|
marketReferencePrice: marketRef,
|
|
competitive: {
|
|
id: 'competitive',
|
|
name: 'Alta Rotación / Gancho',
|
|
badge: '⚡ Mejor Precio',
|
|
description: 'Precio imbatible para arrasar en ventas rápidas y captar nuevos clientes.',
|
|
price: competitivePrice,
|
|
profitCOP: competitiveProfit,
|
|
marginPercent: Math.round((competitiveProfit / cost) * 100),
|
|
clientSavingsPercent: competitiveSavingsPct,
|
|
savingsCOP: competitiveSavingsCOP,
|
|
},
|
|
balanced: {
|
|
id: 'balanced',
|
|
name: 'Recomendado DripZone',
|
|
badge: '⚖️ Equilibrio Perfecto',
|
|
description: 'Excelente ganancia neta manteniendo un descuento masivo frente a tiendas oficiales.',
|
|
price: balancedPrice,
|
|
profitCOP: balancedProfit,
|
|
marginPercent: Math.round((balancedProfit / cost) * 100),
|
|
clientSavingsPercent: balancedSavingsPct,
|
|
savingsCOP: balancedSavingsCOP,
|
|
},
|
|
premium: {
|
|
id: 'premium',
|
|
name: 'Máximo Margen / Hype',
|
|
badge: '💎 Mayor Ganancia',
|
|
description: 'Aprovecha modelos exclusivos o de alta demanda para maximizar tu ganancia neta.',
|
|
price: premiumPrice,
|
|
profitCOP: premiumProfit,
|
|
marginPercent: Math.round((premiumProfit / cost) * 100),
|
|
clientSavingsPercent: premiumSavingsPct,
|
|
savingsCOP: premiumSavingsCOP,
|
|
},
|
|
};
|
|
}
|
|
|
|
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} (EUR)
|
|
💎 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,
|
|
};
|
|
}
|
|
|