const round4 = (value) => Math.round(Number(value || 0) * 10000) / 10000; const DEPRECIATION_METHOD_OPTIONS = [ { value: 'SLM', label: 'Straight Line Method (SLM)', description: 'Equal depreciation each year on original cost. Rate auto-calculated from useful life when not provided.', }, { value: 'WDV', label: 'Written Down Value (WDV)', description: 'Depreciation on reducing book value. Rate auto-calculated from cost, salvage and useful life when not provided.', }, { value: 'CUSTOM', label: 'Custom', description: 'Custom method. Depreciation rate must be entered manually.', }, ]; /** Map legacy OTHER → CUSTOM for reads/writes. */ const normalizeDepreciationMethod = (method) => { if (method === 'OTHER') return 'CUSTOM'; return method || null; }; /** * Resolve salvage amount and % from either input. * - If only salvage_percentage → derive salvage_value from purchase_cost * - If only salvage_value → derive salvage_percentage * - If both → prefer salvage_percentage and recompute salvage_value * - If neither → both 0 */ const resolveSalvageFields = ({ purchase_cost: cost, salvage_value: salvageAmount, salvage_percentage: salvagePct, } = {}) => { const purchaseCost = Number(cost || 0); const hasPct = salvagePct !== undefined && salvagePct !== null && salvagePct !== ''; const hasAmount = salvageAmount !== undefined && salvageAmount !== null && salvageAmount !== ''; let salvagePercentage = 0; let salvageValue = 0; if (hasPct && !hasAmount) { salvagePercentage = round4(Math.min(Math.max(Number(salvagePct), 0), 100)); salvageValue = purchaseCost > 0 ? round4((purchaseCost * salvagePercentage) / 100) : 0; } else if (hasAmount && !hasPct) { salvageValue = round4(Math.max(Number(salvageAmount), 0)); salvagePercentage = purchaseCost > 0 ? round4(Math.min((salvageValue / purchaseCost) * 100, 100)) : 0; } else if (hasPct && hasAmount) { // Both sent: percentage is source of truth (FE amount/% toggle) salvagePercentage = round4(Math.min(Math.max(Number(salvagePct), 0), 100)); salvageValue = purchaseCost > 0 ? round4((purchaseCost * salvagePercentage) / 100) : 0; } return { salvage_value: salvageValue, salvage_percentage: salvagePercentage, }; }; const yearsElapsed = (purchaseDate, asOfDate = new Date()) => { if (!purchaseDate) return 0; const start = new Date(purchaseDate); const end = new Date(asOfDate); if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end < start) return 0; const ms = end.getTime() - start.getTime(); return round4(ms / (365.25 * 24 * 60 * 60 * 1000)); }; const resolveDepreciationRate = ({ method, depreciation_rate: rate, purchase_cost: cost, salvage_value: salvage, useful_life_years: lifeYears, }) => { if (rate !== undefined && rate !== null && rate !== '') { return round4(rate); } const purchaseCost = Number(cost || 0); const salvageValue = Number(salvage || 0); const years = Number(lifeYears || 0); const normalizedMethod = normalizeDepreciationMethod(method); if (!normalizedMethod || normalizedMethod === 'CUSTOM' || years <= 0) return null; if (normalizedMethod === 'SLM') { if (purchaseCost <= 0) return round4(100 / years); const depreciable = Math.max(purchaseCost - salvageValue, 0); return round4((depreciable / purchaseCost / years) * 100); } if (normalizedMethod === 'WDV') { if (purchaseCost <= 0 || salvageValue <= 0 || salvageValue >= purchaseCost) return null; return round4((1 - (salvageValue / purchaseCost) ** (1 / years)) * 100); } return null; }; const calculateDepreciation = ({ depreciation_method: method, depreciation_rate: rateInput, purchase_cost: cost, salvage_value: salvage, salvage_percentage: salvagePct, useful_life_years: lifeYears, commencement_date: commencementDate, purchase_date: purchaseDate, as_of_date: asOfDate, } = {}) => { const purchaseCost = Number(cost || 0); const { salvage_value: salvageValue, salvage_percentage: salvagePercentage } = resolveSalvageFields({ purchase_cost: purchaseCost, salvage_value: salvage, salvage_percentage: salvagePct, }); const normalizedMethod = normalizeDepreciationMethod(method); const rate = resolveDepreciationRate({ method: normalizedMethod, depreciation_rate: rateInput, purchase_cost: purchaseCost, salvage_value: salvageValue, useful_life_years: lifeYears, }); // Depreciation starts from the asset's usage start date (commencement_date). // Fall back to purchase_date for assets created before this field existed. const depreciationStartDate = commencementDate || purchaseDate; const elapsed = yearsElapsed(depreciationStartDate, asOfDate || new Date()); const cappedYears = lifeYears && Number(lifeYears) > 0 ? Math.min(elapsed, Number(lifeYears)) : elapsed; let annualDepreciation = 0; let accumulatedDepreciation = 0; let bookValue = purchaseCost; if (normalizedMethod === 'SLM' && rate !== null) { annualDepreciation = round4((purchaseCost * rate) / 100); const maxDepreciable = Math.max(purchaseCost - salvageValue, 0); accumulatedDepreciation = round4(Math.min(annualDepreciation * cappedYears, maxDepreciable)); bookValue = round4(Math.max(purchaseCost - accumulatedDepreciation, salvageValue)); } else if (normalizedMethod === 'WDV' && rate !== null) { annualDepreciation = round4((purchaseCost * rate) / 100); bookValue = purchaseCost; const fullYears = Math.floor(cappedYears); const fraction = cappedYears - fullYears; for (let i = 0; i < fullYears; i += 1) { const yearDep = round4((bookValue * rate) / 100); bookValue = round4(Math.max(bookValue - yearDep, salvageValue)); accumulatedDepreciation = round4(purchaseCost - bookValue); if (bookValue <= salvageValue) break; } if (fraction > 0 && bookValue > salvageValue) { const yearDep = round4(((bookValue * rate) / 100) * fraction); bookValue = round4(Math.max(bookValue - yearDep, salvageValue)); accumulatedDepreciation = round4(purchaseCost - bookValue); } annualDepreciation = round4((bookValue * rate) / 100); } else if (normalizedMethod === 'CUSTOM' && rate !== null) { annualDepreciation = round4((purchaseCost * rate) / 100); const maxDepreciable = Math.max(purchaseCost - salvageValue, 0); accumulatedDepreciation = round4(Math.min(annualDepreciation * cappedYears, maxDepreciable)); bookValue = round4(Math.max(purchaseCost - accumulatedDepreciation, salvageValue)); } return { depreciation_method: normalizedMethod, depreciation_rate: rate, annual_depreciation: annualDepreciation, accumulated_depreciation: accumulatedDepreciation, book_value: bookValue, years_elapsed: elapsed, purchase_cost: purchaseCost, salvage_value: salvageValue, salvage_percentage: salvagePercentage, useful_life_years: lifeYears ?? null, depreciation_start_date: depreciationStartDate || null, }; }; module.exports = { DEPRECIATION_METHOD_OPTIONS, resolveDepreciationRate, resolveSalvageFields, normalizeDepreciationMethod, calculateDepreciation, yearsElapsed, round4, };