/** * Calculadora SENAE — Lógica exacta de la documentación §15 * * Categorías SENAE: * REGIMEN_4X4 → 0% Hasta $400, 4 kg, 4 envíos/año — proceso simplificado * CATEGORIA_B → 10% Bienes de consumo general * CATEGORIA_C → 20% Textiles, calzado, artículos del hogar * CATEGORIA_D → 0-15% Electrónicos — se usa 10% como base (puede variar) * * Fórmulas (doc §15): * Peso_volumétrico = (largo_cm × ancho_cm × alto_cm) / 139 * Peso_final = MAX(peso_real_lbs, Peso_volumétrico) * Flete = Peso_final × $3.50 (configurable) * Seguro = valor_declarado × 0.02 * FODINFA = valor_declarado × 0.005 * Arancel = valor_declarado × % categoría * IVA = (valor_declarado + FODINFA + Arancel) × 0.15 * TOTAL = Flete + Seguro + FODINFA + Arancel + IVA */ export enum SenaeCategory { REGIMEN_4X4 = "REGIMEN_4X4", CATEGORIA_B = "CATEGORIA_B", CATEGORIA_C = "CATEGORIA_C", CATEGORIA_D = "CATEGORIA_D", } export const SENAE_TARIFF_RATES: Record = { [SenaeCategory.REGIMEN_4X4]: 0, [SenaeCategory.CATEGORIA_B]: 0.10, [SenaeCategory.CATEGORIA_C]: 0.20, [SenaeCategory.CATEGORIA_D]: 0.10, // base; puede ajustarse por subpartida }; export interface CalculatorInput { /** Valor declarado del producto en USD */ declaredValueUsd: number; /** Peso real en libras */ weightLbs: number; /** Largo en cm (para peso volumétrico, opcional) */ lengthCm?: number; /** Ancho en cm */ widthCm?: number; /** Alto en cm */ heightCm?: number; /** Categoría SENAE del producto */ category: SenaeCategory; /** Precio por libra en USD (default: 3.50) */ pricePerLb?: number; /** Porcentaje de seguro (default: 0.02 = 2%) */ insurancePct?: number; /** FODINFA (default: 0.005 = 0.5%) */ fodinfaPct?: number; /** IVA (default: 0.15 = 15%) */ ivaPct?: number; } export interface CalculatorResult { /** Peso volumétrico calculado en lbs */ volumetricWeightLbs: number | null; /** Peso final usado para el cálculo (MAX entre real y volumétrico) */ finalWeightLbs: number; /** Costo de flete */ flete: number; /** Costo de seguro */ seguro: number; /** FODINFA */ fodinfa: number; /** Arancel */ arancel: number; /** IVA */ iva: number; /** Total a pagar */ total: number; /** Categoría SENAE aplicada */ category: SenaeCategory; /** Tasa de arancel aplicada (ej: 0.10 = 10%) */ arancelRate: number; /** Si aplica el régimen 4×4 (sin arancel) */ is4x4Regime: boolean; /** Desglose completo en formato display */ breakdown: CalculatorBreakdown; } export interface CalculatorBreakdown { items: Array<{ label: string; formula: string; value: number; isHighlighted?: boolean; }>; } /** * Calcula el peso volumétrico en libras. * Fórmula: (largo × ancho × alto) / 139 * Si faltan dimensiones retorna null. */ export function calcVolumetricWeight( lengthCm?: number, widthCm?: number, heightCm?: number, ): number | null { if (!lengthCm || !widthCm || !heightCm) return null; if (lengthCm <= 0 || widthCm <= 0 || heightCm <= 0) return null; return (lengthCm * widthCm * heightCm) / 139; } /** * Redondea a 2 decimales. */ function round2(value: number): number { return Math.round(value * 100) / 100; } /** * Calcula el costo total de envío con desglose SENAE. * Implementa exactamente las fórmulas del documento §15. */ export function calculateShipping(input: CalculatorInput): CalculatorResult { const { declaredValueUsd, weightLbs, lengthCm, widthCm, heightCm, category, pricePerLb = 3.5, insurancePct = 0.02, fodinfaPct = 0.005, ivaPct = 0.15, } = input; if (declaredValueUsd < 0) throw new Error("El valor declarado no puede ser negativo."); if (weightLbs <= 0) throw new Error("El peso debe ser mayor a 0."); const arancelRate = SENAE_TARIFF_RATES[category]; const is4x4Regime = category === SenaeCategory.REGIMEN_4X4; // Peso volumétrico const volumetricWeightLbs = calcVolumetricWeight(lengthCm, widthCm, heightCm); const finalWeightLbs = volumetricWeightLbs !== null ? Math.max(weightLbs, volumetricWeightLbs) : weightLbs; // Cálculos const flete = round2(finalWeightLbs * pricePerLb); const seguro = round2(declaredValueUsd * insurancePct); const fodinfa = round2(declaredValueUsd * fodinfaPct); const arancel = round2(declaredValueUsd * arancelRate); const iva = round2((declaredValueUsd + fodinfa + arancel) * ivaPct); const total = round2(flete + seguro + fodinfa + arancel + iva); const breakdown: CalculatorBreakdown = { items: [ { label: "Flete", formula: `${finalWeightLbs.toFixed(2)} lbs × $${pricePerLb.toFixed(2)}/lb`, value: flete, }, { label: "Seguro (2%)", formula: `$${declaredValueUsd.toFixed(2)} × ${(insurancePct * 100).toFixed(1)}%`, value: seguro, }, { label: "FODINFA (0.5%)", formula: `$${declaredValueUsd.toFixed(2)} × 0.5%`, value: fodinfa, }, { label: `Arancel ${category.replace("_", " ")} (${(arancelRate * 100).toFixed(0)}%)`, formula: `$${declaredValueUsd.toFixed(2)} × ${(arancelRate * 100).toFixed(0)}%`, value: arancel, }, { label: "IVA (15%)", formula: `($${declaredValueUsd.toFixed(2)} + FODINFA + Arancel) × 15%`, value: iva, }, { label: "TOTAL", formula: "Flete + Seguro + FODINFA + Arancel + IVA", value: total, isHighlighted: true, }, ], }; return { volumetricWeightLbs: volumetricWeightLbs !== null ? round2(volumetricWeightLbs) : null, finalWeightLbs: round2(finalWeightLbs), flete, seguro, fodinfa, arancel, iva, total, category, arancelRate, is4x4Regime, breakdown, }; } /** * Determina si un paquete aplica al régimen 4×4. * Condiciones: valor ≤ $400, peso ≤ 4 kg. (doc §08 estado 06 / §15) * * @param valueUsd Valor declarado en USD * @param weightLbs Peso en libras * @param shipmentsThisYear Número de envíos ya realizados en el año calendario (default 0) * @param max4x4Value Límite de valor (default $400) * @param max4x4WeightKg Límite de peso en kg (default 4 kg) * @param max4x4PerYear Máximo envíos/año (default 4) */ export function qualifiesFor4x4( valueUsd: number, weightLbs: number, shipmentsThisYear = 0, max4x4Value = 400, max4x4WeightKg = 4, max4x4PerYear = 4, ): boolean { const weightKg = weightLbs * 0.453592; return ( valueUsd <= max4x4Value && weightKg <= max4x4WeightKg && shipmentsThisYear < max4x4PerYear ); }