@archerjessop/utilities 7.29.1 → 7.31.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{getEnvVar as E}from"../environment/utilities.js";const _={ASSIGNMENT_FEE_PERCENTAGE:.05,CASH_OFFER_ASSIGNMENT_PERCENTAGE:.07,CASH_OFFER_ROUNDING:1e4,NET_TO_BUYER_PERCENTAGE:.1,CLOSING_COSTS_PERCENTAGE:.0125,HARD_MONEY_RATE:.03,REHAB_RATE:0,SELLER_AGENT_COMMISSION:.025,BUYER_AGENT_COMMISSION:.025,MINIMUM_COCR15_PRICE:1e4,MAX_COCR15_PRICE_MULTIPLIER:50,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:20,MAX_ITERATIONS:50,CALCULATION_TOLERANCE:.001,ADJUSTMENT_FACTOR:.5,EXPORT_URL_BASE:E("EXPORT_URL_BASE","https://app.archerjessop.com/property-dashboard/import"),EXCLUDED_EXPORT_VALUES:["Loading...","Not found","",null,void 0],DASHBOARD_URL_BASE:E("DASHBOARD_URL_BASE","https://app.archerjessop.com/property-dashboard/")},{ASSIGNMENT_FEE_PERCENTAGE:R,CASH_OFFER_ASSIGNMENT_PERCENTAGE:A,CASH_OFFER_ROUNDING:C,NET_TO_BUYER_PERCENTAGE:N,CLOSING_COSTS_PERCENTAGE:T,HARD_MONEY_RATE:I,SELLER_AGENT_COMMISSION:O,BUYER_AGENT_COMMISSION:S,MINIMUM_COCR15_PRICE:M,MAX_COCR15_PRICE_MULTIPLIER:L,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:P,REHAB_RATE:G}=_;export{R as ASSIGNMENT_FEE_PERCENTAGE,_ as BUSINESS_CONSTANTS,S as BUYER_AGENT_COMMISSION,A as CASH_OFFER_ASSIGNMENT_PERCENTAGE,C as CASH_OFFER_ROUNDING,T as CLOSING_COSTS_PERCENTAGE,P as CONSERVATIVE_COCR15_PRICE_MULTIPLIER,I as HARD_MONEY_RATE,L as MAX_COCR15_PRICE_MULTIPLIER,M as MINIMUM_COCR15_PRICE,N as NET_TO_BUYER_PERCENTAGE,G as REHAB_RATE,O as SELLER_AGENT_COMMISSION};
1
+ import{getEnvVar as E}from"../environment/utilities.js";const _={ASSIGNMENT_FEE_PERCENTAGE:.05,CASH_OFFER_ASSIGNMENT_PERCENTAGE:.07,CASH_OFFER_ROUNDING:1e4,NET_TO_BUYER_PERCENTAGE:.1,SELLER_FINANCE_MAX_DOWN_PERCENT:9.2,CLOSING_COSTS_PERCENTAGE:.0125,HARD_MONEY_RATE:.03,REHAB_RATE:0,SELLER_AGENT_COMMISSION:.025,BUYER_AGENT_COMMISSION:.025,MINIMUM_COCR15_PRICE:1e4,MAX_COCR15_PRICE_MULTIPLIER:50,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:20,MAX_ITERATIONS:50,CALCULATION_TOLERANCE:.001,ADJUSTMENT_FACTOR:.5,EXPORT_URL_BASE:E("EXPORT_URL_BASE","https://app.archerjessop.com/property-dashboard/import"),EXCLUDED_EXPORT_VALUES:["Loading...","Not found","",null,void 0],DASHBOARD_URL_BASE:E("DASHBOARD_URL_BASE","https://app.archerjessop.com/property-dashboard/")},{ASSIGNMENT_FEE_PERCENTAGE:R,CASH_OFFER_ASSIGNMENT_PERCENTAGE:A,CASH_OFFER_ROUNDING:N,NET_TO_BUYER_PERCENTAGE:C,CLOSING_COSTS_PERCENTAGE:T,HARD_MONEY_RATE:I,SELLER_FINANCE_MAX_DOWN_PERCENT:O,SELLER_AGENT_COMMISSION:S,BUYER_AGENT_COMMISSION:M,MINIMUM_COCR15_PRICE:L,MAX_COCR15_PRICE_MULTIPLIER:P,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:G,REHAB_RATE:U}=_;export{R as ASSIGNMENT_FEE_PERCENTAGE,_ as BUSINESS_CONSTANTS,M as BUYER_AGENT_COMMISSION,A as CASH_OFFER_ASSIGNMENT_PERCENTAGE,N as CASH_OFFER_ROUNDING,T as CLOSING_COSTS_PERCENTAGE,G as CONSERVATIVE_COCR15_PRICE_MULTIPLIER,I as HARD_MONEY_RATE,P as MAX_COCR15_PRICE_MULTIPLIER,L as MINIMUM_COCR15_PRICE,C as NET_TO_BUYER_PERCENTAGE,U as REHAB_RATE,S as SELLER_AGENT_COMMISSION,O as SELLER_FINANCE_MAX_DOWN_PERCENT};
2
2
  //# sourceMappingURL=business.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"business.js","sources":["../../src/config/business.js"],"sourcesContent":["/**\r\n * Business logic and fee constants\r\n */\r\n\r\nimport { getEnvVar } from \"../environment/utilities.js\";\r\n\r\nexport const BUSINESS_CONSTANTS = {\r\n // Assignment and transaction fees\r\n ASSIGNMENT_FEE_PERCENTAGE: 0.05, // 5% assignment fee\r\n CASH_OFFER_ASSIGNMENT_PERCENTAGE: 0.07, // 7% haircut off the 15%-COCR price for a cash offer (assignment + costs)\r\n CASH_OFFER_ROUNDING: 10000, // cash offer price floors DOWN to this step (stays at/below the 15%-COCR max threshold)\r\n NET_TO_BUYER_PERCENTAGE: 0.10, // 10% net to buyer\r\n CLOSING_COSTS_PERCENTAGE: 0.0125, // 1.25% closing costs\r\n HARD_MONEY_RATE: 0.03, // 3% financing fee\r\n REHAB_RATE: 0.0, // 0% rehab fee\r\n \r\n // Agent commissions\r\n SELLER_AGENT_COMMISSION: 0.025, // 3% seller agent commission\r\n BUYER_AGENT_COMMISSION: 0.025, // 3% buyer agent commission\r\n \r\n // COCR15 price calculation limits\r\n MINIMUM_COCR15_PRICE: 10000, // $10,000 minimum\r\n MAX_COCR15_PRICE_MULTIPLIER: 50, // Maximum price as multiple of NOI\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER: 20, // Conservative price limit\r\n \r\n // Algorithm parameters (duplicated for business logic context)\r\n\r\n \r\n // Calculation parameters\r\n MAX_ITERATIONS: 50, // Maximum iterations for iterative calculations\r\n CALCULATION_TOLERANCE: 0.001, // Tolerance for convergence\r\n ADJUSTMENT_FACTOR: 0.5, // Adjustment factor for iterations\r\n\r\n\r\n \r\n // URL constants - safe for both Node.js and browser\r\n EXPORT_URL_BASE: getEnvVar(\r\n \"EXPORT_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/import\"\r\n ),\r\n EXCLUDED_EXPORT_VALUES: [\"Loading...\", \"Not found\", \"\", null, undefined],\r\n DASHBOARD_URL_BASE: getEnvVar(\r\n \"DASHBOARD_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/\"\r\n ),\r\n \r\n};\r\n\r\n// Convenience exports for commonly used values\r\nexport const {\r\n ASSIGNMENT_FEE_PERCENTAGE,\r\n CASH_OFFER_ASSIGNMENT_PERCENTAGE,\r\n CASH_OFFER_ROUNDING,\r\n NET_TO_BUYER_PERCENTAGE,\r\n CLOSING_COSTS_PERCENTAGE,\r\n HARD_MONEY_RATE,\r\n SELLER_AGENT_COMMISSION,\r\n BUYER_AGENT_COMMISSION,\r\n MINIMUM_COCR15_PRICE,\r\n MAX_COCR15_PRICE_MULTIPLIER,\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER,\r\n REHAB_RATE,\r\n} = BUSINESS_CONSTANTS;\r\n"],"names":["BUSINESS_CONSTANTS","ASSIGNMENT_FEE_PERCENTAGE","CASH_OFFER_ASSIGNMENT_PERCENTAGE","CASH_OFFER_ROUNDING","NET_TO_BUYER_PERCENTAGE","CLOSING_COSTS_PERCENTAGE","HARD_MONEY_RATE","REHAB_RATE","SELLER_AGENT_COMMISSION","BUYER_AGENT_COMMISSION","MINIMUM_COCR15_PRICE","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MAX_ITERATIONS","CALCULATION_TOLERANCE","ADJUSTMENT_FACTOR","EXPORT_URL_BASE","getEnvVar","EXCLUDED_EXPORT_VALUES","undefined","DASHBOARD_URL_BASE"],"mappings":"wDAMY,MAACA,EAAqB,CAEhCC,0BAA2B,IAC3BC,iCAAkC,IAClCC,oBAAqB,IACrBC,wBAAyB,GACzBC,yBAA0B,MAC1BC,gBAAiB,IACjBC,WAAY,EAGZC,wBAAyB,KACzBC,uBAAwB,KAGxBC,qBAAsB,IACtBC,4BAA6B,GAC7BC,qCAAsC,GAMtCC,eAAgB,GAChBC,sBAAuB,KACvBC,kBAAmB,GAKnBC,gBAAiBC,EACf,kBACA,0DAEFC,uBAAwB,CAAC,aAAc,YAAa,GAAI,UAAMC,GAC9DC,mBAAoBH,EAClB,qBACA,sDAMShB,0BACXA,EAAyBC,iCACzBA,EAAgCC,oBAChCA,EAAmBC,wBACnBA,EAAuBC,yBACvBA,EAAwBC,gBACxBA,EAAeE,wBACfA,EAAuBC,uBACvBA,EAAsBC,qBACtBA,EAAoBC,4BACpBA,EAA2BC,qCAC3BA,EAAoCL,WACpCA,GACEP"}
1
+ {"version":3,"file":"business.js","sources":["../../src/config/business.js"],"sourcesContent":["/**\r\n * Business logic and fee constants\r\n */\r\n\r\nimport { getEnvVar } from \"../environment/utilities.js\";\r\n\r\nexport const BUSINESS_CONSTANTS = {\r\n // Assignment and transaction fees\r\n ASSIGNMENT_FEE_PERCENTAGE: 0.05, // 5% assignment fee\r\n CASH_OFFER_ASSIGNMENT_PERCENTAGE: 0.07, // 7% haircut off the 15%-COCR price for a cash offer (assignment + costs)\r\n CASH_OFFER_ROUNDING: 10000, // cash offer price floors DOWN to this step (stays at/below the 15%-COCR max threshold)\r\n NET_TO_BUYER_PERCENTAGE: 0.10, // 10% net to buyer\r\n SELLER_FINANCE_MAX_DOWN_PERCENT: 9.2, // ceiling on the down payment for a Standard Seller Financing offer (whole percent); down = min(down-for-15%-COCR, this)\r\n CLOSING_COSTS_PERCENTAGE: 0.0125, // 1.25% closing costs\r\n HARD_MONEY_RATE: 0.03, // 3% financing fee\r\n REHAB_RATE: 0.0, // 0% rehab fee\r\n \r\n // Agent commissions\r\n SELLER_AGENT_COMMISSION: 0.025, // 3% seller agent commission\r\n BUYER_AGENT_COMMISSION: 0.025, // 3% buyer agent commission\r\n \r\n // COCR15 price calculation limits\r\n MINIMUM_COCR15_PRICE: 10000, // $10,000 minimum\r\n MAX_COCR15_PRICE_MULTIPLIER: 50, // Maximum price as multiple of NOI\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER: 20, // Conservative price limit\r\n \r\n // Algorithm parameters (duplicated for business logic context)\r\n\r\n \r\n // Calculation parameters\r\n MAX_ITERATIONS: 50, // Maximum iterations for iterative calculations\r\n CALCULATION_TOLERANCE: 0.001, // Tolerance for convergence\r\n ADJUSTMENT_FACTOR: 0.5, // Adjustment factor for iterations\r\n\r\n\r\n \r\n // URL constants - safe for both Node.js and browser\r\n EXPORT_URL_BASE: getEnvVar(\r\n \"EXPORT_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/import\"\r\n ),\r\n EXCLUDED_EXPORT_VALUES: [\"Loading...\", \"Not found\", \"\", null, undefined],\r\n DASHBOARD_URL_BASE: getEnvVar(\r\n \"DASHBOARD_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/\"\r\n ),\r\n \r\n};\r\n\r\n// Convenience exports for commonly used values\r\nexport const {\r\n ASSIGNMENT_FEE_PERCENTAGE,\r\n CASH_OFFER_ASSIGNMENT_PERCENTAGE,\r\n CASH_OFFER_ROUNDING,\r\n NET_TO_BUYER_PERCENTAGE,\r\n CLOSING_COSTS_PERCENTAGE,\r\n HARD_MONEY_RATE,\r\n SELLER_FINANCE_MAX_DOWN_PERCENT,\r\n SELLER_AGENT_COMMISSION,\r\n BUYER_AGENT_COMMISSION,\r\n MINIMUM_COCR15_PRICE,\r\n MAX_COCR15_PRICE_MULTIPLIER,\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER,\r\n REHAB_RATE,\r\n} = BUSINESS_CONSTANTS;\r\n"],"names":["BUSINESS_CONSTANTS","ASSIGNMENT_FEE_PERCENTAGE","CASH_OFFER_ASSIGNMENT_PERCENTAGE","CASH_OFFER_ROUNDING","NET_TO_BUYER_PERCENTAGE","SELLER_FINANCE_MAX_DOWN_PERCENT","CLOSING_COSTS_PERCENTAGE","HARD_MONEY_RATE","REHAB_RATE","SELLER_AGENT_COMMISSION","BUYER_AGENT_COMMISSION","MINIMUM_COCR15_PRICE","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MAX_ITERATIONS","CALCULATION_TOLERANCE","ADJUSTMENT_FACTOR","EXPORT_URL_BASE","getEnvVar","EXCLUDED_EXPORT_VALUES","undefined","DASHBOARD_URL_BASE"],"mappings":"wDAMY,MAACA,EAAqB,CAEhCC,0BAA2B,IAC3BC,iCAAkC,IAClCC,oBAAqB,IACrBC,wBAAyB,GACzBC,gCAAiC,IACjCC,yBAA0B,MAC1BC,gBAAiB,IACjBC,WAAY,EAGZC,wBAAyB,KACzBC,uBAAwB,KAGxBC,qBAAsB,IACtBC,4BAA6B,GAC7BC,qCAAsC,GAMtCC,eAAgB,GAChBC,sBAAuB,KACvBC,kBAAmB,GAKnBC,gBAAiBC,EACf,kBACA,0DAEFC,uBAAwB,CAAC,aAAc,YAAa,GAAI,UAAMC,GAC9DC,mBAAoBH,EAClB,qBACA,sDAMSjB,0BACXA,EAAyBC,iCACzBA,EAAgCC,oBAChCA,EAAmBC,wBACnBA,EAAuBE,yBACvBA,EAAwBC,gBACxBA,EAAeF,gCACfA,EAA+BI,wBAC/BA,EAAuBC,uBACvBA,EAAsBC,qBACtBA,EAAoBC,4BACpBA,EAA2BC,qCAC3BA,EAAoCL,WACpCA,GACER"}
@@ -0,0 +1,2 @@
1
+ const e={EBITDA_ADVANCE_RATE:.2,EBITDA_ADVANCE_THRESHOLD:1e6,FF_E_ADVANCE_RATE:.3,INVENTORY_ADVANCE_RATE:.2,OFFER_MULTIPLE_HIGH:3,OFFER_MULTIPLE_LOW:2,REAL_ESTATE_ADVANCE_RATE:.5};function toNumber(e){if(null==e||""===e)return null;const n="number"==typeof e?e:parseFloat(String(e).replace(/[$,\s]/g,""));return Number.isFinite(n)?n:null}function toNonNegative(e){const n=toNumber(e);return null===n||n<0?null:n}function resolveBusinessEarnings(e={}){const n=toNumber(e.ebitda);if(null!==n)return{earnings:n,source:"ebitda"};const t=toNumber(e.sde);if(null!==t)return{earnings:t,source:"sde"};const l=toNumber(e.cashFlow??e.cash_flow);return null!==l?{earnings:l,source:"cash_flow"}:{earnings:null,source:null}}function shouldIncludeRealEstate(e={}){if("boolean"==typeof e.statedInclusion)return e.statedInclusion;const n=toNumber(e.askingPrice),t=toNumber(e.realEstateValue);return!(null!==n&&n>0&&null!==t&&t>=n)}function calculateBusinessDownPayment(n={}){const{EBITDA_ADVANCE_RATE:t,EBITDA_ADVANCE_THRESHOLD:l,FF_E_ADVANCE_RATE:s,INVENTORY_ADVANCE_RATE:a,REAL_ESTATE_ADVANCE_RATE:u}=e,r=toNonNegative(n.realEstateValue)??0,o=toNonNegative(n.ffeValue)??0,i=toNonNegative(n.inventoryValue)??0,c=toNumber(n.ebitda)??0,f={ebitda:c>=l?c*t:0,ffe:o*s,inventory:i*a,realEstate:r*u};return{downPayment:f.realEstate+f.ffe+f.inventory+f.ebitda,legs:f}}function calculateBusinessOffer(n={}){const{OFFER_MULTIPLE_HIGH:t,OFFER_MULTIPLE_LOW:l}=e,{includeFfe:s=!0,includeInventory:a=!0,includeRealEstate:u=!0}=n,r=(u?toNonNegative(n.realEstateValue)??0:0)+(s?toNonNegative(n.ffeValue)??0:0)+(a?toNonNegative(n.inventoryValue)??0:0),o=toNumber(n.earnings);return null===o?{assetsIncluded:r,offerHigh:null,offerLow:null}:{assetsIncluded:r,offerHigh:o*t+r,offerLow:o*l+r}}function underwriteBusinessListing(e={}){const n=e.realEstateValue??e.real_estate_value,t=e.ffeValue??e.ff_e_value,l=e.inventoryValue??e.inventory_value,s=e.ebitda,{earnings:a,source:u}=resolveBusinessEarnings({cashFlow:e.cashFlow??e.cash_flow,ebitda:s,sde:e.sde}),{downPayment:r,legs:o}=calculateBusinessDownPayment({ebitda:s,ffeValue:t,inventoryValue:l,realEstateValue:n}),{assetsIncluded:i,offerHigh:c,offerLow:f}=calculateBusinessOffer({earnings:a,ffeValue:t,includeFfe:e.includeFfe??e.include_ff_e??!0,includeInventory:e.includeInventory??e.include_inventory??!0,includeRealEstate:e.includeRealEstate??e.include_real_estate??!0,inventoryValue:l,realEstateValue:n});return{assetsIncluded:i,collateralShortfall:null!==c&&r>c,downPayment:r,earnings:a,earningsSource:e.earningsSource??e.earnings_source??u,legs:o,offerHigh:c,offerLow:f,sellerCarry:null===c?null:c-r}}export{e as BUSINESS_UNDERWRITING_CONSTANTS,calculateBusinessDownPayment,calculateBusinessOffer,resolveBusinessEarnings,shouldIncludeRealEstate,underwriteBusinessListing};
2
+ //# sourceMappingURL=business-underwriting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"business-underwriting.js","sources":["../../src/financial/business-underwriting.js"],"sourcesContent":["// src/financial/business-underwriting.js\n//\n// Underwriting for businesses backed by real estate (the BizBuySell pipeline).\n// The real-estate model prices a property off NOI and a cap rate; this one\n// prices a business off an earnings multiple and adds the tangible assets that\n// convey with it. Shared by the BizBuySell extension panel and the dashboard so\n// both quote the same numbers from the same inputs.\n\n/**\n * Collateral advance rates for the down payment, and the earnings multiples\n * bounding the offer. The EBITDA leg only contributes at or above its threshold\n * — smaller earnings are not treated as collateral.\n */\nexport const BUSINESS_UNDERWRITING_CONSTANTS = {\n EBITDA_ADVANCE_RATE: 0.20,\n EBITDA_ADVANCE_THRESHOLD: 1000000,\n FF_E_ADVANCE_RATE: 0.30,\n INVENTORY_ADVANCE_RATE: 0.20,\n OFFER_MULTIPLE_HIGH: 3,\n OFFER_MULTIPLE_LOW: 2,\n REAL_ESTATE_ADVANCE_RATE: 0.50,\n};\n\n// Coerce a scraped/stored figure to a finite number, else null. Absent data must\n// stay absent — a missing earnings figure means \"no offer\", never \"an offer of 0\".\nfunction toNumber(value) {\n if (value === null || value === undefined || value === \"\") return null;\n const numeric = typeof value === \"number\" ? value : parseFloat(String(value).replace(/[$,\\s]/g, \"\"));\n return Number.isFinite(numeric) ? numeric : null;\n}\n\nfunction toNonNegative(value) {\n const numeric = toNumber(value);\n return numeric === null || numeric < 0 ? null : numeric;\n}\n\n/**\n * Pick the earnings figure that drives the offer multiple, preferring EBITDA and\n * falling back to SDE then cash flow — most BizBuySell listings publish only\n * \"Cash Flow (SDE)\". The source is returned alongside so callers can label which\n * figure the multiple was applied to instead of implying it was EBITDA.\n * @param {{ebitda?:number, sde?:number, cashFlow?:number}} listing\n * @returns {{earnings:number|null, source:string|null}}\n */\nexport function resolveBusinessEarnings(listing = {}) {\n const ebitda = toNumber(listing.ebitda);\n if (ebitda !== null) return { earnings: ebitda, source: \"ebitda\" };\n\n const sde = toNumber(listing.sde);\n if (sde !== null) return { earnings: sde, source: \"sde\" };\n\n const cashFlow = toNumber(listing.cashFlow ?? listing.cash_flow);\n if (cashFlow !== null) return { earnings: cashFlow, source: \"cash_flow\" };\n\n return { earnings: null, source: null };\n}\n\n/**\n * Whether the real estate should be ADDED to the offer on top of the earnings multiple.\n *\n * Three inputs, in priority order:\n * 1. What the listing says. Explicit wording (\"included in asking price\", or its negation)\n * is a statement of fact from the seller and always wins.\n * 2. The arithmetic. When the stated real-estate value is at or above the asking price,\n * the building IS the ask — adding it again would quote an offer ceiling above the\n * price being asked, which is nonsense. This is the common case on listings that\n * publish a real-estate value and no inclusion wording at all.\n * 3. Otherwise include it, matching the default for every other asset: understating an\n * offer by silently dropping an asset is worse than an inclusion that can be toggled\n * off by hand.\n *\n * @param {{askingPrice?:number, realEstateValue?:number, statedInclusion?:boolean|null}} inputs\n * @returns {boolean}\n */\nexport function shouldIncludeRealEstate(inputs = {}) {\n if (typeof inputs.statedInclusion === \"boolean\") return inputs.statedInclusion;\n\n const askingPrice = toNumber(inputs.askingPrice);\n const realEstateValue = toNumber(inputs.realEstateValue);\n\n if (askingPrice !== null && askingPrice > 0 && realEstateValue !== null && realEstateValue >= askingPrice) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Down payment as the sum of per-asset collateral advances: 50% of the real\n * estate, 30% of FF&E, 20% of inventory, plus 20% of EBITDA once EBITDA reaches\n * $1M. Each leg is capped at its own rate, and a leg with no reported value\n * contributes nothing.\n *\n * The EBITDA leg reads EBITDA specifically, not the coalesced earnings figure —\n * an SDE-only listing does not qualify, because SDE includes owner compensation\n * and is not the same measure the threshold was set against.\n *\n * @param {{realEstateValue?:number, ffeValue?:number, inventoryValue?:number, ebitda?:number}} inputs\n * @returns {{downPayment:number, legs:{realEstate:number, ffe:number, inventory:number, ebitda:number}}}\n */\nexport function calculateBusinessDownPayment(inputs = {}) {\n const {\n EBITDA_ADVANCE_RATE,\n EBITDA_ADVANCE_THRESHOLD,\n FF_E_ADVANCE_RATE,\n INVENTORY_ADVANCE_RATE,\n REAL_ESTATE_ADVANCE_RATE,\n } = BUSINESS_UNDERWRITING_CONSTANTS;\n\n const realEstateValue = toNonNegative(inputs.realEstateValue) ?? 0;\n const ffeValue = toNonNegative(inputs.ffeValue) ?? 0;\n const inventoryValue = toNonNegative(inputs.inventoryValue) ?? 0;\n const ebitda = toNumber(inputs.ebitda) ?? 0;\n\n const legs = {\n ebitda: ebitda >= EBITDA_ADVANCE_THRESHOLD ? ebitda * EBITDA_ADVANCE_RATE : 0,\n ffe: ffeValue * FF_E_ADVANCE_RATE,\n inventory: inventoryValue * INVENTORY_ADVANCE_RATE,\n realEstate: realEstateValue * REAL_ESTATE_ADVANCE_RATE,\n };\n\n return {\n downPayment: legs.realEstate + legs.ffe + legs.inventory + legs.ebitda,\n legs,\n };\n}\n\n/**\n * The offer range: 2x to 3x earnings, plus the tangible assets that convey.\n * An asset the listing states is already covered by the asking price is excluded\n * by its include flag; the flags default to true, so an asset whose status could\n * not be determined is still offered on (dropping it silently would understate\n * the offer, which is the more damaging error).\n *\n * Returns nulls when no earnings figure exists — there is no honest multiple to\n * take, and quoting the assets alone would read as an offer.\n *\n * @param {object} inputs\n * @param {number} inputs.earnings - Earnings figure the multiple applies to\n * @param {number} [inputs.realEstateValue]\n * @param {number} [inputs.ffeValue]\n * @param {number} [inputs.inventoryValue]\n * @param {boolean} [inputs.includeRealEstate=true]\n * @param {boolean} [inputs.includeFfe=true]\n * @param {boolean} [inputs.includeInventory=true]\n * @returns {{assetsIncluded:number, offerHigh:number|null, offerLow:number|null}}\n */\nexport function calculateBusinessOffer(inputs = {}) {\n const { OFFER_MULTIPLE_HIGH, OFFER_MULTIPLE_LOW } = BUSINESS_UNDERWRITING_CONSTANTS;\n\n const {\n includeFfe = true,\n includeInventory = true,\n includeRealEstate = true,\n } = inputs;\n\n const assetsIncluded =\n (includeRealEstate ? toNonNegative(inputs.realEstateValue) ?? 0 : 0) +\n (includeFfe ? toNonNegative(inputs.ffeValue) ?? 0 : 0) +\n (includeInventory ? toNonNegative(inputs.inventoryValue) ?? 0 : 0);\n\n const earnings = toNumber(inputs.earnings);\n if (earnings === null) return { assetsIncluded, offerHigh: null, offerLow: null };\n\n return {\n assetsIncluded,\n offerHigh: earnings * OFFER_MULTIPLE_HIGH + assetsIncluded,\n offerLow: earnings * OFFER_MULTIPLE_LOW + assetsIncluded,\n };\n}\n\n/**\n * Full underwrite for one listing: resolve earnings, size the down payment from\n * collateral, and bound the offer. The seller carries the balance of the ceiling\n * offer as preferred equity.\n *\n * collateralShortfall is set when the collateral-driven down payment exceeds the\n * offer itself — a real signal that the asset value has outrun what the earnings\n * multiple justifies. It is surfaced, never clamped away.\n *\n * @param {object} listing - Scraped/stored figures (snake_case or camelCase)\n * @returns {object} earnings, earningsSource, downPayment, legs, offerLow, offerHigh, sellerCarry, collateralShortfall\n */\nexport function underwriteBusinessListing(listing = {}) {\n const realEstateValue = listing.realEstateValue ?? listing.real_estate_value;\n const ffeValue = listing.ffeValue ?? listing.ff_e_value;\n const inventoryValue = listing.inventoryValue ?? listing.inventory_value;\n const ebitda = listing.ebitda;\n\n const { earnings, source } = resolveBusinessEarnings({\n cashFlow: listing.cashFlow ?? listing.cash_flow,\n ebitda,\n sde: listing.sde,\n });\n\n const { downPayment, legs } = calculateBusinessDownPayment({\n ebitda,\n ffeValue,\n inventoryValue,\n realEstateValue,\n });\n\n const { assetsIncluded, offerHigh, offerLow } = calculateBusinessOffer({\n earnings,\n ffeValue,\n includeFfe: listing.includeFfe ?? listing.include_ff_e ?? true,\n includeInventory: listing.includeInventory ?? listing.include_inventory ?? true,\n includeRealEstate: listing.includeRealEstate ?? listing.include_real_estate ?? true,\n inventoryValue,\n realEstateValue,\n });\n\n return {\n assetsIncluded,\n collateralShortfall: offerHigh !== null && downPayment > offerHigh,\n downPayment,\n earnings,\n earningsSource: listing.earningsSource ?? listing.earnings_source ?? source,\n legs,\n offerHigh,\n offerLow,\n sellerCarry: offerHigh === null ? null : offerHigh - downPayment,\n };\n}\n"],"names":["BUSINESS_UNDERWRITING_CONSTANTS","EBITDA_ADVANCE_RATE","EBITDA_ADVANCE_THRESHOLD","FF_E_ADVANCE_RATE","INVENTORY_ADVANCE_RATE","OFFER_MULTIPLE_HIGH","OFFER_MULTIPLE_LOW","REAL_ESTATE_ADVANCE_RATE","toNumber","value","numeric","parseFloat","String","replace","Number","isFinite","toNonNegative","resolveBusinessEarnings","listing","ebitda","earnings","source","sde","cashFlow","cash_flow","shouldIncludeRealEstate","inputs","statedInclusion","askingPrice","realEstateValue","calculateBusinessDownPayment","ffeValue","inventoryValue","legs","ffe","inventory","realEstate","downPayment","calculateBusinessOffer","includeFfe","includeInventory","includeRealEstate","assetsIncluded","offerHigh","offerLow","underwriteBusinessListing","real_estate_value","ff_e_value","inventory_value","include_ff_e","include_inventory","include_real_estate","collateralShortfall","earningsSource","earnings_source","sellerCarry"],"mappings":"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"}
@@ -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:i=c.rate,dscrTerm:o=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),i,o),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:i=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:o=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:i});case r.ASSISTED_LIVING:return s*o*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 i=null!=l&&Number.isFinite(l),o=i?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,o,n,{bedroomCount:e,strApiResult:u});const t=(n||"").toLowerCase();T=t===r.STR?s?"measured":"estimate":t===r.ASSISTED_LIVING?"bedrooms":i?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(c)&&c>0?E/c:null,noi:E,noiSource:T,reportedCapRate:i?l:null}}function calculateAssignmentFee(e,r=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(r/100)}catch(e){return 0}}function calculateCashOfferPrice(e,r=t.CASH_OFFER_ASSIGNMENT_PERCENTAGE,a=t.CASH_OFFER_ROUNDING){const c=Number(e);if(!Number.isFinite(c)||c<=0)return null;const n=c*(1-r);return!Number.isFinite(a)||a<=0?n:Math.floor(Math.round(n)/a)*a}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:i=100*t.HARD_MONEY_RATE,dscrLtvPercent:o=100*e.DEFAULT_DSCR_PERCENTAGE}=a;try{return r*(c/100)-r*((n+l)/100)-r*(u/100)-i/100*(r-r*(o/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),i=t*(u-Math.pow(1+e,l))/(u-1);return Math.max(0,i)}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:i=c.rate,dscrTerm:o=c.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=n;try{const c=calculateAppreciatedValue(t,l,u),n=calculateBalloonBalance(r,i,o,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,calculateCashOfferPrice,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 a,PROPERTY_TYPE_CONSTANTS as r}from"../config/property-types.js";const n=e.INTEREST_RATE_TIERS[e.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(e,t,a){if(0===t)return e/(12*a);const r=t/12,n=12*a;return e*(r*Math.pow(1+r,n))/(Math.pow(1+r,n)-1)}function calculateCOCR30(e,t){try{const a=.3*e,r=12*calculatePMT(.7*e,.075,30);return(t-r)/a*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 a=Number(e),r=Number(t);return!Number.isFinite(a)||a<=0?1:Number.isFinite(r)?(a-r)/a:1}function calculatePriceForCOCR(a,r=.15,c={}){const{downPercent:l=100*e.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:o=100*e.DEFAULT_DSCR_PERCENTAGE,dscrRate:u=n.rate,dscrTerm:i=n.amortization,maxIterations:s=t.MAX_ITERATIONS,tolerance:E=t.CALCULATION_TOLERANCE}=c;try{let e=a/.08,n=0;for(;n<s;){const c=e*(l/100),s=12*calculatePMT(e*(o/100),u,i),T=(a-s)/c;if(Math.abs(T-r)<E)break;const R=T-r,A=R*t.ADJUSTMENT_FACTOR;e*=R>0?1+Math.abs(A):1-Math.abs(A),e<1e3&&(e=1e3),e>a*t.MAX_COCR15_PRICE_MULTIPLIER&&(e=a*t.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),n++}return e<t.MINIMUM_COCR15_PRICE&&(e=t.MINIMUM_COCR15_PRICE),e}catch(e){return 0}}function calculateCOCRAtPercent(e,t,a,r={}){const{dscrRate:c=n.rate,dscrTerm:l=n.amortization}=r;try{const r=e*(a/100),n=12*calculatePMT(e-r,c,l);return(t-n)/r*100}catch(e){return 0}}function calculateSTRNOI(e,t=null,a={}){const{grossRate:n=r.STR.ESTIMATED_GROSS_RATE,noiPercentage:c=r.STR.NOI_PERCENTAGE}=a;try{if(t&&Number.isFinite(t.value)&&t.value>=0){if("noi"===t.type)return t.value;if("gross"===t.type)return t.value*c}return!Number.isFinite(e)||e<=0?0:e*n*c}catch(e){return 0}}function calculateNOIByType(e,t,n=a.MULTIFAMILY,c={}){const{strApiResult:l=null,strGrossIncomeMultiplier:o=r.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:u=r.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:i=r.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:s=r.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=c;try{switch((n||a.MULTIFAMILY).toLowerCase()){case a.STR:return calculateSTRNOI(e,l,{grossRate:o,noiPercentage:u});case a.ASSISTED_LIVING:return s*i*12;case a.MULTIFAMILY:default:return e*t}}catch(e){return 0}}function resolveListingFinancials({bedroomCount:e=null,confirmedNOI:t=null,estimatedCapRate:r,price:n,propertyType:c=a.MULTIFAMILY,reportedCapRate:l=null,strApiResult:o=null}={}){const u=null!=l&&Number.isFinite(l),i=u?l:r,s=o&&Number.isFinite(o.value)&&o.value>=0&&("noi"===o.type||"gross"===o.type);let E,T;if(null!=t&&Number.isFinite(t)&&t>=0)E=t,T="confirmed";else{E=calculateNOIByType(n,i,c,{bedroomCount:e,strApiResult:o});const t=(c||"").toLowerCase();T=t===a.STR?s?"measured":"estimate":t===a.ASSISTED_LIVING?"bedrooms":u?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(n)&&n>0?E/n:null,noi:E,noiSource:T,reportedCapRate:u?l:null}}function calculateAssignmentFee(e,a=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(a/100)}catch(e){return 0}}function calculateCashOfferPrice(e,a=t.CASH_OFFER_ASSIGNMENT_PERCENTAGE,r=t.CASH_OFFER_ROUNDING){const n=Number(e);if(!Number.isFinite(n)||n<=0)return null;const c=n*(1-a);return!Number.isFinite(r)||r<=0?c:Math.floor(Math.round(c)/r)*r}function calculateSellerFinanceOffer(a,r,n={}){const{amortizationYears:c=e.SELLER_FI_AMORTIZATION,interestRate:l=e.SELLER_FI_INTEREST_RATE,maxDownPercent:o=t.SELLER_FINANCE_MAX_DOWN_PERCENT,minDownPercent:u=1,targetCOCR:i=15,yearsBalloon:s=e.DEFAULT_BALLOON_PERIOD_YEARS}=n,cocrAtDown=e=>{const t=r*(e/100),n=r*((100-e)/100),o=n>0?calculatePMT(n,l,c):0;return t>0?(a-12*o)/t*100:0};let E;if(cocrAtDown(u)<=i)E=u;else{let e=u,a=100,r=0;for(;r<t.MAX_ITERATIONS&&a-e>.01;){const t=(e+a)/2;cocrAtDown(t)<i?a=t:e=t,r++}E=(e+a)/2}const T=Math.min(E,o),R=r*(T/100),A=r*((100-T)/100),_=A>0?calculatePMT(A,l,c):0,N=12*_,I=a-N;return{annualCashFlow:I,annualDebtService:N,balloonBalance:calculateBalloonBalance(A,l,c,s),capped:E>o,cashFlowYield:r>0?I/r*100:0,cocr:R>0?I/R*100:0,downPaymentAmount:R,downPercent:T,monthlyPayment:_,sellerNoteAmount:A,solvedDownPercent:E,totalPaymentsToBalloon:N*s}}function calculateNetToBuyer(a,r={}){const{buyerCostPercent:n=100*t.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:c=100*t.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*t.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:o=100*t.REHAB_RATE,additionalCostFinancing:u=100*t.HARD_MONEY_RATE,dscrLtvPercent:i=100*e.DEFAULT_DSCR_PERCENTAGE}=r;try{return a*(n/100)-a*((c+l)/100)-a*(o/100)-u/100*(a-a*(i/100))}catch(e){return 0}}function calculateBalloonBalance(t,a,r,n=e.DEFAULT_BALLOON_PERIOD_YEARS){try{if(t<=0||a<0||r<=0||n<=0)return 0;if(n>=r)return 0;if(0===a){const e=12*r;return t*(e-12*n)/e}const e=a/12,c=12*r,l=12*n,o=Math.pow(1+e,c),u=t*(o-Math.pow(1+e,l))/(o-1);return Math.max(0,u)}catch(e){return 0}}function calculateAppreciatedValue(t,a=e.APPRECIATION_RATE,r=e.DEFAULT_BALLOON_PERIOD_YEARS){try{return t<=0||a<0||r<0?t:t*Math.pow(1+a,r)}catch(e){return t}}function calculateCashOutAfterRefi(t,a,r,c={}){const{appreciationRate:l=e.APPRECIATION_RATE,balloonYears:o=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:u=n.rate,dscrTerm:i=n.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=c;try{const n=calculateAppreciatedValue(t,l,o),c=calculateBalloonBalance(a,u,i,o),T=calculateBalloonBalance(r,e.SELLER_FI_INTEREST_RATE,s,o);return n*(E/100)-(c+T)}catch(e){return 0}}function calculateCashFlow(e,t,a){return e-(t+a)}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,calculateCashOfferPrice,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,calculateSellerFinanceOffer,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 || 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 the all-cash offer price from the 15%-COCR price.\r\n * The cash offer is the standard-investor 15%-COCR price less a haircut that\r\n * covers the assignment fee and associated costs, then floored DOWN to the\r\n * rounding step. Flooring (never rounding up) keeps the offer at or below the\r\n * 15%-COCR maximum threshold.\r\n * @param {number} cocr15Price - Price at which COCR hits 15% on the NOI (dollars)\r\n * @param {number} assignmentPercent - Haircut as a decimal (uses config default)\r\n * @param {number} roundingStep - Floor the result down to this step; 0 disables (uses config default)\r\n * @returns {number|null} Cash offer price, or null when cocr15Price is not a positive number\r\n */\r\nexport function calculateCashOfferPrice(\r\n cocr15Price,\r\n assignmentPercent = BUSINESS_CONSTANTS.CASH_OFFER_ASSIGNMENT_PERCENTAGE,\r\n roundingStep = BUSINESS_CONSTANTS.CASH_OFFER_ROUNDING\r\n) {\r\n const price = Number(cocr15Price);\r\n if (!Number.isFinite(price) || price <= 0) return null;\r\n const haircutPrice = price * (1 - assignmentPercent);\r\n if (!Number.isFinite(roundingStep) || roundingStep <= 0) return haircutPrice;\r\n // Round to whole dollars first so binary-float noise (e.g. 929999.9999) can't\r\n // knock the value down a whole step, then floor to the step.\r\n return Math.floor(Math.round(haircutPrice) / roundingStep) * roundingStep;\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","calculateCashOfferPrice","cocr15Price","CASH_OFFER_ASSIGNMENT_PERCENTAGE","roundingStep","CASH_OFFER_ROUNDING","haircutPrice","floor","round","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
+ {"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 the all-cash offer price from the 15%-COCR price.\r\n * The cash offer is the standard-investor 15%-COCR price less a haircut that\r\n * covers the assignment fee and associated costs, then floored DOWN to the\r\n * rounding step. Flooring (never rounding up) keeps the offer at or below the\r\n * 15%-COCR maximum threshold.\r\n * @param {number} cocr15Price - Price at which COCR hits 15% on the NOI (dollars)\r\n * @param {number} assignmentPercent - Haircut as a decimal (uses config default)\r\n * @param {number} roundingStep - Floor the result down to this step; 0 disables (uses config default)\r\n * @returns {number|null} Cash offer price, or null when cocr15Price is not a positive number\r\n */\r\nexport function calculateCashOfferPrice(\r\n cocr15Price,\r\n assignmentPercent = BUSINESS_CONSTANTS.CASH_OFFER_ASSIGNMENT_PERCENTAGE,\r\n roundingStep = BUSINESS_CONSTANTS.CASH_OFFER_ROUNDING\r\n) {\r\n const price = Number(cocr15Price);\r\n if (!Number.isFinite(price) || price <= 0) return null;\r\n const haircutPrice = price * (1 - assignmentPercent);\r\n if (!Number.isFinite(roundingStep) || roundingStep <= 0) return haircutPrice;\r\n // Round to whole dollars first so binary-float noise (e.g. 929999.9999) can't\r\n // knock the value down a whole step, then floor to the step.\r\n return Math.floor(Math.round(haircutPrice) / roundingStep) * roundingStep;\r\n}\r\n\r\n/**\r\n * Solve and analyze a Standard Seller Financing offer (pure seller carry: down payment\r\n * plus seller note equal 100% of the offer price — no bank/DSCR leg).\r\n *\r\n * The down payment is solved to the value that yields the target cash-on-cash return,\r\n * then capped at maxDownPercent. In the default 0%-interest structure the COCR falls as\r\n * the down payment rises (more cash for the same debt service), so:\r\n * - if the target COCR is reachable at <= maxDownPercent down, that exact down is used\r\n * (COCR lands on target);\r\n * - otherwise the down is capped at maxDownPercent and the COCR comes in ABOVE target.\r\n * Either way the down never exceeds maxDownPercent and the COCR never falls below target\r\n * for a cash-flowing deal. A deal that cannot cash-flow even at the minimum down (COCR at\r\n * minDownPercent already below target) returns minDownPercent with its actual (poor) COCR.\r\n *\r\n * @param {number} noi - Annual net operating income\r\n * @param {number} price - Offer price the down payment and seller note are sized against\r\n * @param {Object} options - Term and target overrides (default to config constants)\r\n * @param {number} [options.amortizationYears] - Seller note amortization (years)\r\n * @param {number} [options.interestRate] - Seller note annual interest rate (decimal, e.g. 0.07)\r\n * @param {number} [options.maxDownPercent] - Down-payment ceiling (whole percent)\r\n * @param {number} [options.minDownPercent] - Down-payment floor (whole percent)\r\n * @param {number} [options.targetCOCR] - Target cash-on-cash return (whole percent)\r\n * @param {number} [options.yearsBalloon] - Balloon period (years)\r\n * @returns {{annualCashFlow:number, annualDebtService:number, balloonBalance:number, capped:boolean, cashFlowYield:number, cocr:number, downPaymentAmount:number, downPercent:number, monthlyPayment:number, sellerNoteAmount:number, solvedDownPercent:number, totalPaymentsToBalloon:number}}\r\n */\r\nexport function calculateSellerFinanceOffer(noi, price, options = {}) {\r\n const {\r\n amortizationYears = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n interestRate = FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE,\r\n maxDownPercent = BUSINESS_CONSTANTS.SELLER_FINANCE_MAX_DOWN_PERCENT,\r\n minDownPercent = 1,\r\n targetCOCR = 15,\r\n yearsBalloon = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n } = options;\r\n\r\n const cocrAtDown = (downPercent) => {\r\n const downPaymentAmount = price * (downPercent / 100);\r\n const sellerNoteAmount = price * ((100 - downPercent) / 100);\r\n const monthlyPayment = sellerNoteAmount > 0 ? calculatePMT(sellerNoteAmount, interestRate, amortizationYears) : 0;\r\n const annualCashFlow = noi - monthlyPayment * 12;\r\n return downPaymentAmount > 0 ? (annualCashFlow / downPaymentAmount) * 100 : 0;\r\n };\r\n\r\n let solvedDownPercent;\r\n if (cocrAtDown(minDownPercent) <= targetCOCR) {\r\n solvedDownPercent = minDownPercent;\r\n } else {\r\n let low = minDownPercent;\r\n let high = 100;\r\n let iterations = 0;\r\n while (iterations < BUSINESS_CONSTANTS.MAX_ITERATIONS && high - low > 0.01) {\r\n const mid = (low + high) / 2;\r\n if (cocrAtDown(mid) < targetCOCR) {\r\n high = mid;\r\n } else {\r\n low = mid;\r\n }\r\n iterations++;\r\n }\r\n solvedDownPercent = (low + high) / 2;\r\n }\r\n\r\n const downPercent = Math.min(solvedDownPercent, maxDownPercent);\r\n const downPaymentAmount = price * (downPercent / 100);\r\n const sellerNoteAmount = price * ((100 - downPercent) / 100);\r\n const monthlyPayment = sellerNoteAmount > 0 ? calculatePMT(sellerNoteAmount, interestRate, amortizationYears) : 0;\r\n const annualDebtService = monthlyPayment * 12;\r\n const annualCashFlow = noi - annualDebtService;\r\n\r\n return {\r\n annualCashFlow,\r\n annualDebtService,\r\n balloonBalance: calculateBalloonBalance(sellerNoteAmount, interestRate, amortizationYears, yearsBalloon),\r\n capped: solvedDownPercent > maxDownPercent,\r\n cashFlowYield: price > 0 ? (annualCashFlow / price) * 100 : 0,\r\n cocr: downPaymentAmount > 0 ? (annualCashFlow / downPaymentAmount) * 100 : 0,\r\n downPaymentAmount,\r\n downPercent,\r\n monthlyPayment,\r\n sellerNoteAmount,\r\n solvedDownPercent,\r\n totalPaymentsToBalloon: annualDebtService * yearsBalloon,\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","calculateCashOfferPrice","cocr15Price","CASH_OFFER_ASSIGNMENT_PERCENTAGE","roundingStep","CASH_OFFER_ROUNDING","haircutPrice","floor","round","calculateSellerFinanceOffer","amortizationYears","SELLER_FI_AMORTIZATION","interestRate","SELLER_FI_INTEREST_RATE","maxDownPercent","SELLER_FINANCE_MAX_DOWN_PERCENT","minDownPercent","yearsBalloon","DEFAULT_BALLOON_PERIOD_YEARS","cocrAtDown","downPaymentAmount","sellerNoteAmount","monthlyPayment","solvedDownPercent","low","high","mid","min","annualDebtService","annualCashFlow","balloonBalance","calculateBalloonBalance","capped","cashFlowYield","cocr","totalPaymentsToBalloon","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","loanAmount","balloonYears","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","fallback","isNaN"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOfferPrice,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage}from"./financial/calculations.js";export{EQUITY_CARRY_TIERS,calculateEquityCarryScore}from"./financial/equity-carry.js";export{fetchDebt}from"./services/debt.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType}from"./export/export-logic.js";export{calculateDOM,formatDate}from"./date/utilities.js";export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput}from"./formatting/financial-formatting.js";export{normalizeWhitespace}from"./formatting/text.js";export{CALCULATION_TOLERANCE,DEFAULT_CAP_RATE,DEFAULT_DOWN_PAYMENT,DEFAULT_DSCR_PERCENTAGE,DEFAULT_EQUITY_ESTIMATE,DEFAULT_INTEREST_RATE_TYPE,FINANCIAL_CONSTANTS,INTEREST_RATE_TIERS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE,determineInterestRateType}from"./config/financial.js";export{ASSISTED_LIVING,MULTIFAMILY,PROPERTY_TYPES,PROPERTY_TYPE_CONSTANTS,STR}from"./config/property-types.js";export{ASSIGNMENT_FEE_PERCENTAGE,BUSINESS_CONSTANTS,BUYER_AGENT_COMMISSION,CASH_OFFER_ASSIGNMENT_PERCENTAGE,CASH_OFFER_ROUNDING,CLOSING_COSTS_PERCENTAGE,CONSERVATIVE_COCR15_PRICE_MULTIPLIER,HARD_MONEY_RATE,MAX_COCR15_PRICE_MULTIPLIER,MINIMUM_COCR15_PRICE,NET_TO_BUYER_PERCENTAGE,REHAB_RATE,SELLER_AGENT_COMMISSION}from"./config/business.js";export{lookupLOI}from"./services/loi-lookup.js";export{LOI_LOOKUP_CONFIG,LOI_SENT_STATUS,MATCH_TYPES}from"./config/loi-lookup.js";export{getEnvVar,isBrowserEnvironment,isNodeEnvironment}from"./environment/utilities.js";const e="./dist/styles/base.css";export{e as STYLES_PATH};
1
+ export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOfferPrice,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,calculateSellerFinanceOffer,equityPercentFromDebt,resolveListingFinancials,safePercentage}from"./financial/calculations.js";export{EQUITY_CARRY_TIERS,calculateEquityCarryScore}from"./financial/equity-carry.js";export{BUSINESS_UNDERWRITING_CONSTANTS,calculateBusinessDownPayment,calculateBusinessOffer,resolveBusinessEarnings,shouldIncludeRealEstate,underwriteBusinessListing}from"./financial/business-underwriting.js";export{fetchDebt}from"./services/debt.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType}from"./export/export-logic.js";export{calculateDOM,formatDate}from"./date/utilities.js";export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput}from"./formatting/financial-formatting.js";export{normalizeWhitespace}from"./formatting/text.js";export{CALCULATION_TOLERANCE,DEFAULT_CAP_RATE,DEFAULT_DOWN_PAYMENT,DEFAULT_DSCR_PERCENTAGE,DEFAULT_EQUITY_ESTIMATE,DEFAULT_INTEREST_RATE_TYPE,FINANCIAL_CONSTANTS,INTEREST_RATE_TIERS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE,determineInterestRateType}from"./config/financial.js";export{ASSISTED_LIVING,MULTIFAMILY,PROPERTY_TYPES,PROPERTY_TYPE_CONSTANTS,STR}from"./config/property-types.js";export{ASSIGNMENT_FEE_PERCENTAGE,BUSINESS_CONSTANTS,BUYER_AGENT_COMMISSION,CASH_OFFER_ASSIGNMENT_PERCENTAGE,CASH_OFFER_ROUNDING,CLOSING_COSTS_PERCENTAGE,CONSERVATIVE_COCR15_PRICE_MULTIPLIER,HARD_MONEY_RATE,MAX_COCR15_PRICE_MULTIPLIER,MINIMUM_COCR15_PRICE,NET_TO_BUYER_PERCENTAGE,REHAB_RATE,SELLER_AGENT_COMMISSION,SELLER_FINANCE_MAX_DOWN_PERCENT}from"./config/business.js";export{lookupLOI}from"./services/loi-lookup.js";export{LOI_LOOKUP_CONFIG,LOI_SENT_STATUS,MATCH_TYPES}from"./config/loi-lookup.js";export{getEnvVar,isBrowserEnvironment,isNodeEnvironment}from"./environment/utilities.js";const e="./dist/styles/base.css";export{e as STYLES_PATH};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\r\n * @archerjessop/utilities\r\n * Shared utilities for ArcherJessop property analysis tools\r\n */\r\n\r\n// Financial calculations\r\nexport { \r\n calculateAppreciatedValue,\r\n calculateAssignmentFee,\r\n calculateBalloonBalance,\r\n calculateCashFlow,\r\n calculateCashFlowYield,\r\n calculateCashOfferPrice,\r\n calculateCashOutAfterRefi,\r\n calculateCOCR30, \r\n calculateCOCRAtPercent,\r\n calculateDiscountFromPrice,\r\n calculateNetToBuyer,\r\n calculateNOIByType,\r\n calculatePMT,\r\n calculatePriceForCOCR,\r\n calculatePriceFromDiscount,\r\n calculateSTRNOI,\r\n equityPercentFromDebt,\r\n resolveListingFinancials,\r\n safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Equity Carry scoring engine (pure; scores scraped listings into deal pools)\r\nexport { EQUITY_CARRY_TIERS, calculateEquityCarryScore } from \"./financial/equity-carry.js\";\r\n\r\n// Agnostic debt service (pure IO; Node + browser)\r\nexport { fetchDebt } from \"./services/debt.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Export logic (pure export-object creation)\r\nexport {\r\n calculateOriginalPrice,\r\n convertCapRateToDecimal,\r\n createExportObjectCore,\r\n formatDownPaymentPercent,\r\n mapPropertyType,\r\n} from \"./export/export-logic.js\";\r\n\r\n// Date utilities\r\nexport { calculateDOM, formatDate } from \"./date/utilities.js\";\r\n\r\n// Formatting utilities\r\nexport { \r\n calculateCursorPosition,\r\n extractNumericValue,\r\n filterNumericInput,\r\n formatInputDisplay,\r\n formatLiveInput,\r\n formatLiveNumber,\r\n parseNumericInput\r\n} from \"./formatting/financial-formatting.js\";\r\n\r\n// Text formatting utilities\r\nexport { normalizeWhitespace } from \"./formatting/text.js\";\r\n\r\n// Configuration constants\r\nexport * from \"./config/financial.js\";\r\nexport * from \"./config/property-types.js\";\r\nexport * from \"./config/business.js\";\r\n\r\nexport const STYLES_PATH = \"./dist/styles/base.css\";\r\n\r\n// LOI Lookup service and config\r\nexport { lookupLOI } from \"./services/loi-lookup.js\";\r\nexport { LOI_LOOKUP_CONFIG, MATCH_TYPES, LOI_SENT_STATUS } from \"./config/loi-lookup.js\";\r\n\r\n// Environment utilities\r\nexport { \r\n getEnvVar, \r\n isNodeEnvironment, \r\n isBrowserEnvironment \r\n} from \"./environment/utilities.js\";"],"names":["STYLES_PATH"],"mappings":"qkEAoEY,MAACA,EAAc"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\r\n * @archerjessop/utilities\r\n * Shared utilities for ArcherJessop property analysis tools\r\n */\r\n\r\n// Financial calculations\r\nexport { \r\n calculateAppreciatedValue,\r\n calculateAssignmentFee,\r\n calculateBalloonBalance,\r\n calculateCashFlow,\r\n calculateCashFlowYield,\r\n calculateCashOfferPrice,\r\n calculateCashOutAfterRefi,\r\n calculateCOCR30, \r\n calculateCOCRAtPercent,\r\n calculateDiscountFromPrice,\r\n calculateNetToBuyer,\r\n calculateNOIByType,\r\n calculatePMT,\r\n calculatePriceForCOCR,\r\n calculatePriceFromDiscount,\r\n calculateSellerFinanceOffer,\r\n calculateSTRNOI,\r\n equityPercentFromDebt,\r\n resolveListingFinancials,\r\n safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Equity Carry scoring engine (pure; scores scraped listings into deal pools)\r\nexport { EQUITY_CARRY_TIERS, calculateEquityCarryScore } from \"./financial/equity-carry.js\";\r\n\r\n// Business-backed-by-real-estate underwriting (pure; offer range + collateral down payment)\r\nexport {\r\n BUSINESS_UNDERWRITING_CONSTANTS,\r\n calculateBusinessDownPayment,\r\n calculateBusinessOffer,\r\n resolveBusinessEarnings,\r\n shouldIncludeRealEstate,\r\n underwriteBusinessListing,\r\n} from \"./financial/business-underwriting.js\";\r\n\r\n// Agnostic debt service (pure IO; Node + browser)\r\nexport { fetchDebt } from \"./services/debt.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Export logic (pure export-object creation)\r\nexport {\r\n calculateOriginalPrice,\r\n convertCapRateToDecimal,\r\n createExportObjectCore,\r\n formatDownPaymentPercent,\r\n mapPropertyType,\r\n} from \"./export/export-logic.js\";\r\n\r\n// Date utilities\r\nexport { calculateDOM, formatDate } from \"./date/utilities.js\";\r\n\r\n// Formatting utilities\r\nexport { \r\n calculateCursorPosition,\r\n extractNumericValue,\r\n filterNumericInput,\r\n formatInputDisplay,\r\n formatLiveInput,\r\n formatLiveNumber,\r\n parseNumericInput\r\n} from \"./formatting/financial-formatting.js\";\r\n\r\n// Text formatting utilities\r\nexport { normalizeWhitespace } from \"./formatting/text.js\";\r\n\r\n// Configuration constants\r\nexport * from \"./config/financial.js\";\r\nexport * from \"./config/property-types.js\";\r\nexport * from \"./config/business.js\";\r\n\r\nexport const STYLES_PATH = \"./dist/styles/base.css\";\r\n\r\n// LOI Lookup service and config\r\nexport { lookupLOI } from \"./services/loi-lookup.js\";\r\nexport { LOI_LOOKUP_CONFIG, MATCH_TYPES, LOI_SENT_STATUS } from \"./config/loi-lookup.js\";\r\n\r\n// Environment utilities\r\nexport { \r\n getEnvVar, \r\n isNodeEnvironment, \r\n isBrowserEnvironment \r\n} from \"./environment/utilities.js\";"],"names":["STYLES_PATH"],"mappings":"i1EA+EY,MAACA,EAAc"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "7.29.1",
3
+ "version": "7.31.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",