@archerjessop/utilities 4.6.25 → 4.6.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n const estimatedGrossIncome = askingPrice * strGrossIncomeMultiplier;\r\n return estimatedGrossIncome * strNoiPercentage;\r\n \r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}\r\n\r\n/**\r\n * Calculate monthly cash flow from property financials\r\n * Handles all loan calculations internally\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} annualNOI - Annual Net Operating Income\r\n * @param {number} downPaymentPercent - Down payment as decimal (0.3 = 30%)\r\n * @param {number} sellerFiRate - Seller financing interest rate as decimal (0.065 = 6.5%)\r\n * @param {number} sellerAmortization - Seller financing term in years (default 30)\r\n * @param {number} deferralMonths - Interest-only deferral period in months (default 0)\r\n * @returns {number} Monthly cash flow in dollars\r\n */\r\nexport function calculateMonthlyCashFlow(\r\n askingPrice,\r\n annualNOI,\r\n downPaymentPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT,\r\n sellerFiRate = FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE,\r\n sellerAmortization = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n deferralMonths = 0\r\n) {\r\n try {\r\n if (!askingPrice || !annualNOI || askingPrice <= 0 || annualNOI <= 0) {\r\n return 0;\r\n }\r\n\r\n // Monthly NOI\r\n const monthlyNOI = annualNOI / 12;\r\n\r\n // Calculate financing amounts\r\n // DSCR is always 70% of asking price\r\n const dscrLoanAmount = askingPrice * FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE;\r\n \r\n // Seller financing = remaining after down payment and DSCR\r\n const sellerFiAmount = askingPrice * (1 - downPaymentPercent - FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE);\r\n\r\n // Calculate monthly payments\r\n const dscrPayment = calculatePMT(dscrLoanAmount, FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE, 30);\r\n \r\n // Seller financing payment (may be 0 if rate is 0)\r\n let sellerFiPayment = 0;\r\n if (sellerFiRate > 0) {\r\n sellerFiPayment = calculatePMT(sellerFiAmount, sellerFiRate, sellerAmortization);\r\n } else {\r\n // If 0% interest, just divide principal by term\r\n sellerFiPayment = sellerFiAmount / (sellerAmortization * 12);\r\n }\r\n\r\n // Handle deferral period (interest-only)\r\n let effectiveSellerFiPayment = sellerFiPayment;\r\n if (deferralMonths > 0 && sellerFiRate > 0) {\r\n // During deferral, only pay interest\r\n const monthlyRate = sellerFiRate / 12;\r\n effectiveSellerFiPayment = sellerFiAmount * monthlyRate;\r\n }\r\n\r\n // Monthly cash flow\r\n return monthlyNOI - (dscrPayment + effectiveSellerFiPayment);\r\n } catch (error) {\r\n console.error('Error calculating monthly cash flow:', error);\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate assignment fee from decimal rate\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentRateDecimal - Assignment rate as decimal (0.05 = 5%)\r\n * @returns {number} Assignment fee in dollars\r\n */\r\nexport function calculateAssignmentFeeFromDecimal(askingPrice, assignmentRateDecimal = 0.05) {\r\n try {\r\n if (!askingPrice || askingPrice <= 0 || !assignmentRateDecimal) {\r\n return 0;\r\n }\r\n return askingPrice * assignmentRateDecimal;\r\n } catch (error) {\r\n console.error('Error calculating assignment fee:', error);\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate DSCR monthly payment\r\n * @param {number} askingPrice - Property asking price\r\n * @returns {number} Monthly DSCR payment in dollars\r\n */\r\nexport function calculateDscrMonthlyPayment(askingPrice) {\r\n try {\r\n if (!askingPrice || askingPrice <= 0) {\r\n return 0;\r\n }\r\n const dscrLoanAmount = askingPrice * FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE;\r\n return calculatePMT(dscrLoanAmount, FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE, 30);\r\n } catch (error) {\r\n console.error('Error calculating DSCR payment:', error);\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate seller financing monthly payment\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} downPaymentPercent - Down payment as decimal (0.3 = 30%)\r\n * @param {number} sellerFiRate - Seller financing rate as decimal (0.065 = 6.5%)\r\n * @param {number} sellerAmortization - Seller financing term in years\r\n * @returns {number} Monthly seller financing payment in dollars\r\n */\r\nexport function calculateSellerFiMonthlyPayment(\r\n askingPrice,\r\n downPaymentPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT,\r\n sellerFiRate = FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE,\r\n sellerAmortization = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION\r\n) {\r\n try {\r\n if (!askingPrice || askingPrice <= 0) {\r\n return 0;\r\n }\r\n\r\n const sellerFiAmount = askingPrice * (1 - downPaymentPercent - FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE);\r\n\r\n if (sellerFiRate > 0) {\r\n return calculatePMT(sellerFiAmount, sellerFiRate, sellerAmortization);\r\n } else {\r\n return sellerFiAmount / (sellerAmortization * 12);\r\n }\r\n } catch (error) {\r\n console.error('Error calculating seller fi payment:', error);\r\n return 0;\r\n }\r\n}"],"names":["calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","calculatePriceForCOCR","targetCOCR","options","downPercent","FINANCIAL_CONSTANTS","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","DSCR_INTEREST_RATE","dscrTerm","DSCR_AMORTIZATION","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strGrossIncomeMultiplier","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","strNoiPercentage","NOI_PERCENTAGE","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","value","fallback","isNaN"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n const estimatedGrossIncome = askingPrice * strGrossIncomeMultiplier;\r\n return estimatedGrossIncome * strNoiPercentage;\r\n \r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = FINANCIAL_CONSTANTS.DSCR_INTEREST_RATE,\r\n dscrTerm = FINANCIAL_CONSTANTS.DSCR_AMORTIZATION,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","calculatePriceForCOCR","targetCOCR","options","downPercent","FINANCIAL_CONSTANTS","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","DSCR_INTEREST_RATE","dscrTerm","DSCR_AMORTIZATION","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strGrossIncomeMultiplier","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","strNoiPercentage","NOI_PERCENTAGE","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","value","fallback","isNaN"],"mappings":"kNAcO,SAASA,aAAaC,EAAWC,EAAYC,GAClD,GAAmB,IAAfD,EACF,OAAOD,GAAqB,GAARE,GAGtB,MAAMC,EAAcF,EAAa,GAC3BG,EAAsB,GAARF,EAGpB,OAFYF,GAAaG,EAAcE,KAAKC,IAAI,EAAIH,EAAaC,KACpDC,KAAKC,IAAI,EAAIH,EAAaC,GAAe,EAExD,CAEO,SAASG,gBAAgBC,EAAaC,GAC3C,IACE,MAAMC,EAA6B,GAAdF,EAEfG,EAAwD,GAA1CZ,aADiB,GAAdS,EAC0B,KAAO,IAIxD,OAHuBC,EAAME,GACED,EAAgB,GAGjD,CAAE,MAAOE,GACP,OAAO,CACT,CACF,CAEO,SAASC,uBAAuBC,EAAiBC,GACtD,IAAKA,GAAiBA,GAAiB,EAAG,OAAO,EAEjD,OADyC,GAAlBD,EACEC,EAAiB,GAC5C,CAUO,SAASC,sBAAsBP,EAAKQ,EAAa,IAAMC,EAAU,CAAA,GACtE,MAAMC,YACJA,EAAyD,IAA3CC,EAAoBC,qBAA0BC,eAC5DA,EAA+D,IAA9CF,EAAoBG,wBAA6BC,SAClEA,EAAWJ,EAAoBK,mBAAkBC,SACjDA,EAAWN,EAAoBO,kBAAiBC,cAChDA,EAAgBC,EAAmBC,eAAcC,UACjDA,EAAYF,EAAmBG,uBAC7Bd,EAEJ,IACE,IAAIe,EAAcxB,EAAM,IACpByB,EAAa,EAEjB,KAAOA,EAAaN,GAAe,CACjC,MAAMlB,EAAeuB,GAAed,EAAc,KAE5CR,EAAiE,GAAnDZ,aADGkC,GAAeX,EAAiB,KACNE,EAAUE,GAErDS,GADiB1B,EAAME,GACQD,EAErC,GAAIL,KAAK+B,IAAID,EAAclB,GAAcc,EACvC,MAGF,MAAMnB,EAAQuB,EAAclB,EACtBoB,EAAazB,EAAQiB,EAAmBS,kBAG5CL,GADErB,EAAQ,EACmB,EAAIP,KAAK+B,IAAIC,GAEb,EAAIhC,KAAK+B,IAAIC,GAIxCJ,EAAc,MAAMA,EAAc,KAClCA,EAAcxB,EAAMoB,EAAmBU,8BACzCN,EAAcxB,EAAMoB,EAAmBW,sCAGzCN,GACF,CAOA,OAJID,EAAcJ,EAAmBY,uBACnCR,EAAcJ,EAAmBY,sBAG5BR,CACT,CAAE,MAAOrB,GACP,OAAO,CACT,CACF,CAUO,SAAS8B,uBAAuBlC,EAAaC,EAAKU,EAAaD,EAAU,CAAA,GAC9E,MAAMM,SACJA,EAAWJ,EAAoBK,mBAAkBC,SACjDA,EAAWN,EAAoBO,mBAC7BT,EAEJ,IACE,MACMR,EAAeF,GADDW,EAAc,KAM5BR,EAAiE,GAAnDZ,aAFGS,EAAcE,EAEYc,EAAUE,GAK3D,OAHuBjB,EAAME,GACED,EAAgB,GAGjD,CAAE,MAAOE,GACP,OAAO,CACT,CACF,CAUO,SAAS+B,mBAAmBnC,EAAaoC,EAASC,EAAeC,EAAeC,YAAa7B,EAAU,IAC5G,MAAM8B,yBACJA,EAA2BC,EAAwBC,IAAIC,qBAAoBC,iBAC3EA,EAAmBH,EAAwBC,IAAIG,eAAcC,yBAC7DA,EAA2BL,EAAwBM,gBAAgBC,2BAA0BC,aAC7FA,EAAeR,EAAwBM,gBAAgBG,uBACrDxC,EAEJ,IACE,OAAQ2B,EAAac,eACnB,KAAKb,EAAeI,IAElB,OAD6B1C,EAAcwC,EACbI,EAEhC,KAAKN,EAAeS,gBAClB,OAAOE,EAAeH,EAA2B,GAEnD,KAAKR,EAAeC,YACpB,QACE,OAAOvC,EAAcoC,EAE3B,CAAE,MAAOhC,GACP,OAAO,CACT,CACF,CAQO,SAASgD,uBAAuBpD,EAAaqD,EAAmE,IAA/ChC,EAAmBiC,2BACzF,IACE,OAAOtD,GAAeqD,EAAoB,IAC5C,CAAE,MAAOjD,GACP,OAAO,CACT,CACF,CAQO,SAASmD,oBAAoBvD,EAAaU,EAAU,IACzD,MAAM8C,iBACJA,EAAgE,IAA7CnC,EAAmBoC,wBAA6BC,qBACnEA,EAAsE,IAA/CrC,EAAmBiC,0BAA+BK,kBACzEA,EAAkE,IAA9CtC,EAAmBuC,yBAA8BC,oBACrEA,EAAsD,IAAhCxC,EAAmByC,WAAgBC,wBACzDA,EAA+D,IAArC1C,EAAmB2C,gBAAqBlD,eAClEA,EAA+D,IAA9CF,EAAoBG,yBACnCL,EAEJ,IAGE,OAAOV,GAAewD,EAAmB,KAClCxD,IAAgB0D,EAAuBC,GAAqB,KAC5D3D,GAAe6D,EAAsB,KACpCE,EAA0B,KAAQ/D,EALnBA,GAAec,EAAiB,KAMzD,CAAE,MAAOV,GACP,OAAO,CACT,CACF,CAUO,SAAS6D,wBAAwBC,EAAYC,EAAcC,EAAmBC,EAAezD,EAAoB0D,8BACtH,IACE,GAAIJ,GAAc,GAAKC,EAAe,GAAKC,GAAqB,GAAKC,GAAgB,EACnF,OAAO,EAIT,GAAIA,GAAgBD,EAClB,OAAO,EAIT,GAAqB,IAAjBD,EAAoB,CACtB,MAAMI,EAAoC,GAApBH,EAEtB,OAAOF,GAAcK,EADe,GAAfF,GACgCE,CACvD,CAEA,MAAM5E,EAAcwE,EAAe,GAC7BI,EAAoC,GAApBH,EAChBI,EAAiC,GAAfH,EAMlBI,EAAU5E,KAAKC,IAAI,EAAIH,EAAa4E,GAGpCG,EAAmBR,GAAcO,EAFvB5E,KAAKC,IAAI,EAAIH,EAAa6E,KAEmBC,EAAU,GAEvE,OAAO5E,KAAK8E,IAAI,EAAGD,EACrB,CAAE,MAAOtE,GACP,OAAO,CACT,CACF,CASO,SAASwE,0BAA0BC,EAAcC,EAAmBlE,EAAoBmE,kBAAmBrF,EAAQkB,EAAoB0D,8BAC5I,IACE,OAAIO,GAAgB,GAAKC,EAAmB,GAAKpF,EAAQ,EAChDmF,EAGFA,EAAehF,KAAKC,IAAI,EAAIgF,EAAkBpF,EACvD,CAAE,MAAOU,GACP,OAAOyE,CACT,CACF,CAUO,SAASG,0BAA0BC,EAAeC,EAAgBC,EAAgBzE,EAAU,CAAA,GACjG,MAAMoE,iBACJA,EAAmBlE,EAAoBmE,kBAAiBV,aACxDA,EAAezD,EAAoB0D,6BAA4BtD,SAC/DA,EAAWJ,EAAoBK,mBAAkBC,SACjDA,EAAWN,EAAoBO,kBAAiBiE,aAChDA,EAAexE,EAAoByE,uBAAsBC,eACzDA,EAAiB,IACf5E,EAEJ,IAEE,MAAM6E,EAAmBX,0BAA0BK,EAAeH,EAAkBT,GAG9EmB,EAAuBvB,wBAAwBiB,EAAgBlE,EAAUE,EAAUmD,GAGnFoB,EAA2BxB,wBAAwBkB,EAAgBvE,EAAoB8E,wBAAyBN,EAAcf,GAWpI,OALsBkB,GAAoBD,EAAiB,MAHhCE,EAAuBC,EASpD,CAAE,MAAOrF,GACP,OAAO,CACT,CACF,CAGO,SAASuF,kBAAkBC,EAAYzF,EAAa0F,GACzD,OAAOD,GAAczF,EAAc0F,EACrC,CAQO,SAASC,2BAA2B9F,EAAa+F,GACtD,OAAK/F,GAAeA,GAAe,EAAU,GAErCA,EAAc+F,GAAgB/F,CACxC,CAQO,SAASgG,2BAA2BhG,EAAaiG,GACtD,OAAKjG,GAAeA,GAAe,EAAU,EAEtCA,GAAe,EAAIiG,EAC5B,CAEO,SAASC,eAAeC,EAAOC,EAAW,KAC/C,OAAiB,MAATD,GAAkBE,MAAMF,GAA0BC,EAAP,IAARD,CAC7C"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
function formatCurrency(e,t=!1){if(isNaN(e)||!isFinite(e))return"N/A";const i=Math.abs(e),r=e<0?"-$":"$";if(t)return r+i.toLocaleString("en-US",{maximumFractionDigits:0});if(i>=1e6){return r+(i/1e6).toFixed(3).replace(/\.?0+$/,"")+"M"}if(i>=1e3){return r+(i/1e3).toFixed(3).replace(/\.?0+$/,"")+"K"}return r+i.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPriceValue(e){if(isNaN(e)||!isFinite(e))return"N/A";const t=Math.abs(e),i=e<0?"-$":"$";return t>=1e6?i+(t/1e6).toFixed(1)+"M":t>=1e3?i+(t/1e3).toFixed(0)+"K":i+t.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPercentage(e){
|
|
1
|
+
function formatCurrency(e,t=!1){if(isNaN(e)||!isFinite(e))return"N/A";const i=Math.abs(e),r=e<0?"-$":"$";if(t)return r+i.toLocaleString("en-US",{maximumFractionDigits:0});if(i>=1e6){return r+(i/1e6).toFixed(3).replace(/\.?0+$/,"")+"M"}if(i>=1e3){return r+(i/1e3).toFixed(3).replace(/\.?0+$/,"")+"K"}return r+i.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPriceValue(e){if(isNaN(e)||!isFinite(e))return"N/A";const t=Math.abs(e),i=e<0?"-$":"$";return t>=1e6?i+(t/1e6).toFixed(1)+"M":t>=1e3?i+(t/1e3).toFixed(0)+"K":i+t.toLocaleString("en-US",{maximumFractionDigits:0})}function formatPercentage(e){if(isNaN(e)||!isFinite(e))return"N/A";let t=e.toString();t.includes("e")&&(t=Number(t).toFixed(12));const[i,r=""]=t.split("."),n=r.slice(0,2);return(n?`${i}.${n}`:i).replace(/\.?0+$/,"")+"%"}export{formatCurrency,formatPercentage,formatPriceValue};
|
|
2
2
|
//# sourceMappingURL=formatters.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatters.js","sources":["../../src/financial/formatters.js"],"sourcesContent":["/**\r\n * src/financial/formatters.js\r\n * \r\n * OUTPUT/DISPLAY FORMATTERS - For read-only display of financial data\r\n * \r\n * Purpose: Format calculated financial values for compact, readable display\r\n * Use cases:\r\n * - Browser extension metrics and tooltips\r\n * - Dashboard display values\r\n * - Comparison pages\r\n * - Any read-only financial data presentation\r\n * \r\n * Characteristics:\r\n * - Uses K/M notation for compact display (e.g., \"$2.5M\", \"$125K\")\r\n * - Optimized for space-constrained UI elements\r\n * - Not for user input fields (see formatting/financial-formatting.js)\r\n * \r\n * Related files:\r\n * - formatting/financial-formatting.js: Input formatters for editable fields\r\n */\r\n\r\n/**\r\n * Format currency with K/M notation for compact display\r\n * @param {number} amount - The amount to format\r\n * @param {boolean} isMonthly - If true, shows full amount with commas (for monthly payments)\r\n * @returns {string} Formatted currency string (e.g., \"$2.5M\", \"$125K\", \"$1,234\")\r\n */\r\nexport function formatCurrency(amount, isMonthly = false) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (isMonthly) {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n } else {\r\n if (absAmount >= 1000000) {\r\n const millions = absAmount / 1000000;\r\n const formatted = millions.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"M\";\r\n } else if (absAmount >= 1000) {\r\n const thousands = absAmount / 1000;\r\n const formatted = thousands.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Format price value with K/M notation for compact display\r\n * Similar to formatCurrency but with fixed decimal places\r\n * @param {number} amount - The amount to format\r\n * @returns {string} Formatted price string (e.g., \"$2.5M\", \"$125K\")\r\n */\r\nexport function formatPriceValue(amount) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (absAmount >= 1000000) {\r\n return prefix + (absAmount / 1000000).toFixed(1) + \"M\";\r\n } else if (absAmount >= 1000) {\r\n return prefix + (absAmount / 1000).toFixed(0) + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n}\r\n\r\n/**\r\n * Format percentage for display\r\n * @param {number} percentage - The percentage value to format (e.g., 7.5 for 7.5%)\r\n * @returns {string} Formatted percentage string (e.g., \"7.5%\")\r\n */\r\nexport function formatPercentage(percentage) {\r\n if (isNaN(percentage) || !isFinite(percentage)) return \"N/A\";\r\n
|
|
1
|
+
{"version":3,"file":"formatters.js","sources":["../../src/financial/formatters.js"],"sourcesContent":["/**\r\n * src/financial/formatters.js\r\n * \r\n * OUTPUT/DISPLAY FORMATTERS - For read-only display of financial data\r\n * \r\n * Purpose: Format calculated financial values for compact, readable display\r\n * Use cases:\r\n * - Browser extension metrics and tooltips\r\n * - Dashboard display values\r\n * - Comparison pages\r\n * - Any read-only financial data presentation\r\n * \r\n * Characteristics:\r\n * - Uses K/M notation for compact display (e.g., \"$2.5M\", \"$125K\")\r\n * - Optimized for space-constrained UI elements\r\n * - Not for user input fields (see formatting/financial-formatting.js)\r\n * \r\n * Related files:\r\n * - formatting/financial-formatting.js: Input formatters for editable fields\r\n */\r\n\r\n/**\r\n * Format currency with K/M notation for compact display\r\n * @param {number} amount - The amount to format\r\n * @param {boolean} isMonthly - If true, shows full amount with commas (for monthly payments)\r\n * @returns {string} Formatted currency string (e.g., \"$2.5M\", \"$125K\", \"$1,234\")\r\n */\r\nexport function formatCurrency(amount, isMonthly = false) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (isMonthly) {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n } else {\r\n if (absAmount >= 1000000) {\r\n const millions = absAmount / 1000000;\r\n const formatted = millions.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"M\";\r\n } else if (absAmount >= 1000) {\r\n const thousands = absAmount / 1000;\r\n const formatted = thousands.toFixed(3).replace(/\\.?0+$/, \"\");\r\n return prefix + formatted + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Format price value with K/M notation for compact display\r\n * Similar to formatCurrency but with fixed decimal places\r\n * @param {number} amount - The amount to format\r\n * @returns {string} Formatted price string (e.g., \"$2.5M\", \"$125K\")\r\n */\r\nexport function formatPriceValue(amount) {\r\n if (isNaN(amount) || !isFinite(amount)) return \"N/A\";\r\n \r\n const absAmount = Math.abs(amount);\r\n const isNegative = amount < 0;\r\n const prefix = isNegative ? \"-$\" : \"$\";\r\n \r\n if (absAmount >= 1000000) {\r\n return prefix + (absAmount / 1000000).toFixed(1) + \"M\";\r\n } else if (absAmount >= 1000) {\r\n return prefix + (absAmount / 1000).toFixed(0) + \"K\";\r\n } else {\r\n return prefix + absAmount.toLocaleString(\"en-US\", { maximumFractionDigits: 0 });\r\n }\r\n}\r\n\r\n/**\r\n * Format percentage for display\r\n * @param {number} percentage - The percentage value to format (e.g., 7.5 for 7.5%)\r\n * @returns {string} Formatted percentage string (e.g., \"7.5%\")\r\n * \r\n */\r\nexport function formatPercentage(percentage) {\r\n if (isNaN(percentage) || !isFinite(percentage)) return \"N/A\";\r\n\r\n // Convert to string with enough decimals\r\n let str = percentage.toString();\r\n\r\n if (str.includes(\"e\")) {\r\n // Handle scientific notation\r\n str = Number(str).toFixed(2 + 10);\r\n }\r\n\r\n // Split integer and decimal parts\r\n const [intPart, decPart = \"\"] = str.split(\".\");\r\n\r\n // Take up to 2 decimals without rounding\r\n const truncatedDec = decPart.slice(0, 2);\r\n\r\n // Combine and remove trailing zeros\r\n const combined = truncatedDec ? `${intPart}.${truncatedDec}` : intPart;\r\n const cleaned = combined.replace(/\\.?0+$/, \"\");\r\n\r\n return cleaned + \"%\";\r\n}\r\n"],"names":["formatCurrency","amount","isMonthly","isNaN","isFinite","absAmount","Math","abs","prefix","toLocaleString","maximumFractionDigits","toFixed","replace","formatPriceValue","formatPercentage","percentage","str","toString","includes","Number","intPart","decPart","split","truncatedDec","slice"],"mappings":"AA2BO,SAASA,eAAeC,EAAQC,GAAY,GACjD,GAAIC,MAAMF,KAAYG,SAASH,GAAS,MAAO,MAE/C,MAAMI,EAAYC,KAAKC,IAAIN,GAErBO,EADaP,EAAS,EACA,KAAO,IAEnC,GAAIC,EACF,OAAOM,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,IAE3E,GAAIL,GAAa,IAAS,CAGxB,OAAOG,GAFUH,EAAY,KACFM,QAAQ,GAAGC,QAAQ,SAAU,IAC5B,GAC9B,CAAO,GAAIP,GAAa,IAAM,CAG5B,OAAOG,GAFWH,EAAY,KACFM,QAAQ,GAAGC,QAAQ,SAAU,IAC7B,GAC9B,CACE,OAAOJ,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,GAGjF,CAQO,SAASG,iBAAiBZ,GAC/B,GAAIE,MAAMF,KAAYG,SAASH,GAAS,MAAO,MAE/C,MAAMI,EAAYC,KAAKC,IAAIN,GAErBO,EADaP,EAAS,EACA,KAAO,IAEnC,OAAII,GAAa,IACRG,GAAUH,EAAY,KAASM,QAAQ,GAAK,IAC1CN,GAAa,IACfG,GAAUH,EAAY,KAAMM,QAAQ,GAAK,IAEzCH,EAASH,EAAUI,eAAe,QAAS,CAAEC,sBAAuB,GAE/E,CAQO,SAASI,iBAAiBC,GAC/B,GAAIZ,MAAMY,KAAgBX,SAASW,GAAa,MAAO,MAGvD,IAAIC,EAAMD,EAAWE,WAEjBD,EAAIE,SAAS,OAEfF,EAAMG,OAAOH,GAAKL,QAAQ,KAI5B,MAAOS,EAASC,EAAU,IAAML,EAAIM,MAAM,KAGpCC,EAAeF,EAAQG,MAAM,EAAG,GAMtC,OAHiBD,EAAe,GAAGH,KAAWG,IAAiBH,GACtCR,QAAQ,SAAU,IAE1B,GACnB"}
|