@archerjessop/utilities 7.25.0 → 7.26.0

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,2 +1,2 @@
1
- import{FINANCIAL_CONSTANTS as e}from"../config/financial.js";import{BUSINESS_CONSTANTS as t}from"../config/business.js";import{PROPERTY_TYPES as r,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";const c=e.INTEREST_RATE_TIERS[e.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(e,t,r){if(0===t)return e/(12*r);const a=t/12,c=12*r;return e*(a*Math.pow(1+a,c))/(Math.pow(1+a,c)-1)}function calculateCOCR30(e,t){try{const r=.3*e,a=12*calculatePMT(.7*e,.075,30);return(t-a)/r*100}catch(e){return 0}}function calculateCashFlowYield(e,t){if(!t||t<=0)return 0;return 12*e/t*100}function equityPercentFromDebt(e,t){const r=Number(e),a=Number(t);return!Number.isFinite(r)||r<=0?1:Number.isFinite(a)?(r-a)/r:1}function calculatePriceForCOCR(r,a=.15,n={}){const{downPercent:l=100*e.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:u=100*e.DEFAULT_DSCR_PERCENTAGE,dscrRate:o=c.rate,dscrTerm:i=c.amortization,maxIterations:s=t.MAX_ITERATIONS,tolerance:E=t.CALCULATION_TOLERANCE}=n;try{let e=r/.08,c=0;for(;c<s;){const n=e*(l/100),s=12*calculatePMT(e*(u/100),o,i),T=(r-s)/n;if(Math.abs(T-a)<E)break;const R=T-a,A=R*t.ADJUSTMENT_FACTOR;e*=R>0?1+Math.abs(A):1-Math.abs(A),e<1e3&&(e=1e3),e>r*t.MAX_COCR15_PRICE_MULTIPLIER&&(e=r*t.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),c++}return e<t.MINIMUM_COCR15_PRICE&&(e=t.MINIMUM_COCR15_PRICE),e}catch(e){return 0}}function calculateCOCRAtPercent(e,t,r,a={}){const{dscrRate:n=c.rate,dscrTerm:l=c.amortization}=a;try{const a=e*(r/100),c=12*calculatePMT(e-a,n,l);return(t-c)/a*100}catch(e){return 0}}function calculateSTRNOI(e,t=null,r={}){const{grossRate:c=a.STR.ESTIMATED_GROSS_RATE,noiPercentage:n=a.STR.NOI_PERCENTAGE}=r;try{if(t&&Number.isFinite(t.value)&&t.value>=0){if("noi"===t.type)return t.value;if("gross"===t.type)return t.value*n}return!Number.isFinite(e)||e<=0?0:e*c*n}catch(e){return 0}}function calculateNOIByType(e,t,c=r.MULTIFAMILY,n={}){const{strApiResult:l=null,strGrossIncomeMultiplier:u=a.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:o=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:i=a.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:s=a.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch(c.toLowerCase()){case r.STR:return calculateSTRNOI(e,l,{grossRate:u,noiPercentage:o});case r.ASSISTED_LIVING:return s*i*12;case r.MULTIFAMILY:default:return e*t}}catch(e){return 0}}function resolveListingFinancials({bedroomCount:e=null,confirmedNOI:t=null,estimatedCapRate:a,price:c,propertyType:n=r.MULTIFAMILY,reportedCapRate:l=null,strApiResult:u=null}={}){const o=null!=l&&Number.isFinite(l),i=o?l:a,s=u&&Number.isFinite(u.value)&&u.value>=0&&("noi"===u.type||"gross"===u.type);let E,T;if(null!=t&&Number.isFinite(t)&&t>=0)E=t,T="confirmed";else{E=calculateNOIByType(c,i,n,{bedroomCount:e,strApiResult:u});const t=(n||"").toLowerCase();T=t===r.STR?s?"measured":"estimate":t===r.ASSISTED_LIVING?"bedrooms":o?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(c)&&c>0?E/c:null,noi:E,noiSource:T,reportedCapRate:o?l:null}}function calculateAssignmentFee(e,r=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(r/100)}catch(e){return 0}}function calculateNetToBuyer(r,a={}){const{buyerCostPercent:c=100*t.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*t.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*t.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*t.REHAB_RATE,additionalCostFinancing:o=100*t.HARD_MONEY_RATE,dscrLtvPercent:i=100*e.DEFAULT_DSCR_PERCENTAGE}=a;try{return r*(c/100)-r*((n+l)/100)-r*(u/100)-o/100*(r-r*(i/100))}catch(e){return 0}}function calculateBalloonBalance(t,r,a,c=e.DEFAULT_BALLOON_PERIOD_YEARS){try{if(t<=0||r<0||a<=0||c<=0)return 0;if(c>=a)return 0;if(0===r){const e=12*a;return t*(e-12*c)/e}const e=r/12,n=12*a,l=12*c,u=Math.pow(1+e,n),o=t*(u-Math.pow(1+e,l))/(u-1);return Math.max(0,o)}catch(e){return 0}}function calculateAppreciatedValue(t,r=e.APPRECIATION_RATE,a=e.DEFAULT_BALLOON_PERIOD_YEARS){try{return t<=0||r<0||a<0?t:t*Math.pow(1+r,a)}catch(e){return t}}function calculateCashOutAfterRefi(t,r,a,n={}){const{appreciationRate:l=e.APPRECIATION_RATE,balloonYears:u=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:o=c.rate,dscrTerm:i=c.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=n;try{const c=calculateAppreciatedValue(t,l,u),n=calculateBalloonBalance(r,o,i,u),T=calculateBalloonBalance(a,e.SELLER_FI_INTEREST_RATE,s,u);return c*(E/100)-(n+T)}catch(e){return 0}}function calculateCashFlow(e,t,r){return e-(t+r)}function calculateDiscountFromPrice(e,t){return!e||e<=0?0:(e-t)/e}function calculatePriceFromDiscount(e,t){return!e||e<=0?0:e*(1-t)}function safePercentage(e,t=100){return null==e||isNaN(e)?t:100*e}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage};
1
+ import{FINANCIAL_CONSTANTS as e}from"../config/financial.js";import{BUSINESS_CONSTANTS as t}from"../config/business.js";import{PROPERTY_TYPES as r,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";const c=e.INTEREST_RATE_TIERS[e.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(e,t,r){if(0===t)return e/(12*r);const a=t/12,c=12*r;return e*(a*Math.pow(1+a,c))/(Math.pow(1+a,c)-1)}function calculateCOCR30(e,t){try{const r=.3*e,a=12*calculatePMT(.7*e,.075,30);return(t-a)/r*100}catch(e){return 0}}function calculateCashFlowYield(e,t){if(!t||t<=0)return 0;return 12*e/t*100}function equityPercentFromDebt(e,t){const r=Number(e),a=Number(t);return!Number.isFinite(r)||r<=0?1:Number.isFinite(a)?(r-a)/r:1}function calculatePriceForCOCR(r,a=.15,n={}){const{downPercent:l=100*e.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:u=100*e.DEFAULT_DSCR_PERCENTAGE,dscrRate:o=c.rate,dscrTerm:i=c.amortization,maxIterations:s=t.MAX_ITERATIONS,tolerance:E=t.CALCULATION_TOLERANCE}=n;try{let e=r/.08,c=0;for(;c<s;){const n=e*(l/100),s=12*calculatePMT(e*(u/100),o,i),T=(r-s)/n;if(Math.abs(T-a)<E)break;const R=T-a,A=R*t.ADJUSTMENT_FACTOR;e*=R>0?1+Math.abs(A):1-Math.abs(A),e<1e3&&(e=1e3),e>r*t.MAX_COCR15_PRICE_MULTIPLIER&&(e=r*t.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),c++}return e<t.MINIMUM_COCR15_PRICE&&(e=t.MINIMUM_COCR15_PRICE),e}catch(e){return 0}}function calculateCOCRAtPercent(e,t,r,a={}){const{dscrRate:n=c.rate,dscrTerm:l=c.amortization}=a;try{const a=e*(r/100),c=12*calculatePMT(e-a,n,l);return(t-c)/a*100}catch(e){return 0}}function calculateSTRNOI(e,t=null,r={}){const{grossRate:c=a.STR.ESTIMATED_GROSS_RATE,noiPercentage:n=a.STR.NOI_PERCENTAGE}=r;try{if(t&&Number.isFinite(t.value)&&t.value>=0){if("noi"===t.type)return t.value;if("gross"===t.type)return t.value*n}return!Number.isFinite(e)||e<=0?0:e*c*n}catch(e){return 0}}function calculateNOIByType(e,t,c=r.MULTIFAMILY,n={}){const{strApiResult:l=null,strGrossIncomeMultiplier:u=a.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:o=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:i=a.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:s=a.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch((c||r.MULTIFAMILY).toLowerCase()){case r.STR:return calculateSTRNOI(e,l,{grossRate:u,noiPercentage:o});case r.ASSISTED_LIVING:return s*i*12;case r.MULTIFAMILY:default:return e*t}}catch(e){return 0}}function resolveListingFinancials({bedroomCount:e=null,confirmedNOI:t=null,estimatedCapRate:a,price:c,propertyType:n=r.MULTIFAMILY,reportedCapRate:l=null,strApiResult:u=null}={}){const o=null!=l&&Number.isFinite(l),i=o?l:a,s=u&&Number.isFinite(u.value)&&u.value>=0&&("noi"===u.type||"gross"===u.type);let E,T;if(null!=t&&Number.isFinite(t)&&t>=0)E=t,T="confirmed";else{E=calculateNOIByType(c,i,n,{bedroomCount:e,strApiResult:u});const t=(n||"").toLowerCase();T=t===r.STR?s?"measured":"estimate":t===r.ASSISTED_LIVING?"bedrooms":o?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(c)&&c>0?E/c:null,noi:E,noiSource:T,reportedCapRate:o?l:null}}function calculateAssignmentFee(e,r=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(r/100)}catch(e){return 0}}function calculateNetToBuyer(r,a={}){const{buyerCostPercent:c=100*t.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*t.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*t.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*t.REHAB_RATE,additionalCostFinancing:o=100*t.HARD_MONEY_RATE,dscrLtvPercent:i=100*e.DEFAULT_DSCR_PERCENTAGE}=a;try{return r*(c/100)-r*((n+l)/100)-r*(u/100)-o/100*(r-r*(i/100))}catch(e){return 0}}function calculateBalloonBalance(t,r,a,c=e.DEFAULT_BALLOON_PERIOD_YEARS){try{if(t<=0||r<0||a<=0||c<=0)return 0;if(c>=a)return 0;if(0===r){const e=12*a;return t*(e-12*c)/e}const e=r/12,n=12*a,l=12*c,u=Math.pow(1+e,n),o=t*(u-Math.pow(1+e,l))/(u-1);return Math.max(0,o)}catch(e){return 0}}function calculateAppreciatedValue(t,r=e.APPRECIATION_RATE,a=e.DEFAULT_BALLOON_PERIOD_YEARS){try{return t<=0||r<0||a<0?t:t*Math.pow(1+r,a)}catch(e){return t}}function calculateCashOutAfterRefi(t,r,a,n={}){const{appreciationRate:l=e.APPRECIATION_RATE,balloonYears:u=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:o=c.rate,dscrTerm:i=c.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=n;try{const c=calculateAppreciatedValue(t,l,u),n=calculateBalloonBalance(r,o,i,u),T=calculateBalloonBalance(a,e.SELLER_FI_INTEREST_RATE,s,u);return c*(E/100)-(n+T)}catch(e){return 0}}function calculateCashFlow(e,t,r){return e-(t+r)}function calculateDiscountFromPrice(e,t){return!e||e<=0?0:(e-t)/e}function calculatePriceFromDiscount(e,t){return!e||e<=0?0:e*(1-t)}function safePercentage(e,t=100){return null==e||isNaN(e)?t:100*e}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage};
2
2
  //# sourceMappingURL=calculations.js.map
@@ -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\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\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 * Equity as a decimal fraction of price, derived from outstanding debt.\r\n * equity = (price - debtBalance) / price. Recompute whenever the user edits price.\r\n * Falls back to 1 (100% equity) when debt is unavailable or price is non-positive,\r\n * matching the \"estimated = 100%\" rule when the debt service returns no number.\r\n * @param {number} price - Listing/asking/offered price.\r\n * @param {number} debtBalance - Outstanding mortgage balance owing.\r\n * @returns {number} Equity as a decimal (e.g. 0.59 for 59%); 1 when debt is unknown.\r\n */\r\nexport function equityPercentFromDebt(price, debtBalance) {\r\n const p = Number(price);\r\n const d = Number(debtBalance);\r\n if (!Number.isFinite(p) || p <= 0) return 1;\r\n if (!Number.isFinite(d)) return 1;\r\n return (p - d) / p;\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 = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.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 = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.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 STR (short-term rental) NOI - the single source of STR NOI math.\r\n *\r\n * Resolution order:\r\n * 1. apiResult type 'noi' -> value is already net, return as-is\r\n * 2. apiResult type 'gross' -> apply NOI margin (value * noiPercentage)\r\n * 3. no/invalid apiResult -> estimate from price (price * grossRate * noiPercentage)\r\n *\r\n * apiResult comes from api.archerjessop.com/str-revenue: { value, type }.\r\n * Pass null while that backend is not live (the price estimate is used).\r\n *\r\n * @param {number} askingPrice - Property asking price\r\n * @param {{value:number, type:'noi'|'gross'}|null} apiResult - STR revenue API result\r\n * @param {Object} options - Rate overrides (default to STR config constants)\r\n * @returns {number} Annual NOI (0 on invalid input)\r\n */\r\nexport function calculateSTRNOI(askingPrice, apiResult = null, options = {}) {\r\n const {\r\n grossRate = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n noiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE\r\n } = options;\r\n\r\n try {\r\n if (apiResult && Number.isFinite(apiResult.value) && apiResult.value >= 0) {\r\n if (apiResult.type === \"noi\") return apiResult.value;\r\n if (apiResult.type === \"gross\") return apiResult.value * noiPercentage;\r\n }\r\n\r\n if (!Number.isFinite(askingPrice) || askingPrice <= 0) return 0;\r\n return askingPrice * grossRate * noiPercentage;\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 strApiResult = null,\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 return calculateSTRNOI(askingPrice, strApiResult, {\r\n grossRate: strGrossIncomeMultiplier,\r\n noiPercentage: strNoiPercentage\r\n });\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 * Resolve a listing's canonical NOI and the cap rates derived from it.\r\n *\r\n * NOI is the source of truth; each property type computes it by its own model\r\n * (delegated to calculateNOIByType, unchanged). The active (displayed) cap rate is\r\n * always derived as NOI / price. The reported cap rate is carried through as\r\n * provenance (shown on hover; null => the UI shows \"N/A\"); it only DRIVES the NOI for\r\n * multifamily, where the cap rate is the model input.\r\n *\r\n * NOI precedence: an analyst-confirmed NOI overrides the per-type model; otherwise STR\r\n * uses measured 3rd-party revenue when present and the price-based estimate otherwise;\r\n * assisted uses bedroom count; multifamily uses the reported cap (or estimatedCapRate\r\n * when none was reported).\r\n *\r\n * @param {Object} input\r\n * @param {number|null} [input.bedroomCount] - Bedroom count for assisted living\r\n * @param {number|null} [input.confirmedNOI] - Analyst-confirmed/edited NOI; overrides the per-type model\r\n * @param {number} [input.estimatedCapRate] - Fallback cap (decimal) used for multifamily NOI when none was reported\r\n * @param {number} input.price - Asking price\r\n * @param {string} [input.propertyType] - One of PROPERTY_TYPES\r\n * @param {number|null} [input.reportedCapRate] - Real scraped/listed cap rate as a decimal (e.g. 0.0486); null when none was reported\r\n * @param {{value:number,type:'noi'|'gross'}|null} [input.strApiResult] - Measured STR revenue (3rd-party); null until that backend ships\r\n * @returns {{activeCapRate:(number|null), noi:number, noiSource:string, reportedCapRate:(number|null)}}\r\n */\r\nexport function resolveListingFinancials({\r\n bedroomCount = null,\r\n confirmedNOI = null,\r\n estimatedCapRate,\r\n price,\r\n propertyType = PROPERTY_TYPES.MULTIFAMILY,\r\n reportedCapRate = null,\r\n strApiResult = null,\r\n} = {}) {\r\n const hasReported = reportedCapRate != null && Number.isFinite(reportedCapRate);\r\n const capForNOI = hasReported ? reportedCapRate : estimatedCapRate;\r\n const measured = strApiResult && Number.isFinite(strApiResult.value) && strApiResult.value >= 0 &&\r\n (strApiResult.type === \"noi\" || strApiResult.type === \"gross\");\r\n\r\n let noi;\r\n let noiSource;\r\n if (confirmedNOI != null && Number.isFinite(confirmedNOI) && confirmedNOI >= 0) {\r\n noi = confirmedNOI;\r\n noiSource = \"confirmed\";\r\n } else {\r\n noi = calculateNOIByType(price, capForNOI, propertyType, { bedroomCount, strApiResult });\r\n const type = (propertyType || \"\").toLowerCase();\r\n if (type === PROPERTY_TYPES.STR) {\r\n noiSource = measured ? \"measured\" : \"estimate\";\r\n } else if (type === PROPERTY_TYPES.ASSISTED_LIVING) {\r\n noiSource = \"bedrooms\";\r\n } else {\r\n noiSource = hasReported ? \"cap\" : \"estimate\";\r\n }\r\n }\r\n\r\n if (!Number.isFinite(noi)) noi = 0;\r\n const activeCapRate = Number.isFinite(price) && price > 0 ? noi / price : null;\r\n\r\n return {\r\n activeCapRate,\r\n noi,\r\n noiSource,\r\n reportedCapRate: hasReported ? reportedCapRate : null,\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 = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.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":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","equityPercentFromDebt","price","debtBalance","p","Number","d","isFinite","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","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","calculateSTRNOI","apiResult","grossRate","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","noiPercentage","NOI_PERCENTAGE","value","type","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strApiResult","strGrossIncomeMultiplier","strNoiPercentage","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","resolveListingFinancials","confirmedNOI","estimatedCapRate","reportedCapRate","hasReported","capForNOI","measured","noiSource","activeCapRate","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","fallback","isNaN"],"mappings":"kNAMA,MAAMA,EAAeC,EAAoBC,oBAAoBD,EAAoBE,4BAU1E,SAASC,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,CAWO,SAASC,sBAAsBC,EAAOC,GAC3C,MAAMC,EAAIC,OAAOH,GACXI,EAAID,OAAOF,GACjB,OAAKE,OAAOE,SAASH,IAAMA,GAAK,EAAU,EACrCC,OAAOE,SAASD,IACbF,EAAIE,GAAKF,EADe,CAElC,CAUO,SAASI,sBAAsBd,EAAKe,EAAa,IAAMC,EAAU,CAAA,GACtE,MAAMC,YACJA,EAAyD,IAA3C9B,EAAoB+B,qBAA0BC,eAC5DA,EAA+D,IAA9ChC,EAAoBiC,wBAA6BC,SAClEA,EAAWnC,EAAaoC,KAAIC,SAC5BA,EAAWrC,EAAasC,aAAYC,cACpCA,EAAgBC,EAAmBC,eAAcC,UACjDA,EAAYF,EAAmBG,uBAC7Bb,EAEJ,IACE,IAAIc,EAAc9B,EAAM,IACpB+B,EAAa,EAEjB,KAAOA,EAAaN,GAAe,CACjC,MAAMxB,EAAe6B,GAAeb,EAAc,KAE5Cf,EAAiE,GAAnDZ,aADGwC,GAAeX,EAAiB,KACNE,EAAUE,GAErDS,GADiBhC,EAAME,GACQD,EAErC,GAAIL,KAAKqC,IAAID,EAAcjB,GAAca,EACvC,MAGF,MAAMzB,EAAQ6B,EAAcjB,EACtBmB,EAAa/B,EAAQuB,EAAmBS,kBAG5CL,GADE3B,EAAQ,EACmB,EAAIP,KAAKqC,IAAIC,GAEb,EAAItC,KAAKqC,IAAIC,GAIxCJ,EAAc,MAAMA,EAAc,KAClCA,EAAc9B,EAAM0B,EAAmBU,8BACzCN,EAAc9B,EAAM0B,EAAmBW,sCAGzCN,GACF,CAOA,OAJID,EAAcJ,EAAmBY,uBACnCR,EAAcJ,EAAmBY,sBAG5BR,CACT,CAAE,MAAO3B,GACP,OAAO,CACT,CACF,CAUO,SAASoC,uBAAuBxC,EAAaC,EAAKiB,EAAaD,EAAU,CAAA,GAC9E,MAAMK,SACJA,EAAWnC,EAAaoC,KAAIC,SAC5BA,EAAWrC,EAAasC,cACtBR,EAEJ,IACE,MACMf,EAAeF,GADDkB,EAAc,KAM5Bf,EAAiE,GAAnDZ,aAFGS,EAAcE,EAEYoB,EAAUE,GAK3D,OAHuBvB,EAAME,GACED,EAAgB,GAGjD,CAAE,MAAOE,GACP,OAAO,CACT,CACF,CAkBO,SAASqC,gBAAgBzC,EAAa0C,EAAY,KAAMzB,EAAU,CAAA,GACvE,MAAM0B,UACJA,EAAYC,EAAwBC,IAAIC,qBAAoBC,cAC5DA,EAAgBH,EAAwBC,IAAIG,gBAC1C/B,EAEJ,IACE,GAAIyB,GAAa9B,OAAOE,SAAS4B,EAAUO,QAAUP,EAAUO,OAAS,EAAG,CACzE,GAAuB,QAAnBP,EAAUQ,KAAgB,OAAOR,EAAUO,MAC/C,GAAuB,UAAnBP,EAAUQ,KAAkB,OAAOR,EAAUO,MAAQF,CAC3D,CAEA,OAAKnC,OAAOE,SAASd,IAAgBA,GAAe,EAAU,EACvDA,EAAc2C,EAAYI,CACnC,CAAE,MAAO3C,GACP,OAAO,CACT,CACF,CAUO,SAAS+C,mBAAmBnD,EAAaoD,EAASC,EAAeC,EAAeC,YAAatC,EAAU,IAC5G,MAAMuC,aACJA,EAAe,KAAIC,yBACnBA,EAA2Bb,EAAwBC,IAAIC,qBAAoBY,iBAC3EA,EAAmBd,EAAwBC,IAAIG,eAAcW,yBAC7DA,EAA2Bf,EAAwBgB,gBAAgBC,2BAA0BC,aAC7FA,EAAelB,EAAwBgB,gBAAgBG,uBACrD9C,EAEJ,IACE,OAAQoC,EAAaW,eACnB,KAAKV,EAAeT,IAClB,OAAOJ,gBAAgBzC,EAAawD,EAAc,CAChDb,UAAWc,EACXV,cAAeW,IAGnB,KAAKJ,EAAeM,gBAClB,OAAOE,EAAeH,EAA2B,GAEnD,KAAKL,EAAeC,YACpB,QACE,OAAOvD,EAAcoD,EAE3B,CAAE,MAAOhD,GACP,OAAO,CACT,CACF,CA0BO,SAAS6D,0BAAyBH,aACvCA,EAAe,KAAII,aACnBA,EAAe,KAAIC,iBACnBA,EAAgB1D,MAChBA,EAAK4C,aACLA,EAAeC,EAAeC,YAAWa,gBACzCA,EAAkB,KAAIZ,aACtBA,EAAe,MACb,IACF,MAAMa,EAAiC,MAAnBD,GAA2BxD,OAAOE,SAASsD,GACzDE,EAAYD,EAAcD,EAAkBD,EAC5CI,EAAWf,GAAgB5C,OAAOE,SAAS0C,EAAaP,QAAUO,EAAaP,OAAS,IACrE,QAAtBO,EAAaN,MAAwC,UAAtBM,EAAaN,MAE/C,IAAIjD,EACAuE,EACJ,GAAoB,MAAhBN,GAAwBtD,OAAOE,SAASoD,IAAiBA,GAAgB,EAC3EjE,EAAMiE,EACNM,EAAY,gBACP,CACLvE,EAAMkD,mBAAmB1C,EAAO6D,EAAWjB,EAAc,CAAES,eAAcN,iBACzE,MAAMN,GAAQG,GAAgB,IAAIW,cAEhCQ,EADEtB,IAASI,EAAeT,IACd0B,EAAW,WAAa,WAC3BrB,IAASI,EAAeM,gBACrB,WAEAS,EAAc,MAAQ,UAEtC,CAEKzD,OAAOE,SAASb,KAAMA,EAAM,GAGjC,MAAO,CACLwE,cAHoB7D,OAAOE,SAASL,IAAUA,EAAQ,EAAIR,EAAMQ,EAAQ,KAIxER,MACAuE,YACAJ,gBAAiBC,EAAcD,EAAkB,KAErD,CAQO,SAASM,uBAAuB1E,EAAa2E,EAAmE,IAA/ChD,EAAmBiD,2BACzF,IACE,OAAO5E,GAAe2E,EAAoB,IAC5C,CAAE,MAAOvE,GACP,OAAO,CACT,CACF,CAQO,SAASyE,oBAAoB7E,EAAaiB,EAAU,IACzD,MAAM6D,iBACJA,EAAgE,IAA7CnD,EAAmBoD,wBAA6BC,qBACnEA,EAAsE,IAA/CrD,EAAmBiD,0BAA+BK,kBACzEA,EAAkE,IAA9CtD,EAAmBuD,yBAA8BC,oBACrEA,EAAsD,IAAhCxD,EAAmByD,WAAgBC,wBACzDA,EAA+D,IAArC1D,EAAmB2D,gBAAqBlE,eAClEA,EAA+D,IAA9ChC,EAAoBiC,yBACnCJ,EAEJ,IAGE,OAAOjB,GAAe8E,EAAmB,KAClC9E,IAAgBgF,EAAuBC,GAAqB,KAC5DjF,GAAemF,EAAsB,KACpCE,EAA0B,KAAQrF,EALnBA,GAAeoB,EAAiB,KAMzD,CAAE,MAAOhB,GACP,OAAO,CACT,CACF,CAUO,SAASmF,wBAAwBC,EAAYC,EAAcC,EAAmBC,EAAevG,EAAoBwG,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,MAAMlG,EAAc8F,EAAe,GAC7BI,EAAoC,GAApBH,EAChBI,EAAiC,GAAfH,EAMlBI,EAAUlG,KAAKC,IAAI,EAAIH,EAAakG,GAGpCG,EAAmBR,GAAcO,EAFvBlG,KAAKC,IAAI,EAAIH,EAAamG,KAEmBC,EAAU,GAEvE,OAAOlG,KAAKoG,IAAI,EAAGD,EACrB,CAAE,MAAO5F,GACP,OAAO,CACT,CACF,CASO,SAAS8F,0BAA0BC,EAAcC,EAAmBhH,EAAoBiH,kBAAmB3G,EAAQN,EAAoBwG,8BAC5I,IACE,OAAIO,GAAgB,GAAKC,EAAmB,GAAK1G,EAAQ,EAChDyG,EAGFA,EAAetG,KAAKC,IAAI,EAAIsG,EAAkB1G,EACvD,CAAE,MAAOU,GACP,OAAO+F,CACT,CACF,CAUO,SAASG,0BAA0BC,EAAeC,EAAgBC,EAAgBxF,EAAU,CAAA,GACjG,MAAMmF,iBACJA,EAAmBhH,EAAoBiH,kBAAiBV,aACxDA,EAAevG,EAAoBwG,6BAA4BtE,SAC/DA,EAAWnC,EAAaoC,KAAIC,SAC5BA,EAAWrC,EAAasC,aAAYiF,aACpCA,EAAetH,EAAoBuH,uBAAsBC,eACzDA,EAAiB,IACf3F,EAEJ,IAEE,MAAM4F,EAAmBX,0BAA0BK,EAAeH,EAAkBT,GAG9EmB,EAAuBvB,wBAAwBiB,EAAgBlF,EAAUE,EAAUmE,GAGnFoB,EAA2BxB,wBAAwBkB,EAAgBrH,EAAoB4H,wBAAyBN,EAAcf,GAWpI,OALsBkB,GAAoBD,EAAiB,MAHhCE,EAAuBC,EASpD,CAAE,MAAO3G,GACP,OAAO,CACT,CACF,CAGO,SAAS6G,kBAAkBC,EAAY/G,EAAagH,GACzD,OAAOD,GAAc/G,EAAcgH,EACrC,CAQO,SAASC,2BAA2BpH,EAAaqH,GACtD,OAAKrH,GAAeA,GAAe,EAAU,GAErCA,EAAcqH,GAAgBrH,CACxC,CAQO,SAASsH,2BAA2BtH,EAAauH,GACtD,OAAKvH,GAAeA,GAAe,EAAU,EAEtCA,GAAe,EAAIuH,EAC5B,CAEO,SAASC,eAAevE,EAAOwE,EAAW,KAC/C,OAAiB,MAATxE,GAAkByE,MAAMzE,GAA0BwE,EAAP,IAARxE,CAC7C"}
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\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\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 * Equity as a decimal fraction of price, derived from outstanding debt.\r\n * equity = (price - debtBalance) / price. Recompute whenever the user edits price.\r\n * Falls back to 1 (100% equity) when debt is unavailable or price is non-positive,\r\n * matching the \"estimated = 100%\" rule when the debt service returns no number.\r\n * @param {number} price - Listing/asking/offered price.\r\n * @param {number} debtBalance - Outstanding mortgage balance owing.\r\n * @returns {number} Equity as a decimal (e.g. 0.59 for 59%); 1 when debt is unknown.\r\n */\r\nexport function equityPercentFromDebt(price, debtBalance) {\r\n const p = Number(price);\r\n const d = Number(debtBalance);\r\n if (!Number.isFinite(p) || p <= 0) return 1;\r\n if (!Number.isFinite(d)) return 1;\r\n return (p - d) / p;\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 = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.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 = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.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 STR (short-term rental) NOI - the single source of STR NOI math.\r\n *\r\n * Resolution order:\r\n * 1. apiResult type 'noi' -> value is already net, return as-is\r\n * 2. apiResult type 'gross' -> apply NOI margin (value * noiPercentage)\r\n * 3. no/invalid apiResult -> estimate from price (price * grossRate * noiPercentage)\r\n *\r\n * apiResult comes from api.archerjessop.com/str-revenue: { value, type }.\r\n * Pass null while that backend is not live (the price estimate is used).\r\n *\r\n * @param {number} askingPrice - Property asking price\r\n * @param {{value:number, type:'noi'|'gross'}|null} apiResult - STR revenue API result\r\n * @param {Object} options - Rate overrides (default to STR config constants)\r\n * @returns {number} Annual NOI (0 on invalid input)\r\n */\r\nexport function calculateSTRNOI(askingPrice, apiResult = null, options = {}) {\r\n const {\r\n grossRate = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n noiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE\r\n } = options;\r\n\r\n try {\r\n if (apiResult && Number.isFinite(apiResult.value) && apiResult.value >= 0) {\r\n if (apiResult.type === \"noi\") return apiResult.value;\r\n if (apiResult.type === \"gross\") return apiResult.value * noiPercentage;\r\n }\r\n\r\n if (!Number.isFinite(askingPrice) || askingPrice <= 0) return 0;\r\n return askingPrice * grossRate * noiPercentage;\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 strApiResult = null,\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 || PROPERTY_TYPES.MULTIFAMILY).toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n return calculateSTRNOI(askingPrice, strApiResult, {\r\n grossRate: strGrossIncomeMultiplier,\r\n noiPercentage: strNoiPercentage\r\n });\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 * Resolve a listing's canonical NOI and the cap rates derived from it.\r\n *\r\n * NOI is the source of truth; each property type computes it by its own model\r\n * (delegated to calculateNOIByType, unchanged). The active (displayed) cap rate is\r\n * always derived as NOI / price. The reported cap rate is carried through as\r\n * provenance (shown on hover; null => the UI shows \"N/A\"); it only DRIVES the NOI for\r\n * multifamily, where the cap rate is the model input.\r\n *\r\n * NOI precedence: an analyst-confirmed NOI overrides the per-type model; otherwise STR\r\n * uses measured 3rd-party revenue when present and the price-based estimate otherwise;\r\n * assisted uses bedroom count; multifamily uses the reported cap (or estimatedCapRate\r\n * when none was reported).\r\n *\r\n * @param {Object} input\r\n * @param {number|null} [input.bedroomCount] - Bedroom count for assisted living\r\n * @param {number|null} [input.confirmedNOI] - Analyst-confirmed/edited NOI; overrides the per-type model\r\n * @param {number} [input.estimatedCapRate] - Fallback cap (decimal) used for multifamily NOI when none was reported\r\n * @param {number} input.price - Asking price\r\n * @param {string} [input.propertyType] - One of PROPERTY_TYPES\r\n * @param {number|null} [input.reportedCapRate] - Real scraped/listed cap rate as a decimal (e.g. 0.0486); null when none was reported\r\n * @param {{value:number,type:'noi'|'gross'}|null} [input.strApiResult] - Measured STR revenue (3rd-party); null until that backend ships\r\n * @returns {{activeCapRate:(number|null), noi:number, noiSource:string, reportedCapRate:(number|null)}}\r\n */\r\nexport function resolveListingFinancials({\r\n bedroomCount = null,\r\n confirmedNOI = null,\r\n estimatedCapRate,\r\n price,\r\n propertyType = PROPERTY_TYPES.MULTIFAMILY,\r\n reportedCapRate = null,\r\n strApiResult = null,\r\n} = {}) {\r\n const hasReported = reportedCapRate != null && Number.isFinite(reportedCapRate);\r\n const capForNOI = hasReported ? reportedCapRate : estimatedCapRate;\r\n const measured = strApiResult && Number.isFinite(strApiResult.value) && strApiResult.value >= 0 &&\r\n (strApiResult.type === \"noi\" || strApiResult.type === \"gross\");\r\n\r\n let noi;\r\n let noiSource;\r\n if (confirmedNOI != null && Number.isFinite(confirmedNOI) && confirmedNOI >= 0) {\r\n noi = confirmedNOI;\r\n noiSource = \"confirmed\";\r\n } else {\r\n noi = calculateNOIByType(price, capForNOI, propertyType, { bedroomCount, strApiResult });\r\n const type = (propertyType || \"\").toLowerCase();\r\n if (type === PROPERTY_TYPES.STR) {\r\n noiSource = measured ? \"measured\" : \"estimate\";\r\n } else if (type === PROPERTY_TYPES.ASSISTED_LIVING) {\r\n noiSource = \"bedrooms\";\r\n } else {\r\n noiSource = hasReported ? \"cap\" : \"estimate\";\r\n }\r\n }\r\n\r\n if (!Number.isFinite(noi)) noi = 0;\r\n const activeCapRate = Number.isFinite(price) && price > 0 ? noi / price : null;\r\n\r\n return {\r\n activeCapRate,\r\n noi,\r\n noiSource,\r\n reportedCapRate: hasReported ? reportedCapRate : null,\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 = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.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":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","equityPercentFromDebt","price","debtBalance","p","Number","d","isFinite","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","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","calculateSTRNOI","apiResult","grossRate","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","noiPercentage","NOI_PERCENTAGE","value","type","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strApiResult","strGrossIncomeMultiplier","strNoiPercentage","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","resolveListingFinancials","confirmedNOI","estimatedCapRate","reportedCapRate","hasReported","capForNOI","measured","noiSource","activeCapRate","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","fallback","isNaN"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{INTEREST_RATE_TIERS as e,DEFAULT_CAP_RATE as r,determineInterestRateType as n,SELLER_FI_AMORTIZATION as t,SELLER_FI_INTEREST_RATE as c}from"../config/financial.js";import{resolveListingFinancials as i,calculatePriceForCOCR as o,calculateCOCRAtPercent as l,calculateAssignmentFee as s,calculatePMT as u}from"./calculations.js";const a=[{downPercent:60,dscrPercent:70,sellerPercent:40},{downPercent:50,dscrPercent:60,sellerPercent:50},{downPercent:40,dscrPercent:50,sellerPercent:60},{downPercent:30,dscrPercent:40,sellerPercent:70},{downPercent:20,dscrPercent:30,sellerPercent:80}],d=a[a.length-1];function tierAnnualDebtService(e,r,n,i){return 12*u(r*(e.dscrPercent/100),n,i)+12*u(r*(e.sellerPercent/100),c,t)}function sweepTiers(e,r,n,t,c){for(const i of a){const o=e-tierAnnualDebtService(i,r,n,t),l=i.downPercent/100*r;if(o>0&&l>=c)return{cashFlow:o,tier:i}}return null}function calculateEquityCarryScore({bedroomCount:t=null,capRate:c,price:u,propertyType:a="mfr",units:p=null}={}){const m=Number(u);if(!Number.isFinite(m)||m<=0)return null;const P=null==c||""===c?NaN:Number(c),f=Number.isFinite(P)&&P>0?P/100:null,w=Number(p),b=n(a,Number.isFinite(w)?w:void 0),{amortization:y,rate:N}=e[b],{noi:_}=i({bedroomCount:t,estimatedCapRate:r,price:m,propertyType:a,reportedCapRate:f});let F,T,h=sweepTiers(_,m,N,y,0);if(h)F="prospect",T=m;else{const e=.85*m;h=sweepTiers(_,e,N,y,0),h?(F="discount",T=e):(F="dead",T=m)}const C=_-tierAnnualDebtService(d,T,N,y),R=T>0?C/T:0;let S;S="dead"===F?"none":R>.08?"high":R>=.04?"medium":"low";let g=null,v=null,q=null;if(_>0){const e=o(_,.15,{dscrRate:N,dscrTerm:y});Number.isFinite(e)&&e>0&&(g=e,v=(e-m)/m);const r=l(m,_,30,{dscrRate:N,dscrTerm:y});Number.isFinite(r)&&(q=r)}return{assignment:s(T),cap_source:null===f?"estimate":"reported",cash_flow:h?h.cashFlow:null,cocr15_discount:v,cocr15_price:g,cocr30:q,deal_pool:F,downpayment_percent:h?h.tier.downPercent:-1,equity_tier:h?{...h.tier}:null,offer_price:T,raw_yield:R,yield_band:S}}export{a as EQUITY_CARRY_TIERS,calculateEquityCarryScore};
1
+ import{INTEREST_RATE_TIERS as e,DEFAULT_CAP_RATE as r,determineInterestRateType as n,SELLER_FI_AMORTIZATION as t,SELLER_FI_INTEREST_RATE as c}from"../config/financial.js";import{resolveListingFinancials as i,calculatePriceForCOCR as o,calculateCOCRAtPercent as l,calculateAssignmentFee as s,calculatePMT as u}from"./calculations.js";const a=[{downPercent:60,dscrPercent:70,sellerPercent:40},{downPercent:50,dscrPercent:60,sellerPercent:50},{downPercent:40,dscrPercent:50,sellerPercent:60},{downPercent:30,dscrPercent:40,sellerPercent:70},{downPercent:20,dscrPercent:30,sellerPercent:80}],d=a[a.length-1];function tierAnnualDebtService(e,r,n,i){return 12*u(r*(e.dscrPercent/100),n,i)+12*u(r*(e.sellerPercent/100),c,t)}function sweepTiers(e,r,n,t,c){for(const i of a){const o=e-tierAnnualDebtService(i,r,n,t),l=i.downPercent/100*r;if(o>0&&l>=c)return{cashFlow:o,tier:i}}return null}function calculateEquityCarryScore({bedroomCount:t=null,capRate:c,price:u,propertyType:a="mfr",units:p=null}={}){const m=Number(u);if(!Number.isFinite(m)||m<=0)return null;const P=null==c||""===c?NaN:Number(c),f=Number.isFinite(P)&&P>0?P/100:null,w=Number(p),b=a||"mfr",y=n(b,Number.isFinite(w)?w:void 0),{amortization:N,rate:_}=e[y],{noi:F}=i({bedroomCount:t,estimatedCapRate:r,price:m,propertyType:b,reportedCapRate:f});let T,h,C=sweepTiers(F,m,_,N,0);if(C)T="prospect",h=m;else{const e=.85*m;C=sweepTiers(F,e,_,N,0),C?(T="discount",h=e):(T="dead",h=m)}const R=F-tierAnnualDebtService(d,h,_,N),S=h>0?R/h:0;let g;g="dead"===T?"none":S>.08?"high":S>=.04?"medium":"low";let v=null,q=null,A=null;if(F>0){const e=o(F,.15,{dscrRate:_,dscrTerm:N});Number.isFinite(e)&&e>0&&(v=e,q=(e-m)/m);const r=l(m,F,30,{dscrRate:_,dscrTerm:N});Number.isFinite(r)&&(A=r)}return{assignment:s(h),cap_source:null===f?"estimate":"reported",cash_flow:C?C.cashFlow:null,cocr15_discount:q,cocr15_price:v,cocr30:A,deal_pool:T,downpayment_percent:C?C.tier.downPercent:-1,equity_tier:C?{...C.tier}:null,offer_price:h,raw_yield:S,yield_band:g}}export{a as EQUITY_CARRY_TIERS,calculateEquityCarryScore};
2
2
  //# sourceMappingURL=equity-carry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"equity-carry.js","sources":["../../src/financial/equity-carry.js"],"sourcesContent":["// src/financial/equity-carry.js\n\nimport { DEFAULT_CAP_RATE, INTEREST_RATE_TIERS, SELLER_FI_AMORTIZATION, SELLER_FI_INTEREST_RATE, determineInterestRateType } from \"../config/financial.js\";\nimport { calculateAssignmentFee, calculateCOCRAtPercent, calculatePMT, calculatePriceForCOCR, resolveListingFinancials } from \"./calculations.js\";\n\n/**\n * Equity Carry financing tiers (constant 110% leverage: DSCR % + seller carry % = 110).\n * Ordered highest down-payment first so a sweep stops at the strongest qualifying tier:\n * cash flow falls as the down payment rises (more of the stack moves to the market-rate\n * DSCR leg and off the 0% seller note), so the first tier that clears in a 60->20 sweep is\n * the highest down payment that still cash-flows.\n */\nexport const EQUITY_CARRY_TIERS = [\n { downPercent: 60, dscrPercent: 70, sellerPercent: 40 },\n { downPercent: 50, dscrPercent: 60, sellerPercent: 50 },\n { downPercent: 40, dscrPercent: 50, sellerPercent: 60 },\n { downPercent: 30, dscrPercent: 40, sellerPercent: 70 },\n { downPercent: 20, dscrPercent: 30, sellerPercent: 80 },\n];\n\nconst DISCOUNT_BASIS = 0.85;\nconst FIXED_YIELD_TIER = EQUITY_CARRY_TIERS[EQUITY_CARRY_TIERS.length - 1]; // 20% down\nconst YIELD_BAND_HIGH = 0.08;\nconst YIELD_BAND_MEDIUM = 0.04;\n\n/**\n * Annual debt service for one tier at a given price basis, from the suite primitives.\n * DSCR leg amortizes at the property's market rate/term; the seller carry leg is 0% / 30yr.\n * @param {{dscrPercent:number, sellerPercent:number}} tier\n * @param {number} basis - Price the financing is sized against (asking or discounted)\n * @param {number} dscrRate - DSCR annual interest rate (decimal)\n * @param {number} dscrAmortization - DSCR amortization (years)\n * @returns {number} Combined annual debt service (DSCR + seller carry)\n */\nfunction tierAnnualDebtService(tier, basis, dscrRate, dscrAmortization) {\n const dscrAnnual = calculatePMT(basis * (tier.dscrPercent / 100), dscrRate, dscrAmortization) * 12;\n const sellerAnnual = calculatePMT(basis * (tier.sellerPercent / 100), SELLER_FI_INTEREST_RATE, SELLER_FI_AMORTIZATION) * 12;\n return dscrAnnual + sellerAnnual;\n}\n\n/**\n * Sweep the tiers (60 -> 20) at one price basis; return the first (highest down payment)\n * tier whose annual cash flow is positive AND whose down payment covers any known debt.\n * @returns {{tier:object, cashFlow:number}|null} Winning tier + its annual cash flow, or null\n */\nfunction sweepTiers(noi, basis, dscrRate, dscrAmortization, estimatedDebt) {\n for (const tier of EQUITY_CARRY_TIERS) {\n const cashFlow = noi - tierAnnualDebtService(tier, basis, dscrRate, dscrAmortization);\n const downPayment = (tier.downPercent / 100) * basis;\n if (cashFlow > 0 && downPayment >= estimatedDebt) {\n return { cashFlow, tier };\n }\n }\n return null;\n}\n\n/**\n * Score a scraped listing with the Equity Carry Method and route it to a deal pool.\n *\n * Pure, no IO. Accepts inputs in the raw shape Postgres returns: `price` and `capRate` may\n * be NUMERIC strings, and `capRate` is a PERCENT (e.g. \"6.5\", not 0.065) — both are coerced\n * here, so the trap where a clean-decimal fixture passes while production data fails silently\n * cannot occur. A null/non-finite reported cap falls back to DEFAULT_CAP_RATE for multifamily.\n *\n * At score time the property is assumed 100% equity (estimatedDebt = 0), so debt coverage\n * always passes and the routing is driven purely by cash flow; the real debt figure is tested\n * later by the debt drain, which demotes uncovered prospect/discount rows to `shadow`.\n *\n * Run A (routing): sweep 60->20 at the asking price; the first qualifying tier => prospect.\n * If none qualify, re-sweep at 85% of asking; the first qualifying tier => discount. If none\n * qualify in either pass => dead.\n *\n * Run B (comparable yield): independent of Run A, the cash flow at a fixed 20%-down stack on\n * the chosen offer price, divided by that offer price. Banded high/medium/low; dead rows are\n * forced to band `none` (the raw yield is still returned).\n *\n * @param {Object} input\n * @param {number|null} [input.bedroomCount] - Bedroom count (assisted living NOI only)\n * @param {number|string|null} input.capRate - Reported cap rate as a PERCENT; null => estimate\n * @param {number|string} input.price - Asking price (positive); null/0 returns null\n * @param {string} [input.propertyType] - DB property type (mfr/str/assisted/other)\n * @param {number|string|null} [input.units] - Unit count (selects residential vs commercial DSCR tier)\n * @returns {{assignment:number, cap_source:string, cash_flow:(number|null), cocr15_discount:(number|null), cocr15_price:(number|null), cocr30:(number|null), deal_pool:string, downpayment_percent:number, equity_tier:(object|null), offer_price:number, raw_yield:number, yield_band:string}|null}\n * `cap_source` is \"reported\" when a usable (positive) cap was supplied, \"estimate\" when the\n * row was scored on DEFAULT_CAP_RATE because none was reported (or a non-positive cap was) —\n * the UI flags \"estimate\" rows as needs-confirmation and offers a manual cap-rate input that\n * re-scores the row. Most meaningful for cap-driven types (mfr/other); STR/assisted derive NOI\n * from other models. `cocr15_price` is the standard-investor (30% down / 70% DSCR) price that\n * yields a 15% COCR on the row's NOI; `cocr15_discount` is that price as a fraction of asking\n * ((target - price)/price, a decimal); `cocr30` is the actual COCR percent at asking with 30%\n * down. All three are null when NOI <= 0. Null (whole result) when price is not a positive\n * number (caller should skip + log; the SQL gate excludes these).\n */\nexport function calculateEquityCarryScore({\n bedroomCount = null,\n capRate,\n price,\n propertyType = \"mfr\",\n units = null,\n} = {}) {\n const priceNum = Number(price);\n if (!Number.isFinite(priceNum) || priceNum <= 0) return null;\n\n const capPercent = capRate === null || capRate === undefined || capRate === \"\" ? NaN : Number(capRate);\n const reportedCapRate = Number.isFinite(capPercent) && capPercent > 0 ? capPercent / 100 : null;\n\n const unitsNum = Number(units);\n const rateType = determineInterestRateType(propertyType, Number.isFinite(unitsNum) ? unitsNum : undefined);\n const { amortization: dscrAmortization, rate: dscrRate } = INTEREST_RATE_TIERS[rateType];\n\n const { noi } = resolveListingFinancials({\n bedroomCount,\n estimatedCapRate: DEFAULT_CAP_RATE,\n price: priceNum,\n propertyType,\n reportedCapRate,\n });\n\n const estimatedDebt = 0;\n\n let dealPool;\n let offerPrice;\n let winner = sweepTiers(noi, priceNum, dscrRate, dscrAmortization, estimatedDebt);\n if (winner) {\n dealPool = \"prospect\";\n offerPrice = priceNum;\n } else {\n const discountBasis = priceNum * DISCOUNT_BASIS;\n winner = sweepTiers(noi, discountBasis, dscrRate, dscrAmortization, estimatedDebt);\n if (winner) {\n dealPool = \"discount\";\n offerPrice = discountBasis;\n } else {\n dealPool = \"dead\";\n offerPrice = priceNum;\n }\n }\n\n const cashFlow20 = noi - tierAnnualDebtService(FIXED_YIELD_TIER, offerPrice, dscrRate, dscrAmortization);\n const rawYield = offerPrice > 0 ? cashFlow20 / offerPrice : 0;\n\n let yieldBand;\n if (dealPool === \"dead\") {\n yieldBand = \"none\";\n } else if (rawYield > YIELD_BAND_HIGH) {\n yieldBand = \"high\";\n } else if (rawYield >= YIELD_BAND_MEDIUM) {\n yieldBand = \"medium\";\n } else {\n yieldBand = \"low\";\n }\n\n let cocr15Price = null;\n let cocr15Discount = null;\n let cocr30 = null;\n if (noi > 0) {\n const targetPrice = calculatePriceForCOCR(noi, 0.15, { dscrRate, dscrTerm: dscrAmortization });\n if (Number.isFinite(targetPrice) && targetPrice > 0) {\n cocr15Price = targetPrice;\n cocr15Discount = (targetPrice - priceNum) / priceNum;\n }\n const cocrAtThirty = calculateCOCRAtPercent(priceNum, noi, 30, { dscrRate, dscrTerm: dscrAmortization });\n if (Number.isFinite(cocrAtThirty)) cocr30 = cocrAtThirty;\n }\n\n return {\n assignment: calculateAssignmentFee(offerPrice),\n cap_source: reportedCapRate === null ? \"estimate\" : \"reported\",\n cash_flow: winner ? winner.cashFlow : null,\n cocr15_discount: cocr15Discount,\n cocr15_price: cocr15Price,\n cocr30,\n deal_pool: dealPool,\n downpayment_percent: winner ? winner.tier.downPercent : -1,\n equity_tier: winner ? { ...winner.tier } : null,\n offer_price: offerPrice,\n raw_yield: rawYield,\n yield_band: yieldBand,\n };\n}\n"],"names":["EQUITY_CARRY_TIERS","downPercent","dscrPercent","sellerPercent","FIXED_YIELD_TIER","length","tierAnnualDebtService","tier","basis","dscrRate","dscrAmortization","calculatePMT","SELLER_FI_INTEREST_RATE","SELLER_FI_AMORTIZATION","sweepTiers","noi","estimatedDebt","cashFlow","downPayment","calculateEquityCarryScore","bedroomCount","capRate","price","propertyType","units","priceNum","Number","isFinite","capPercent","NaN","reportedCapRate","unitsNum","rateType","determineInterestRateType","undefined","amortization","rate","INTEREST_RATE_TIERS","resolveListingFinancials","estimatedCapRate","DEFAULT_CAP_RATE","dealPool","offerPrice","winner","discountBasis","cashFlow20","rawYield","yieldBand","cocr15Price","cocr15Discount","cocr30","targetPrice","calculatePriceForCOCR","dscrTerm","cocrAtThirty","calculateCOCRAtPercent","assignment","calculateAssignmentFee","cap_source","cash_flow","cocr15_discount","cocr15_price","deal_pool","downpayment_percent","equity_tier","offer_price","raw_yield","yield_band"],"mappings":"6UAYY,MAACA,EAAqB,CAChC,CAAEC,YAAa,GAAIC,YAAa,GAAIC,cAAe,IACnD,CAAEF,YAAa,GAAIC,YAAa,GAAIC,cAAe,IACnD,CAAEF,YAAa,GAAIC,YAAa,GAAIC,cAAe,IACnD,CAAEF,YAAa,GAAIC,YAAa,GAAIC,cAAe,IACnD,CAAEF,YAAa,GAAIC,YAAa,GAAIC,cAAe,KAI/CC,EAAmBJ,EAAmBA,EAAmBK,OAAS,GAaxE,SAASC,sBAAsBC,EAAMC,EAAOC,EAAUC,GAGpD,OAFgG,GAA7EC,EAAaH,GAASD,EAAKL,YAAc,KAAMO,EAAUC,GAC6C,GAApGC,EAAaH,GAASD,EAAKJ,cAAgB,KAAMS,EAAyBC,EAEjG,CAOA,SAASC,WAAWC,EAAKP,EAAOC,EAAUC,EAAkBM,GAC1D,IAAK,MAAMT,KAAQP,EAAoB,CACrC,MAAMiB,EAAWF,EAAMT,sBAAsBC,EAAMC,EAAOC,EAAUC,GAC9DQ,EAAeX,EAAKN,YAAc,IAAOO,EAC/C,GAAIS,EAAW,GAAKC,GAAeF,EACjC,MAAO,CAAEC,WAAUV,OAEvB,CACA,OAAO,IACT,CAuCO,SAASY,2BAA0BC,aACxCA,EAAe,KAAIC,QACnBA,EAAOC,MACPA,EAAKC,aACLA,EAAe,MAAKC,MACpBA,EAAQ,MACN,IACF,MAAMC,EAAWC,OAAOJ,GACxB,IAAKI,OAAOC,SAASF,IAAaA,GAAY,EAAG,OAAO,KAExD,MAAMG,EAAaP,SAAyD,KAAZA,EAAiBQ,IAAMH,OAAOL,GACxFS,EAAkBJ,OAAOC,SAASC,IAAeA,EAAa,EAAIA,EAAa,IAAM,KAErFG,EAAWL,OAAOF,GAClBQ,EAAWC,EAA0BV,EAAcG,OAAOC,SAASI,GAAYA,OAAWG,IACxFC,aAAczB,EAAkB0B,KAAM3B,GAAa4B,EAAoBL,IAEzEjB,IAAEA,GAAQuB,EAAyB,CACvClB,eACAmB,iBAAkBC,EAClBlB,MAAOG,EACPF,eACAO,oBAKF,IAAIW,EACAC,EACAC,EAAS7B,WAAWC,EAAKU,EAAUhB,EAAUC,EAJ3B,GAKtB,GAAIiC,EACFF,EAAW,WACXC,EAAajB,MACR,CACL,MAAMmB,EA3Ga,IA2GGnB,EACtBkB,EAAS7B,WAAWC,EAAK6B,EAAenC,EAAUC,EAV9B,GAWhBiC,GACFF,EAAW,WACXC,EAAaE,IAEbH,EAAW,OACXC,EAAajB,EAEjB,CAEA,MAAMoB,EAAa9B,EAAMT,sBAAsBF,EAAkBsC,EAAYjC,EAAUC,GACjFoC,EAAWJ,EAAa,EAAIG,EAAaH,EAAa,EAE5D,IAAIK,EAEFA,EADe,SAAbN,EACU,OACHK,EA1HW,IA2HR,OACHA,GA3Ha,IA4HV,SAEA,MAGd,IAAIE,EAAc,KACdC,EAAiB,KACjBC,EAAS,KACb,GAAInC,EAAM,EAAG,CACX,MAAMoC,EAAcC,EAAsBrC,EAAK,IAAM,CAAEN,WAAU4C,SAAU3C,IACvEgB,OAAOC,SAASwB,IAAgBA,EAAc,IAChDH,EAAcG,EACdF,GAAkBE,EAAc1B,GAAYA,GAE9C,MAAM6B,EAAeC,EAAuB9B,EAAUV,EAAK,GAAI,CAAEN,WAAU4C,SAAU3C,IACjFgB,OAAOC,SAAS2B,KAAeJ,EAASI,EAC9C,CAEA,MAAO,CACLE,WAAYC,EAAuBf,GACnCgB,WAAgC,OAApB5B,EAA2B,WAAa,WACpD6B,UAAWhB,EAASA,EAAO1B,SAAW,KACtC2C,gBAAiBX,EACjBY,aAAcb,EACdE,SACAY,UAAWrB,EACXsB,oBAAqBpB,EAASA,EAAOpC,KAAKN,aAAc,EACxD+D,YAAarB,EAAS,IAAKA,EAAOpC,MAAS,KAC3C0D,YAAavB,EACbwB,UAAWpB,EACXqB,WAAYpB,EAEhB"}
1
+ {"version":3,"file":"equity-carry.js","sources":["../../src/financial/equity-carry.js"],"sourcesContent":["// src/financial/equity-carry.js\n\nimport { DEFAULT_CAP_RATE, INTEREST_RATE_TIERS, SELLER_FI_AMORTIZATION, SELLER_FI_INTEREST_RATE, determineInterestRateType } from \"../config/financial.js\";\nimport { calculateAssignmentFee, calculateCOCRAtPercent, calculatePMT, calculatePriceForCOCR, resolveListingFinancials } from \"./calculations.js\";\n\n/**\n * Equity Carry financing tiers (constant 110% leverage: DSCR % + seller carry % = 110).\n * Ordered highest down-payment first so a sweep stops at the strongest qualifying tier:\n * cash flow falls as the down payment rises (more of the stack moves to the market-rate\n * DSCR leg and off the 0% seller note), so the first tier that clears in a 60->20 sweep is\n * the highest down payment that still cash-flows.\n */\nexport const EQUITY_CARRY_TIERS = [\n { downPercent: 60, dscrPercent: 70, sellerPercent: 40 },\n { downPercent: 50, dscrPercent: 60, sellerPercent: 50 },\n { downPercent: 40, dscrPercent: 50, sellerPercent: 60 },\n { downPercent: 30, dscrPercent: 40, sellerPercent: 70 },\n { downPercent: 20, dscrPercent: 30, sellerPercent: 80 },\n];\n\nconst DISCOUNT_BASIS = 0.85;\nconst FIXED_YIELD_TIER = EQUITY_CARRY_TIERS[EQUITY_CARRY_TIERS.length - 1]; // 20% down\nconst YIELD_BAND_HIGH = 0.08;\nconst YIELD_BAND_MEDIUM = 0.04;\n\n/**\n * Annual debt service for one tier at a given price basis, from the suite primitives.\n * DSCR leg amortizes at the property's market rate/term; the seller carry leg is 0% / 30yr.\n * @param {{dscrPercent:number, sellerPercent:number}} tier\n * @param {number} basis - Price the financing is sized against (asking or discounted)\n * @param {number} dscrRate - DSCR annual interest rate (decimal)\n * @param {number} dscrAmortization - DSCR amortization (years)\n * @returns {number} Combined annual debt service (DSCR + seller carry)\n */\nfunction tierAnnualDebtService(tier, basis, dscrRate, dscrAmortization) {\n const dscrAnnual = calculatePMT(basis * (tier.dscrPercent / 100), dscrRate, dscrAmortization) * 12;\n const sellerAnnual = calculatePMT(basis * (tier.sellerPercent / 100), SELLER_FI_INTEREST_RATE, SELLER_FI_AMORTIZATION) * 12;\n return dscrAnnual + sellerAnnual;\n}\n\n/**\n * Sweep the tiers (60 -> 20) at one price basis; return the first (highest down payment)\n * tier whose annual cash flow is positive AND whose down payment covers any known debt.\n * @returns {{tier:object, cashFlow:number}|null} Winning tier + its annual cash flow, or null\n */\nfunction sweepTiers(noi, basis, dscrRate, dscrAmortization, estimatedDebt) {\n for (const tier of EQUITY_CARRY_TIERS) {\n const cashFlow = noi - tierAnnualDebtService(tier, basis, dscrRate, dscrAmortization);\n const downPayment = (tier.downPercent / 100) * basis;\n if (cashFlow > 0 && downPayment >= estimatedDebt) {\n return { cashFlow, tier };\n }\n }\n return null;\n}\n\n/**\n * Score a scraped listing with the Equity Carry Method and route it to a deal pool.\n *\n * Pure, no IO. Accepts inputs in the raw shape Postgres returns: `price` and `capRate` may\n * be NUMERIC strings, and `capRate` is a PERCENT (e.g. \"6.5\", not 0.065) — both are coerced\n * here, so the trap where a clean-decimal fixture passes while production data fails silently\n * cannot occur. A null/non-finite reported cap falls back to DEFAULT_CAP_RATE for multifamily.\n *\n * At score time the property is assumed 100% equity (estimatedDebt = 0), so debt coverage\n * always passes and the routing is driven purely by cash flow; the real debt figure is tested\n * later by the debt drain, which demotes uncovered prospect/discount rows to `shadow`.\n *\n * Run A (routing): sweep 60->20 at the asking price; the first qualifying tier => prospect.\n * If none qualify, re-sweep at 85% of asking; the first qualifying tier => discount. If none\n * qualify in either pass => dead.\n *\n * Run B (comparable yield): independent of Run A, the cash flow at a fixed 20%-down stack on\n * the chosen offer price, divided by that offer price. Banded high/medium/low; dead rows are\n * forced to band `none` (the raw yield is still returned).\n *\n * @param {Object} input\n * @param {number|null} [input.bedroomCount] - Bedroom count (assisted living NOI only)\n * @param {number|string|null} input.capRate - Reported cap rate as a PERCENT; null => estimate\n * @param {number|string} input.price - Asking price (positive); null/0 returns null\n * @param {string} [input.propertyType] - DB property type (mfr/str/assisted/other)\n * @param {number|string|null} [input.units] - Unit count (selects residential vs commercial DSCR tier)\n * @returns {{assignment:number, cap_source:string, cash_flow:(number|null), cocr15_discount:(number|null), cocr15_price:(number|null), cocr30:(number|null), deal_pool:string, downpayment_percent:number, equity_tier:(object|null), offer_price:number, raw_yield:number, yield_band:string}|null}\n * `cap_source` is \"reported\" when a usable (positive) cap was supplied, \"estimate\" when the\n * row was scored on DEFAULT_CAP_RATE because none was reported (or a non-positive cap was) —\n * the UI flags \"estimate\" rows as needs-confirmation and offers a manual cap-rate input that\n * re-scores the row. Most meaningful for cap-driven types (mfr/other); STR/assisted derive NOI\n * from other models. `cocr15_price` is the standard-investor (30% down / 70% DSCR) price that\n * yields a 15% COCR on the row's NOI; `cocr15_discount` is that price as a fraction of asking\n * ((target - price)/price, a decimal); `cocr30` is the actual COCR percent at asking with 30%\n * down. All three are null when NOI <= 0. Null (whole result) when price is not a positive\n * number (caller should skip + log; the SQL gate excludes these).\n */\nexport function calculateEquityCarryScore({\n bedroomCount = null,\n capRate,\n price,\n propertyType = \"mfr\",\n units = null,\n} = {}) {\n const priceNum = Number(price);\n if (!Number.isFinite(priceNum) || priceNum <= 0) return null;\n\n const capPercent = capRate === null || capRate === undefined || capRate === \"\" ? NaN : Number(capRate);\n const reportedCapRate = Number.isFinite(capPercent) && capPercent > 0 ? capPercent / 100 : null;\n\n const unitsNum = Number(units);\n const resolvedType = propertyType || \"mfr\";\n const rateType = determineInterestRateType(resolvedType, Number.isFinite(unitsNum) ? unitsNum : undefined);\n const { amortization: dscrAmortization, rate: dscrRate } = INTEREST_RATE_TIERS[rateType];\n\n const { noi } = resolveListingFinancials({\n bedroomCount,\n estimatedCapRate: DEFAULT_CAP_RATE,\n price: priceNum,\n propertyType: resolvedType,\n reportedCapRate,\n });\n\n const estimatedDebt = 0;\n\n let dealPool;\n let offerPrice;\n let winner = sweepTiers(noi, priceNum, dscrRate, dscrAmortization, estimatedDebt);\n if (winner) {\n dealPool = \"prospect\";\n offerPrice = priceNum;\n } else {\n const discountBasis = priceNum * DISCOUNT_BASIS;\n winner = sweepTiers(noi, discountBasis, dscrRate, dscrAmortization, estimatedDebt);\n if (winner) {\n dealPool = \"discount\";\n offerPrice = discountBasis;\n } else {\n dealPool = \"dead\";\n offerPrice = priceNum;\n }\n }\n\n const cashFlow20 = noi - tierAnnualDebtService(FIXED_YIELD_TIER, offerPrice, dscrRate, dscrAmortization);\n const rawYield = offerPrice > 0 ? cashFlow20 / offerPrice : 0;\n\n let yieldBand;\n if (dealPool === \"dead\") {\n yieldBand = \"none\";\n } else if (rawYield > YIELD_BAND_HIGH) {\n yieldBand = \"high\";\n } else if (rawYield >= YIELD_BAND_MEDIUM) {\n yieldBand = \"medium\";\n } else {\n yieldBand = \"low\";\n }\n\n let cocr15Price = null;\n let cocr15Discount = null;\n let cocr30 = null;\n if (noi > 0) {\n const targetPrice = calculatePriceForCOCR(noi, 0.15, { dscrRate, dscrTerm: dscrAmortization });\n if (Number.isFinite(targetPrice) && targetPrice > 0) {\n cocr15Price = targetPrice;\n cocr15Discount = (targetPrice - priceNum) / priceNum;\n }\n const cocrAtThirty = calculateCOCRAtPercent(priceNum, noi, 30, { dscrRate, dscrTerm: dscrAmortization });\n if (Number.isFinite(cocrAtThirty)) cocr30 = cocrAtThirty;\n }\n\n return {\n assignment: calculateAssignmentFee(offerPrice),\n cap_source: reportedCapRate === null ? \"estimate\" : \"reported\",\n cash_flow: winner ? winner.cashFlow : null,\n cocr15_discount: cocr15Discount,\n cocr15_price: cocr15Price,\n cocr30,\n deal_pool: dealPool,\n downpayment_percent: winner ? winner.tier.downPercent : -1,\n equity_tier: winner ? { ...winner.tier } : null,\n offer_price: offerPrice,\n raw_yield: rawYield,\n yield_band: yieldBand,\n };\n}\n"],"names":["EQUITY_CARRY_TIERS","downPercent","dscrPercent","sellerPercent","FIXED_YIELD_TIER","length","tierAnnualDebtService","tier","basis","dscrRate","dscrAmortization","calculatePMT","SELLER_FI_INTEREST_RATE","SELLER_FI_AMORTIZATION","sweepTiers","noi","estimatedDebt","cashFlow","downPayment","calculateEquityCarryScore","bedroomCount","capRate","price","propertyType","units","priceNum","Number","isFinite","capPercent","NaN","reportedCapRate","unitsNum","resolvedType","rateType","determineInterestRateType","undefined","amortization","rate","INTEREST_RATE_TIERS","resolveListingFinancials","estimatedCapRate","DEFAULT_CAP_RATE","dealPool","offerPrice","winner","discountBasis","cashFlow20","rawYield","yieldBand","cocr15Price","cocr15Discount","cocr30","targetPrice","calculatePriceForCOCR","dscrTerm","cocrAtThirty","calculateCOCRAtPercent","assignment","calculateAssignmentFee","cap_source","cash_flow","cocr15_discount","cocr15_price","deal_pool","downpayment_percent","equity_tier","offer_price","raw_yield","yield_band"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "7.25.0",
3
+ "version": "7.26.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",