@archerjessop/utilities 7.9.0 → 7.11.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
- export{extractBedrooms,extractPhoneNumber}from"./data/extractors.js";export{calculateCashFlowTooltip,calculateDownPaymentTooltip,parseCashFlowData,parseFinancialData}from"./financial/tooltip-calculations.js";export{generateCapRateTooltipHTML,generateCashFlowTooltipHTML,generateDownPaymentTooltipHTML,generatePriceTooltipHTML}from"./financial/tooltip-content-generators.js";export{setupCapRateClickHandler,setupDiscountButtonHandler,setupDownPaymentClickHandler,setupPriceClickHandler}from"./ui/click-handlers.js";export{CLICKABLE_TOOLTIPS,TOOLTIP_ENABLED_METRICS}from"./ui/tooltip-config.js";export{attachTooltip,hasTooltip,isTooltipVisible,removeAllTooltips,removeTooltip,updateTooltipContent}from"./ui/tooltip-manager.js";export{createNavigationGuard}from"./widget/createNavigationGuard.js";export{createPanel}from"./widget/createPanel.js";export{runReveals}from"./widget/runReveals.js";export{createAnalyzer}from"./widget/createAnalyzer.js";export{createAnalyzerState}from"./widget/createAnalyzerState.js";export{calculateFinancials}from"./financial/calculateFinancials.js";export{fetchEquity}from"./services/equity.js";export{fetchStrRevenue}from"./services/str-revenue.js";
1
+ export{extractBedrooms,extractPhoneNumber}from"./data/extractors.js";export{calculateCashFlowTooltip,calculateDownPaymentTooltip,parseCashFlowData,parseFinancialData}from"./financial/tooltip-calculations.js";export{generateCapRateTooltipHTML,generateCashFlowTooltipHTML,generateDownPaymentTooltipHTML,generatePriceTooltipHTML}from"./financial/tooltip-content-generators.js";export{setupCapRateClickHandler,setupDiscountButtonHandler,setupDownPaymentClickHandler,setupPriceClickHandler}from"./ui/click-handlers.js";export{CLICKABLE_TOOLTIPS,TOOLTIP_ENABLED_METRICS}from"./ui/tooltip-config.js";export{attachTooltip,hasTooltip,isTooltipVisible,removeAllTooltips,removeTooltip,updateTooltipContent}from"./ui/tooltip-manager.js";export{createNavigationGuard}from"./widget/createNavigationGuard.js";export{createPanel}from"./widget/createPanel.js";export{runReveals}from"./widget/runReveals.js";export{createAnalyzer}from"./widget/createAnalyzer.js";export{createAnalyzerState}from"./widget/createAnalyzerState.js";export{calculateFinancials}from"./financial/calculateFinancials.js";export{fetchDebt}from"../services/debt.js";export{fetchStrRevenue}from"./services/str-revenue.js";
2
2
  //# sourceMappingURL=index.js.map
@@ -1,2 +1,2 @@
1
- import{createAnalyzerState as e}from"./createAnalyzerState.js";import{createExportOps as r}from"./exportOps.js";import{createFinance as t}from"./finance.js";import{createNav as n}from"./nav.js";import{createPipeline as o}from"./pipeline.js";import{createRender as a}from"./render.js";import{createServices as p}from"./services.js";function createAnalyzer(i){if(!i||"object"!=typeof i)throw new TypeError("createAnalyzer(adapter): adapter must be an object");for(const e of["matches","getListingId","scrape"])if("function"!=typeof i[e])throw new TypeError(`createAnalyzer(adapter): adapter.${e} must be a function`);const c=i.config||{},s=e({defaultPropertyType:c.defaultPropertyType}),f=a({ctx:s}),m=t({ctx:s,adapter:i,render:f}),d=p({ctx:s}),u=r({ctx:s,ensureEquityLoaded:d.ensureEquityLoaded,scrapeAndApply:m.scrapeAndApply}),l=o({adapter:i,config:c,ctx:s,exportOps:u,finance:m,render:f,resolveCssUrls:resolveCssUrls,services:d}),y=n({adapter:i,config:c,ctx:s,runPipeline:l.runPipeline});return{ctx:s,createExportObject:u.createExportObject,handleNavigation:y.handleNavigation,runPipeline:l.runPipeline,start:y.start}}function resolveCssUrls(e=[]){const r="undefined"!=typeof chrome&&chrome.runtime&&"function"==typeof chrome.runtime.getURL;return e.map(e=>r?chrome.runtime.getURL(e):e)}export{createAnalyzer};
1
+ import{createAnalyzerState as e}from"./createAnalyzerState.js";import{createExportOps as r}from"./exportOps.js";import{createFinance as t}from"./finance.js";import{createNav as n}from"./nav.js";import{createPipeline as o}from"./pipeline.js";import{createRender as a}from"./render.js";import{createServices as p}from"./services.js";function createAnalyzer(c){if(!c||"object"!=typeof c)throw new TypeError("createAnalyzer(adapter): adapter must be an object");for(const e of["matches","getListingId","scrape"])if("function"!=typeof c[e])throw new TypeError(`createAnalyzer(adapter): adapter.${e} must be a function`);const i=c.config||{},s=e({defaultPropertyType:i.defaultPropertyType}),f=a({ctx:s}),m=t({ctx:s,adapter:c,render:f}),d=p({ctx:s}),l=r({ctx:s,ensureDebtLoaded:d.ensureDebtLoaded,scrapeAndApply:m.scrapeAndApply}),u=o({adapter:c,config:i,ctx:s,exportOps:l,finance:m,render:f,resolveCssUrls:resolveCssUrls,services:d}),y=n({adapter:c,config:i,ctx:s,runPipeline:u.runPipeline});return{ctx:s,createExportObject:l.createExportObject,handleNavigation:y.handleNavigation,runPipeline:u.runPipeline,start:y.start}}function resolveCssUrls(e=[]){const r="undefined"!=typeof chrome&&chrome.runtime&&"function"==typeof chrome.runtime.getURL;return e.map(e=>r?chrome.runtime.getURL(e):e)}export{createAnalyzer};
2
2
  //# sourceMappingURL=createAnalyzer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"createAnalyzer.js","sources":["../../../src/browser/widget/createAnalyzer.js"],"sourcesContent":["// The common analyzer engine — composition root. A site becomes a thin adapter; this wires the\r\n// decomposed units (render / finance / services / export / pipeline / nav) over a single injected\r\n// ctx and exposes the public surface. Each unit lives in its own module and is independently\r\n// tested; this file just validates the adapter and connects the pieces in dependency order.\r\n//\r\n// Generalized from zillow-analyzer/content.js (the green SPA copy). The per-repo state /\r\n// updateState / resetForNavigation singletons become an injected ctx (createAnalyzerState);\r\n// extractData / isPropertyPage / getListingId become the adapter; the equity / STR / LOI fetches\r\n// become agnostic service calls the engine orchestrates.\r\n//\r\n/* global chrome */\r\n// adapter = {\r\n// matches(url): boolean, // is this a listing page (was isPropertyPage)\r\n// getListingId(url): string|null, // listing identity for the SPA watcher (was zpid)\r\n// scrape(): Listing, // the Listing contract (see browser/index.js)\r\n// config: { defaultPropertyType, cssFiles, spa? },\r\n// }\r\n\r\nimport { createAnalyzerState } from \"./createAnalyzerState.js\";\r\nimport { createExportOps } from \"./exportOps.js\";\r\nimport { createFinance } from \"./finance.js\";\r\nimport { createNav } from \"./nav.js\";\r\nimport { createPipeline } from \"./pipeline.js\";\r\nimport { createRender } from \"./render.js\";\r\nimport { createServices } from \"./services.js\";\r\n\r\nexport function createAnalyzer(adapter) {\r\n // Construction-validation: fail loud at wiring time, not deep in the pipeline.\r\n if (!adapter || typeof adapter !== \"object\") {\r\n throw new TypeError(\"createAnalyzer(adapter): adapter must be an object\");\r\n }\r\n for (const fn of [\"matches\", \"getListingId\", \"scrape\"]) {\r\n if (typeof adapter[fn] !== \"function\") {\r\n throw new TypeError(`createAnalyzer(adapter): adapter.${fn} must be a function`);\r\n }\r\n }\r\n const config = adapter.config || {};\r\n\r\n const ctx = createAnalyzerState({ defaultPropertyType: config.defaultPropertyType });\r\n\r\n // Wire the units in dependency order: render (leaf) -> finance/services -> export -> pipeline -> nav.\r\n const render = createRender({ ctx });\r\n const finance = createFinance({ ctx, adapter, render });\r\n const services = createServices({ ctx });\r\n const exportOps = createExportOps({\r\n ctx,\r\n ensureEquityLoaded: services.ensureEquityLoaded,\r\n scrapeAndApply: finance.scrapeAndApply,\r\n });\r\n const pipeline = createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services });\r\n const nav = createNav({ adapter, config, ctx, runPipeline: pipeline.runPipeline });\r\n\r\n return {\r\n ctx,\r\n createExportObject: exportOps.createExportObject,\r\n handleNavigation: nav.handleNavigation,\r\n runPipeline: pipeline.runPipeline,\r\n start: nav.start,\r\n };\r\n}\r\n\r\n// chrome.runtime.getURL resolves extension-relative CSS paths at runtime; guard it so the\r\n// engine is loadable under jsdom / Node (tests) where `chrome` is absent.\r\nfunction resolveCssUrls(cssFiles = []) {\r\n const hasChrome = typeof chrome !== \"undefined\" && chrome.runtime && typeof chrome.runtime.getURL === \"function\";\r\n return cssFiles.map((f) => (hasChrome ? chrome.runtime.getURL(f) : f));\r\n}\r\n"],"names":["createAnalyzer","adapter","TypeError","fn","config","ctx","createAnalyzerState","defaultPropertyType","render","createRender","finance","createFinance","services","createServices","exportOps","createExportOps","ensureEquityLoaded","scrapeAndApply","pipeline","createPipeline","resolveCssUrls","nav","createNav","runPipeline","createExportObject","handleNavigation","start","cssFiles","hasChrome","chrome","runtime","getURL","map","f"],"mappings":"2UA0BO,SAASA,eAAeC,GAE7B,IAAKA,GAA8B,iBAAZA,EACrB,MAAM,IAAIC,UAAU,sDAEtB,IAAK,MAAMC,IAAM,CAAC,UAAW,eAAgB,UAC3C,GAA2B,mBAAhBF,EAAQE,GACjB,MAAM,IAAID,UAAU,oCAAoCC,wBAG5D,MAAMC,EAASH,EAAQG,QAAU,GAE3BC,EAAMC,EAAoB,CAAEC,oBAAqBH,EAAOG,sBAGxDC,EAASC,EAAa,CAAEJ,QACxBK,EAAUC,EAAc,CAAEN,MAAKJ,UAASO,WACxCI,EAAWC,EAAe,CAAER,QAC5BS,EAAYC,EAAgB,CAChCV,MACAW,mBAAoBJ,EAASI,mBAC7BC,eAAgBP,EAAQO,iBAEpBC,EAAWC,EAAe,CAAElB,UAASG,SAAQC,MAAKS,YAAWJ,UAASF,SAAQY,8BAAgBR,aAC9FS,EAAMC,EAAU,CAAErB,UAASG,SAAQC,MAAKkB,YAAaL,EAASK,cAEpE,MAAO,CACLlB,MACAmB,mBAAoBV,EAAUU,mBAC9BC,iBAAkBJ,EAAII,iBACtBF,YAAaL,EAASK,YACtBG,MAAOL,EAAIK,MAEf,CAIA,SAASN,eAAeO,EAAW,IACjC,MAAMC,EAA8B,oBAAXC,QAA0BA,OAAOC,SAA4C,mBAA1BD,OAAOC,QAAQC,OAC3F,OAAOJ,EAASK,IAAKC,GAAOL,EAAYC,OAAOC,QAAQC,OAAOE,GAAKA,EACrE"}
1
+ {"version":3,"file":"createAnalyzer.js","sources":["../../../src/browser/widget/createAnalyzer.js"],"sourcesContent":["// The common analyzer engine — composition root. A site becomes a thin adapter; this wires the\r\n// decomposed units (render / finance / services / export / pipeline / nav) over a single injected\r\n// ctx and exposes the public surface. Each unit lives in its own module and is independently\r\n// tested; this file just validates the adapter and connects the pieces in dependency order.\r\n//\r\n// Generalized from zillow-analyzer/content.js (the green SPA copy). The per-repo state /\r\n// updateState / resetForNavigation singletons become an injected ctx (createAnalyzerState);\r\n// extractData / isPropertyPage / getListingId become the adapter; the equity / STR / LOI fetches\r\n// become agnostic service calls the engine orchestrates.\r\n//\r\n/* global chrome */\r\n// adapter = {\r\n// matches(url): boolean, // is this a listing page (was isPropertyPage)\r\n// getListingId(url): string|null, // listing identity for the SPA watcher (was zpid)\r\n// scrape(): Listing, // the Listing contract (see browser/index.js)\r\n// config: { defaultPropertyType, cssFiles, spa? },\r\n// }\r\n\r\nimport { createAnalyzerState } from \"./createAnalyzerState.js\";\r\nimport { createExportOps } from \"./exportOps.js\";\r\nimport { createFinance } from \"./finance.js\";\r\nimport { createNav } from \"./nav.js\";\r\nimport { createPipeline } from \"./pipeline.js\";\r\nimport { createRender } from \"./render.js\";\r\nimport { createServices } from \"./services.js\";\r\n\r\nexport function createAnalyzer(adapter) {\r\n // Construction-validation: fail loud at wiring time, not deep in the pipeline.\r\n if (!adapter || typeof adapter !== \"object\") {\r\n throw new TypeError(\"createAnalyzer(adapter): adapter must be an object\");\r\n }\r\n for (const fn of [\"matches\", \"getListingId\", \"scrape\"]) {\r\n if (typeof adapter[fn] !== \"function\") {\r\n throw new TypeError(`createAnalyzer(adapter): adapter.${fn} must be a function`);\r\n }\r\n }\r\n const config = adapter.config || {};\r\n\r\n const ctx = createAnalyzerState({ defaultPropertyType: config.defaultPropertyType });\r\n\r\n // Wire the units in dependency order: render (leaf) -> finance/services -> export -> pipeline -> nav.\r\n const render = createRender({ ctx });\r\n const finance = createFinance({ ctx, adapter, render });\r\n const services = createServices({ ctx });\r\n const exportOps = createExportOps({\r\n ctx,\r\n ensureDebtLoaded: services.ensureDebtLoaded,\r\n scrapeAndApply: finance.scrapeAndApply,\r\n });\r\n const pipeline = createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services });\r\n const nav = createNav({ adapter, config, ctx, runPipeline: pipeline.runPipeline });\r\n\r\n return {\r\n ctx,\r\n createExportObject: exportOps.createExportObject,\r\n handleNavigation: nav.handleNavigation,\r\n runPipeline: pipeline.runPipeline,\r\n start: nav.start,\r\n };\r\n}\r\n\r\n// chrome.runtime.getURL resolves extension-relative CSS paths at runtime; guard it so the\r\n// engine is loadable under jsdom / Node (tests) where `chrome` is absent.\r\nfunction resolveCssUrls(cssFiles = []) {\r\n const hasChrome = typeof chrome !== \"undefined\" && chrome.runtime && typeof chrome.runtime.getURL === \"function\";\r\n return cssFiles.map((f) => (hasChrome ? chrome.runtime.getURL(f) : f));\r\n}\r\n"],"names":["createAnalyzer","adapter","TypeError","fn","config","ctx","createAnalyzerState","defaultPropertyType","render","createRender","finance","createFinance","services","createServices","exportOps","createExportOps","ensureDebtLoaded","scrapeAndApply","pipeline","createPipeline","resolveCssUrls","nav","createNav","runPipeline","createExportObject","handleNavigation","start","cssFiles","hasChrome","chrome","runtime","getURL","map","f"],"mappings":"2UA0BO,SAASA,eAAeC,GAE7B,IAAKA,GAA8B,iBAAZA,EACrB,MAAM,IAAIC,UAAU,sDAEtB,IAAK,MAAMC,IAAM,CAAC,UAAW,eAAgB,UAC3C,GAA2B,mBAAhBF,EAAQE,GACjB,MAAM,IAAID,UAAU,oCAAoCC,wBAG5D,MAAMC,EAASH,EAAQG,QAAU,GAE3BC,EAAMC,EAAoB,CAAEC,oBAAqBH,EAAOG,sBAGxDC,EAASC,EAAa,CAAEJ,QACxBK,EAAUC,EAAc,CAAEN,MAAKJ,UAASO,WACxCI,EAAWC,EAAe,CAAER,QAC5BS,EAAYC,EAAgB,CAChCV,MACAW,iBAAkBJ,EAASI,iBAC3BC,eAAgBP,EAAQO,iBAEpBC,EAAWC,EAAe,CAAElB,UAASG,SAAQC,MAAKS,YAAWJ,UAASF,SAAQY,8BAAgBR,aAC9FS,EAAMC,EAAU,CAAErB,UAASG,SAAQC,MAAKkB,YAAaL,EAASK,cAEpE,MAAO,CACLlB,MACAmB,mBAAoBV,EAAUU,mBAC9BC,iBAAkBJ,EAAII,iBACtBF,YAAaL,EAASK,YACtBG,MAAOL,EAAIK,MAEf,CAIA,SAASN,eAAeO,EAAW,IACjC,MAAMC,EAA8B,oBAAXC,QAA0BA,OAAOC,SAA4C,mBAA1BD,OAAOC,QAAQC,OAC3F,OAAOJ,EAASK,IAAKC,GAAOL,EAAYC,OAAOC,QAAQC,OAAOE,GAAKA,EACrE"}
@@ -1,2 +1,2 @@
1
- import{FINANCIAL_CONSTANTS as e}from"../../config/financial.js";import{PROPERTY_TYPES as t}from"../../config/property-types.js";function createAnalyzerState({defaultPropertyType:a=t.MULTIFAMILY}={}){const n={currentDownPaymentPercent:100*e.SELLER_FI_DOWN_PAYMENT,currentDSCRPercent:100*e.DEFAULT_DSCR_PERCENTAGE,currentSellerFiPercent:100*e.SELLER_FI_CARRY,currentPriceDiscount:0,originalPrice:null,priceWasDefaulted:!1,currentPropertyType:a,isUsingEstimatedCapRate:!1,currentEstimatedCapRate:100*e.DEFAULT_CAP_RATE,originalEstimatedCapRate:100*e.DEFAULT_CAP_RATE,originalCapRate:null,originalMultifamilyCapRate:null,capRateAlreadyDetermined:!1,capManuallySet:!1,baseNOI:null,cachedSTRData:null,cachedStrValue:null,cachedLoiData:{},cachedEquity:null,phoneButtonClicked:!1,equityLoadingStartTime:null,equitySource:"estimated",currentInterestRateType:"dscr_residential",numberOfUnits:4};return{state:n,updateState:e=>{Object.assign(n,e)},resetForNavigation:()=>{Object.assign(n,{baseNOI:null,cachedEquity:null,cachedLoiData:{},cachedSTRData:null,cachedStrValue:null,capManuallySet:!1,capRateAlreadyDetermined:!1,currentEstimatedCapRate:100*e.DEFAULT_CAP_RATE,currentPriceDiscount:0,isUsingEstimatedCapRate:!1,numberOfUnits:4,originalCapRate:null,originalEstimatedCapRate:100*e.DEFAULT_CAP_RATE,originalMultifamilyCapRate:null,originalPrice:null,phoneButtonClicked:!1,priceWasDefaulted:!1})}}}export{createAnalyzerState};
1
+ import{FINANCIAL_CONSTANTS as e}from"../../config/financial.js";import{PROPERTY_TYPES as a}from"../../config/property-types.js";function createAnalyzerState({defaultPropertyType:t=a.MULTIFAMILY}={}){const n={currentDownPaymentPercent:100*e.SELLER_FI_DOWN_PAYMENT,currentDSCRPercent:100*e.DEFAULT_DSCR_PERCENTAGE,currentSellerFiPercent:100*e.SELLER_FI_CARRY,currentPriceDiscount:0,originalPrice:null,priceWasDefaulted:!1,currentPropertyType:t,isUsingEstimatedCapRate:!1,currentEstimatedCapRate:100*e.DEFAULT_CAP_RATE,originalEstimatedCapRate:100*e.DEFAULT_CAP_RATE,originalCapRate:null,originalMultifamilyCapRate:null,capRateAlreadyDetermined:!1,capManuallySet:!1,baseNOI:null,cachedSTRData:null,cachedStrValue:null,cachedLoiData:{},cachedEquity:null,cachedDebtBalance:null,cachedMortgages:[],cachedDebtAddress:null,debtLoaded:!1,phoneButtonClicked:!1,equityLoadingStartTime:null,equitySource:"estimated",currentInterestRateType:"dscr_residential",numberOfUnits:4};return{state:n,updateState:e=>{Object.assign(n,e)},resetForNavigation:()=>{Object.assign(n,{baseNOI:null,cachedDebtAddress:null,cachedDebtBalance:null,cachedEquity:null,cachedLoiData:{},cachedMortgages:[],cachedSTRData:null,cachedStrValue:null,capManuallySet:!1,debtLoaded:!1,capRateAlreadyDetermined:!1,currentEstimatedCapRate:100*e.DEFAULT_CAP_RATE,currentPriceDiscount:0,isUsingEstimatedCapRate:!1,numberOfUnits:4,originalCapRate:null,originalEstimatedCapRate:100*e.DEFAULT_CAP_RATE,originalMultifamilyCapRate:null,originalPrice:null,phoneButtonClicked:!1,priceWasDefaulted:!1})}}}export{createAnalyzerState};
2
2
  //# sourceMappingURL=createAnalyzerState.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"createAnalyzerState.js","sources":["../../../src/browser/widget/createAnalyzerState.js"],"sourcesContent":["// Per-analyzer state factory. Replaces each repo's global-state.js singleton with an\r\n// isolated ctx ({ state, updateState, resetForNavigation }) the engine threads through its\r\n// pipeline. Two analyzers (or two test cases) get independent state — no shared module\r\n// singleton to bleed between them.\r\n//\r\n// The initial values and the resetForNavigation enumeration are lifted verbatim from\r\n// zillow-analyzer/state/global-state.js (the green copy). resetForNavigation clears the\r\n// per-listing memoized values that the SPA stale-state bug (H1) hinges on, and PRESERVES the\r\n// session-sticky user choices (property type, interest-rate tier, the down/DSCR/seller-FI\r\n// percentages) across a navigation.\r\n\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\nimport { PROPERTY_TYPES } from \"../../config/property-types.js\";\r\n\r\nexport function createAnalyzerState({ defaultPropertyType = PROPERTY_TYPES.MULTIFAMILY } = {}) {\r\n const state = {\r\n // Financial percentages (session-sticky — preserved across navigation)\r\n currentDownPaymentPercent: FINANCIAL_CONSTANTS.SELLER_FI_DOWN_PAYMENT * 100,\r\n currentDSCRPercent: FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n currentSellerFiPercent: FINANCIAL_CONSTANTS.SELLER_FI_CARRY * 100,\r\n\r\n // Price state\r\n currentPriceDiscount: 0,\r\n originalPrice: null,\r\n priceWasDefaulted: false,\r\n\r\n // Property and cap rate state. currentEstimatedCapRate is a WHOLE-NUMBER percent (the\r\n // calc divides it by 100, and parsed page caps like \"6%*\" land as 6) — so the default\r\n // estimate is DEFAULT_CAP_RATE * 100 = 5, NOT the decimal 0.05. Storing the decimal here\r\n // was the latent no-cap bug (NOI computed at 0.05%); the engine's cap path matches this.\r\n currentPropertyType: defaultPropertyType,\r\n isUsingEstimatedCapRate: false,\r\n currentEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n originalEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n originalCapRate: null,\r\n originalMultifamilyCapRate: null,\r\n capRateAlreadyDetermined: false,\r\n capManuallySet: false,\r\n\r\n // Cache state\r\n baseNOI: null,\r\n cachedSTRData: null,\r\n cachedStrValue: null,\r\n cachedLoiData: {},\r\n cachedEquity: null,\r\n\r\n // UI state\r\n phoneButtonClicked: false,\r\n equityLoadingStartTime: null,\r\n equitySource: \"estimated\",\r\n\r\n // Property details\r\n currentInterestRateType: \"dscr_residential\",\r\n numberOfUnits: 4,\r\n };\r\n\r\n const updateState = (updates) => {\r\n Object.assign(state, updates);\r\n };\r\n\r\n // Full enumerated reset for SPA navigation between listings (H1, CRITICAL).\r\n // Without clearing the memoized per-listing values, listing B silently shows listing A's\r\n // numbers (baseNOI/originalPrice/cap-rate are guarded by `if (!state.x)`, so stale\r\n // A-values survive). Session-sticky user choices are intentionally preserved.\r\n const resetForNavigation = () => {\r\n Object.assign(state, {\r\n baseNOI: null,\r\n cachedEquity: null,\r\n cachedLoiData: {},\r\n cachedSTRData: null,\r\n cachedStrValue: null,\r\n capManuallySet: false,\r\n capRateAlreadyDetermined: false,\r\n currentEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n currentPriceDiscount: 0,\r\n isUsingEstimatedCapRate: false,\r\n numberOfUnits: 4,\r\n originalCapRate: null,\r\n originalEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n originalMultifamilyCapRate: null,\r\n originalPrice: null,\r\n phoneButtonClicked: false,\r\n priceWasDefaulted: false,\r\n });\r\n };\r\n\r\n return { state, updateState, resetForNavigation };\r\n}\r\n"],"names":["createAnalyzerState","defaultPropertyType","PROPERTY_TYPES","MULTIFAMILY","state","currentDownPaymentPercent","FINANCIAL_CONSTANTS","SELLER_FI_DOWN_PAYMENT","currentDSCRPercent","DEFAULT_DSCR_PERCENTAGE","currentSellerFiPercent","SELLER_FI_CARRY","currentPriceDiscount","originalPrice","priceWasDefaulted","currentPropertyType","isUsingEstimatedCapRate","currentEstimatedCapRate","DEFAULT_CAP_RATE","originalEstimatedCapRate","originalCapRate","originalMultifamilyCapRate","capRateAlreadyDetermined","capManuallySet","baseNOI","cachedSTRData","cachedStrValue","cachedLoiData","cachedEquity","phoneButtonClicked","equityLoadingStartTime","equitySource","currentInterestRateType","numberOfUnits","updateState","updates","Object","assign","resetForNavigation"],"mappings":"gIAcO,SAASA,qBAAoBC,oBAAEA,EAAsBC,EAAeC,aAAgB,CAAA,GACzF,MAAMC,EAAQ,CAEZC,0BAAwE,IAA7CC,EAAoBC,uBAC/CC,mBAAkE,IAA9CF,EAAoBG,wBACxCC,uBAA8D,IAAtCJ,EAAoBK,gBAG5CC,qBAAsB,EACtBC,cAAe,KACfC,mBAAmB,EAMnBC,oBAAqBd,EACrBe,yBAAyB,EACzBC,wBAAgE,IAAvCX,EAAoBY,iBAC7CC,yBAAiE,IAAvCb,EAAoBY,iBAC9CE,gBAAiB,KACjBC,2BAA4B,KAC5BC,0BAA0B,EAC1BC,gBAAgB,EAGhBC,QAAS,KACTC,cAAe,KACfC,eAAgB,KAChBC,cAAe,CAAA,EACfC,aAAc,KAGdC,oBAAoB,EACpBC,uBAAwB,KACxBC,aAAc,YAGdC,wBAAyB,mBACzBC,cAAe,GAiCjB,MAAO,CAAE7B,QAAO8B,YA9BKC,IACnBC,OAAOC,OAAOjC,EAAO+B,IA6BMG,mBAtBF,KACzBF,OAAOC,OAAOjC,EAAO,CACnBoB,QAAS,KACTI,aAAc,KACdD,cAAe,CAAA,EACfF,cAAe,KACfC,eAAgB,KAChBH,gBAAgB,EAChBD,0BAA0B,EAC1BL,wBAAgE,IAAvCX,EAAoBY,iBAC7CN,qBAAsB,EACtBI,yBAAyB,EACzBiB,cAAe,EACfb,gBAAiB,KACjBD,yBAAiE,IAAvCb,EAAoBY,iBAC9CG,2BAA4B,KAC5BR,cAAe,KACfgB,oBAAoB,EACpBf,mBAAmB,KAKzB"}
1
+ {"version":3,"file":"createAnalyzerState.js","sources":["../../../src/browser/widget/createAnalyzerState.js"],"sourcesContent":["// Per-analyzer state factory. Replaces each repo's global-state.js singleton with an\r\n// isolated ctx ({ state, updateState, resetForNavigation }) the engine threads through its\r\n// pipeline. Two analyzers (or two test cases) get independent state — no shared module\r\n// singleton to bleed between them.\r\n//\r\n// The initial values and the resetForNavigation enumeration are lifted verbatim from\r\n// zillow-analyzer/state/global-state.js (the green copy). resetForNavigation clears the\r\n// per-listing memoized values that the SPA stale-state bug (H1) hinges on, and PRESERVES the\r\n// session-sticky user choices (property type, interest-rate tier, the down/DSCR/seller-FI\r\n// percentages) across a navigation.\r\n\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\nimport { PROPERTY_TYPES } from \"../../config/property-types.js\";\r\n\r\nexport function createAnalyzerState({ defaultPropertyType = PROPERTY_TYPES.MULTIFAMILY } = {}) {\r\n const state = {\r\n // Financial percentages (session-sticky — preserved across navigation)\r\n currentDownPaymentPercent: FINANCIAL_CONSTANTS.SELLER_FI_DOWN_PAYMENT * 100,\r\n currentDSCRPercent: FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n currentSellerFiPercent: FINANCIAL_CONSTANTS.SELLER_FI_CARRY * 100,\r\n\r\n // Price state\r\n currentPriceDiscount: 0,\r\n originalPrice: null,\r\n priceWasDefaulted: false,\r\n\r\n // Property and cap rate state. currentEstimatedCapRate is a WHOLE-NUMBER percent (the\r\n // calc divides it by 100, and parsed page caps like \"6%*\" land as 6) — so the default\r\n // estimate is DEFAULT_CAP_RATE * 100 = 5, NOT the decimal 0.05. Storing the decimal here\r\n // was the latent no-cap bug (NOI computed at 0.05%); the engine's cap path matches this.\r\n currentPropertyType: defaultPropertyType,\r\n isUsingEstimatedCapRate: false,\r\n currentEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n originalEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n originalCapRate: null,\r\n originalMultifamilyCapRate: null,\r\n capRateAlreadyDetermined: false,\r\n capManuallySet: false,\r\n\r\n // Cache state\r\n baseNOI: null,\r\n cachedSTRData: null,\r\n cachedStrValue: null,\r\n cachedLoiData: {},\r\n cachedEquity: null,\r\n\r\n // Debt state (scraped from the /debt service; equity is DERIVED from this vs current price,\r\n // so it recomputes whenever the user edits the price). cachedDebtBalance null + debtLoaded\r\n // true means the service returned no figure => equity falls back to 100% (\"estimated\").\r\n cachedDebtBalance: null,\r\n cachedMortgages: [],\r\n cachedDebtAddress: null,\r\n debtLoaded: false,\r\n\r\n // UI state\r\n phoneButtonClicked: false,\r\n equityLoadingStartTime: null,\r\n equitySource: \"estimated\",\r\n\r\n // Property details\r\n currentInterestRateType: \"dscr_residential\",\r\n numberOfUnits: 4,\r\n };\r\n\r\n const updateState = (updates) => {\r\n Object.assign(state, updates);\r\n };\r\n\r\n // Full enumerated reset for SPA navigation between listings (H1, CRITICAL).\r\n // Without clearing the memoized per-listing values, listing B silently shows listing A's\r\n // numbers (baseNOI/originalPrice/cap-rate are guarded by `if (!state.x)`, so stale\r\n // A-values survive). Session-sticky user choices are intentionally preserved.\r\n const resetForNavigation = () => {\r\n Object.assign(state, {\r\n baseNOI: null,\r\n cachedDebtAddress: null,\r\n cachedDebtBalance: null,\r\n cachedEquity: null,\r\n cachedLoiData: {},\r\n cachedMortgages: [],\r\n cachedSTRData: null,\r\n cachedStrValue: null,\r\n capManuallySet: false,\r\n debtLoaded: false,\r\n capRateAlreadyDetermined: false,\r\n currentEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n currentPriceDiscount: 0,\r\n isUsingEstimatedCapRate: false,\r\n numberOfUnits: 4,\r\n originalCapRate: null,\r\n originalEstimatedCapRate: FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100,\r\n originalMultifamilyCapRate: null,\r\n originalPrice: null,\r\n phoneButtonClicked: false,\r\n priceWasDefaulted: false,\r\n });\r\n };\r\n\r\n return { state, updateState, resetForNavigation };\r\n}\r\n"],"names":["createAnalyzerState","defaultPropertyType","PROPERTY_TYPES","MULTIFAMILY","state","currentDownPaymentPercent","FINANCIAL_CONSTANTS","SELLER_FI_DOWN_PAYMENT","currentDSCRPercent","DEFAULT_DSCR_PERCENTAGE","currentSellerFiPercent","SELLER_FI_CARRY","currentPriceDiscount","originalPrice","priceWasDefaulted","currentPropertyType","isUsingEstimatedCapRate","currentEstimatedCapRate","DEFAULT_CAP_RATE","originalEstimatedCapRate","originalCapRate","originalMultifamilyCapRate","capRateAlreadyDetermined","capManuallySet","baseNOI","cachedSTRData","cachedStrValue","cachedLoiData","cachedEquity","cachedDebtBalance","cachedMortgages","cachedDebtAddress","debtLoaded","phoneButtonClicked","equityLoadingStartTime","equitySource","currentInterestRateType","numberOfUnits","updateState","updates","Object","assign","resetForNavigation"],"mappings":"gIAcO,SAASA,qBAAoBC,oBAAEA,EAAsBC,EAAeC,aAAgB,CAAA,GACzF,MAAMC,EAAQ,CAEZC,0BAAwE,IAA7CC,EAAoBC,uBAC/CC,mBAAkE,IAA9CF,EAAoBG,wBACxCC,uBAA8D,IAAtCJ,EAAoBK,gBAG5CC,qBAAsB,EACtBC,cAAe,KACfC,mBAAmB,EAMnBC,oBAAqBd,EACrBe,yBAAyB,EACzBC,wBAAgE,IAAvCX,EAAoBY,iBAC7CC,yBAAiE,IAAvCb,EAAoBY,iBAC9CE,gBAAiB,KACjBC,2BAA4B,KAC5BC,0BAA0B,EAC1BC,gBAAgB,EAGhBC,QAAS,KACTC,cAAe,KACfC,eAAgB,KAChBC,cAAe,CAAA,EACfC,aAAc,KAKdC,kBAAmB,KACnBC,gBAAiB,GACjBC,kBAAmB,KACnBC,YAAY,EAGZC,oBAAoB,EACpBC,uBAAwB,KACxBC,aAAc,YAGdC,wBAAyB,mBACzBC,cAAe,GAqCjB,MAAO,CAAEjC,QAAOkC,YAlCKC,IACnBC,OAAOC,OAAOrC,EAAOmC,IAiCMG,mBA1BF,KACzBF,OAAOC,OAAOrC,EAAO,CACnBoB,QAAS,KACTO,kBAAmB,KACnBF,kBAAmB,KACnBD,aAAc,KACdD,cAAe,CAAA,EACfG,gBAAiB,GACjBL,cAAe,KACfC,eAAgB,KAChBH,gBAAgB,EAChBS,YAAY,EACZV,0BAA0B,EAC1BL,wBAAgE,IAAvCX,EAAoBY,iBAC7CN,qBAAsB,EACtBI,yBAAyB,EACzBqB,cAAe,EACfjB,gBAAiB,KACjBD,yBAAiE,IAAvCb,EAAoBY,iBAC9CG,2BAA4B,KAC5BR,cAAe,KACfoB,oBAAoB,EACpBnB,mBAAmB,KAKzB"}
@@ -1,2 +1,2 @@
1
- import{createExportObjectCore as e}from"../../export/export-logic.js";import{BUSINESS_CONSTANTS as t}from"../../config/business.js";function createExportOps({ctx:r,scrapeAndApply:n,ensureEquityLoaded:o}){const{state:c}=r;async function createExportObject(){const t=n();if(!t)return console.error("❌ Malformed listing data — refusing to export"),null;const r=t.name&&"Property Details"!==t.name?t.name:"",a=await o(r);return e(t,{cachedEquity:a,currentDownPaymentPercent:c.currentDownPaymentPercent,currentInterestRateType:c.currentInterestRateType,currentPriceDiscount:c.currentPriceDiscount,currentPropertyType:c.currentPropertyType,equitySource:c.equitySource,isUsingEstimatedCapRate:c.isUsingEstimatedCapRate,noi:c.baseNOI,numberOfUnits:c.numberOfUnits,priceWasDefaulted:c.priceWasDefaulted,windowLocation:window.location.href})}async function createExportUrl(){try{const e=await createExportObject();if(!e)return null;const r=new URLSearchParams;return Object.entries(e).forEach(([e,t])=>{null!=t&&"Loading..."!==t&&"Not found"!==t&&r.append(e,t)}),`${t.EXPORT_URL_BASE}?${r.toString()}`}catch(e){return console.error("❌ Error creating export URL:",e),null}}return{createExportObject:createExportObject,createExportUrl:createExportUrl,handleExportClick:async function(){try{const e=document.getElementById("ln-export-btn");if(c.priceWasDefaulted){if(console.warn("⚠️ Export blocked: no real price found for this listing"),e){const t=e.textContent;e.textContent="No price — can't export",e.disabled=!0,setTimeout(()=>{e.textContent=t,e.disabled=!1},2e3)}return}if(e){const t=e.textContent;e.textContent="Loading...",e.disabled=!0,setTimeout(()=>{e.textContent=t,e.disabled=!1},2e3)}const t=await createExportUrl();t?window.open(t,"_blank"):(console.error("❌ Failed to create export URL"),alert("Error creating export URL. Check console for details."))}catch(e){console.error("❌ Error in handleExportClick:",e),alert(`Export failed: ${e.message}`)}}}}export{createExportOps};
1
+ import{createExportObjectCore as e}from"../../export/export-logic.js";import{BUSINESS_CONSTANTS as t}from"../../config/business.js";function createExportOps({ctx:r,scrapeAndApply:n,ensureDebtLoaded:o}){const{state:a}=r;async function createExportObject(){const t=n();if(!t)return console.error("❌ Malformed listing data — refusing to export"),null;const r=t.name&&"Property Details"!==t.name?t.name:"",c=await o(r);return e(t,{currentDownPaymentPercent:a.currentDownPaymentPercent,currentInterestRateType:a.currentInterestRateType,currentMortgages:c.mortgages,currentPriceDiscount:a.currentPriceDiscount,currentPropertyType:a.currentPropertyType,equitySource:a.equitySource,estimatedMortgageBalance:c.balance,isUsingEstimatedCapRate:a.isUsingEstimatedCapRate,noi:a.baseNOI,numberOfUnits:a.numberOfUnits,priceWasDefaulted:a.priceWasDefaulted,windowLocation:window.location.href})}async function createExportUrl(){try{const e=await createExportObject();if(!e)return null;const r=new URLSearchParams;return Object.entries(e).forEach(([e,t])=>{null!=t&&"Loading..."!==t&&"Not found"!==t&&r.append(e,t)}),`${t.EXPORT_URL_BASE}?${r.toString()}`}catch(e){return console.error("❌ Error creating export URL:",e),null}}return{createExportObject:createExportObject,createExportUrl:createExportUrl,handleExportClick:async function(){try{const e=document.getElementById("ln-export-btn");if(a.priceWasDefaulted){if(console.warn("⚠️ Export blocked: no real price found for this listing"),e){const t=e.textContent;e.textContent="No price — can't export",e.disabled=!0,setTimeout(()=>{e.textContent=t,e.disabled=!1},2e3)}return}if(e){const t=e.textContent;e.textContent="Loading...",e.disabled=!0,setTimeout(()=>{e.textContent=t,e.disabled=!1},2e3)}const t=await createExportUrl();t?window.open(t,"_blank"):(console.error("❌ Failed to create export URL"),alert("Error creating export URL. Check console for details."))}catch(e){console.error("❌ Error in handleExportClick:",e),alert(`Export failed: ${e.message}`)}}}}export{createExportOps};
2
2
  //# sourceMappingURL=exportOps.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"exportOps.js","sources":["../../../src/browser/widget/exportOps.js"],"sourcesContent":["// Export unit: builds the export object (re-scraping for a fresh priceWasDefaulted flag),\r\n// turns it into the dashboard import URL, and wires the export-button click (the defaulted-price\r\n// refusal + the transient button states). Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createExportObjectCore } from \"../../export/export-logic.js\";\r\nimport { BUSINESS_CONSTANTS } from \"../../config/business.js\";\r\n\r\nexport function createExportOps({ ctx, scrapeAndApply, ensureEquityLoaded }) {\r\n const { state } = ctx;\r\n\r\n async function createExportObject() {\r\n // Re-scrape so priceWasDefaulted reflects the page right now (never trust a stale flag).\r\n const data = scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — refusing to export\");\r\n return null;\r\n }\r\n\r\n const addressForEquity = data.name && data.name !== \"Property Details\" ? data.name : \"\";\r\n const cachedEquity = await ensureEquityLoaded(addressForEquity);\r\n\r\n return createExportObjectCore(data, {\r\n cachedEquity,\r\n currentDownPaymentPercent: state.currentDownPaymentPercent,\r\n currentInterestRateType: state.currentInterestRateType,\r\n currentPriceDiscount: state.currentPriceDiscount,\r\n currentPropertyType: state.currentPropertyType,\r\n equitySource: state.equitySource,\r\n isUsingEstimatedCapRate: state.isUsingEstimatedCapRate,\r\n noi: state.baseNOI,\r\n numberOfUnits: state.numberOfUnits,\r\n priceWasDefaulted: state.priceWasDefaulted,\r\n windowLocation: window.location.href,\r\n });\r\n }\r\n\r\n async function createExportUrl() {\r\n try {\r\n const exportData = await createExportObject();\r\n if (!exportData) return null;\r\n const params = new URLSearchParams();\r\n Object.entries(exportData).forEach(([key, value]) => {\r\n if (value !== null && value !== undefined && value !== \"Loading...\" && value !== \"Not found\") {\r\n params.append(key, value);\r\n }\r\n });\r\n return `${BUSINESS_CONSTANTS.EXPORT_URL_BASE}?${params.toString()}`;\r\n } catch (error) {\r\n console.error(\"❌ Error creating export URL:\", error);\r\n return null;\r\n }\r\n }\r\n\r\n async function handleExportClick() {\r\n try {\r\n const exportBtn = document.getElementById(\"ln-export-btn\");\r\n\r\n if (state.priceWasDefaulted) {\r\n console.warn(\"⚠️ Export blocked: no real price found for this listing\");\r\n if (exportBtn) {\r\n const originalText = exportBtn.textContent;\r\n exportBtn.textContent = \"No price — can't export\";\r\n exportBtn.disabled = true;\r\n setTimeout(() => {\r\n exportBtn.textContent = originalText;\r\n exportBtn.disabled = false;\r\n }, 2000);\r\n }\r\n return;\r\n }\r\n\r\n if (exportBtn) {\r\n const originalText = exportBtn.textContent;\r\n exportBtn.textContent = \"Loading...\";\r\n exportBtn.disabled = true;\r\n setTimeout(() => {\r\n exportBtn.textContent = originalText;\r\n exportBtn.disabled = false;\r\n }, 2000);\r\n }\r\n\r\n const exportUrl = await createExportUrl();\r\n if (exportUrl) {\r\n window.open(exportUrl, \"_blank\");\r\n } else {\r\n console.error(\"❌ Failed to create export URL\");\r\n alert(\"Error creating export URL. Check console for details.\");\r\n }\r\n } catch (error) {\r\n console.error(\"❌ Error in handleExportClick:\", error);\r\n alert(`Export failed: ${error.message}`);\r\n }\r\n }\r\n\r\n return { createExportObject, createExportUrl, handleExportClick };\r\n}\r\n"],"names":["createExportOps","ctx","scrapeAndApply","ensureEquityLoaded","state","async","createExportObject","data","console","error","addressForEquity","name","cachedEquity","createExportObjectCore","currentDownPaymentPercent","currentInterestRateType","currentPriceDiscount","currentPropertyType","equitySource","isUsingEstimatedCapRate","noi","baseNOI","numberOfUnits","priceWasDefaulted","windowLocation","window","location","href","createExportUrl","exportData","params","URLSearchParams","Object","entries","forEach","key","value","append","BUSINESS_CONSTANTS","EXPORT_URL_BASE","toString","handleExportClick","exportBtn","document","getElementById","warn","originalText","textContent","disabled","setTimeout","exportUrl","open","alert","message"],"mappings":"oIAOO,SAASA,iBAAgBC,IAAEA,EAAGC,eAAEA,EAAcC,mBAAEA,IACrD,MAAMC,MAAEA,GAAUH,EAElBI,eAAeC,qBAEb,MAAMC,EAAOL,IACb,IAAKK,EAEH,OADAC,QAAQC,MAAM,iDACP,KAGT,MAAMC,EAAmBH,EAAKI,MAAsB,qBAAdJ,EAAKI,KAA8BJ,EAAKI,KAAO,GAC/EC,QAAqBT,EAAmBO,GAE9C,OAAOG,EAAuBN,EAAM,CAClCK,eACAE,0BAA2BV,EAAMU,0BACjCC,wBAAyBX,EAAMW,wBAC/BC,qBAAsBZ,EAAMY,qBAC5BC,oBAAqBb,EAAMa,oBAC3BC,aAAcd,EAAMc,aACpBC,wBAAyBf,EAAMe,wBAC/BC,IAAKhB,EAAMiB,QACXC,cAAelB,EAAMkB,cACrBC,kBAAmBnB,EAAMmB,kBACzBC,eAAgBC,OAAOC,SAASC,MAEpC,CAEAtB,eAAeuB,kBACb,IACE,MAAMC,QAAmBvB,qBACzB,IAAKuB,EAAY,OAAO,KACxB,MAAMC,EAAS,IAAIC,gBAMnB,OALAC,OAAOC,QAAQJ,GAAYK,QAAQ,EAAEC,EAAKC,MACpCA,SAAmD,eAAVA,GAAoC,cAAVA,GACrEN,EAAOO,OAAOF,EAAKC,KAGhB,GAAGE,EAAmBC,mBAAmBT,EAAOU,YACzD,CAAE,MAAO/B,GAEP,OADAD,QAAQC,MAAM,+BAAgCA,GACvC,IACT,CACF,CA2CA,MAAO,CAAEH,sCAAoBsB,gCAAiBa,kBAzC9CpC,iBACE,IACE,MAAMqC,EAAYC,SAASC,eAAe,iBAE1C,GAAIxC,EAAMmB,kBAAmB,CAE3B,GADAf,QAAQqC,KAAK,2DACTH,EAAW,CACb,MAAMI,EAAeJ,EAAUK,YAC/BL,EAAUK,YAAc,0BACxBL,EAAUM,UAAW,EACrBC,WAAW,KACTP,EAAUK,YAAcD,EACxBJ,EAAUM,UAAW,GACpB,IACL,CACA,MACF,CAEA,GAAIN,EAAW,CACb,MAAMI,EAAeJ,EAAUK,YAC/BL,EAAUK,YAAc,aACxBL,EAAUM,UAAW,EACrBC,WAAW,KACTP,EAAUK,YAAcD,EACxBJ,EAAUM,UAAW,GACpB,IACL,CAEA,MAAME,QAAkBtB,kBACpBsB,EACFzB,OAAO0B,KAAKD,EAAW,WAEvB1C,QAAQC,MAAM,iCACd2C,MAAM,yDAEV,CAAE,MAAO3C,GACPD,QAAQC,MAAM,gCAAiCA,GAC/C2C,MAAM,kBAAkB3C,EAAM4C,UAChC,CACF,EAGF"}
1
+ {"version":3,"file":"exportOps.js","sources":["../../../src/browser/widget/exportOps.js"],"sourcesContent":["// Export unit: builds the export object (re-scraping for a fresh priceWasDefaulted flag),\r\n// turns it into the dashboard import URL, and wires the export-button click (the defaulted-price\r\n// refusal + the transient button states). Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createExportObjectCore } from \"../../export/export-logic.js\";\r\nimport { BUSINESS_CONSTANTS } from \"../../config/business.js\";\r\n\r\nexport function createExportOps({ ctx, scrapeAndApply, ensureDebtLoaded }) {\r\n const { state } = ctx;\r\n\r\n async function createExportObject() {\r\n // Re-scrape so priceWasDefaulted reflects the page right now (never trust a stale flag).\r\n const data = scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — refusing to export\");\r\n return null;\r\n }\r\n\r\n const addressForDebt = data.name && data.name !== \"Property Details\" ? data.name : \"\";\r\n const debt = await ensureDebtLoaded(addressForDebt);\r\n\r\n return createExportObjectCore(data, {\r\n currentDownPaymentPercent: state.currentDownPaymentPercent,\r\n currentInterestRateType: state.currentInterestRateType,\r\n currentMortgages: debt.mortgages,\r\n currentPriceDiscount: state.currentPriceDiscount,\r\n currentPropertyType: state.currentPropertyType,\r\n equitySource: state.equitySource,\r\n estimatedMortgageBalance: debt.balance,\r\n isUsingEstimatedCapRate: state.isUsingEstimatedCapRate,\r\n noi: state.baseNOI,\r\n numberOfUnits: state.numberOfUnits,\r\n priceWasDefaulted: state.priceWasDefaulted,\r\n windowLocation: window.location.href,\r\n });\r\n }\r\n\r\n async function createExportUrl() {\r\n try {\r\n const exportData = await createExportObject();\r\n if (!exportData) return null;\r\n const params = new URLSearchParams();\r\n Object.entries(exportData).forEach(([key, value]) => {\r\n if (value !== null && value !== undefined && value !== \"Loading...\" && value !== \"Not found\") {\r\n params.append(key, value);\r\n }\r\n });\r\n return `${BUSINESS_CONSTANTS.EXPORT_URL_BASE}?${params.toString()}`;\r\n } catch (error) {\r\n console.error(\"❌ Error creating export URL:\", error);\r\n return null;\r\n }\r\n }\r\n\r\n async function handleExportClick() {\r\n try {\r\n const exportBtn = document.getElementById(\"ln-export-btn\");\r\n\r\n if (state.priceWasDefaulted) {\r\n console.warn(\"⚠️ Export blocked: no real price found for this listing\");\r\n if (exportBtn) {\r\n const originalText = exportBtn.textContent;\r\n exportBtn.textContent = \"No price — can't export\";\r\n exportBtn.disabled = true;\r\n setTimeout(() => {\r\n exportBtn.textContent = originalText;\r\n exportBtn.disabled = false;\r\n }, 2000);\r\n }\r\n return;\r\n }\r\n\r\n if (exportBtn) {\r\n const originalText = exportBtn.textContent;\r\n exportBtn.textContent = \"Loading...\";\r\n exportBtn.disabled = true;\r\n setTimeout(() => {\r\n exportBtn.textContent = originalText;\r\n exportBtn.disabled = false;\r\n }, 2000);\r\n }\r\n\r\n const exportUrl = await createExportUrl();\r\n if (exportUrl) {\r\n window.open(exportUrl, \"_blank\");\r\n } else {\r\n console.error(\"❌ Failed to create export URL\");\r\n alert(\"Error creating export URL. Check console for details.\");\r\n }\r\n } catch (error) {\r\n console.error(\"❌ Error in handleExportClick:\", error);\r\n alert(`Export failed: ${error.message}`);\r\n }\r\n }\r\n\r\n return { createExportObject, createExportUrl, handleExportClick };\r\n}\r\n"],"names":["createExportOps","ctx","scrapeAndApply","ensureDebtLoaded","state","async","createExportObject","data","console","error","addressForDebt","name","debt","createExportObjectCore","currentDownPaymentPercent","currentInterestRateType","currentMortgages","mortgages","currentPriceDiscount","currentPropertyType","equitySource","estimatedMortgageBalance","balance","isUsingEstimatedCapRate","noi","baseNOI","numberOfUnits","priceWasDefaulted","windowLocation","window","location","href","createExportUrl","exportData","params","URLSearchParams","Object","entries","forEach","key","value","append","BUSINESS_CONSTANTS","EXPORT_URL_BASE","toString","handleExportClick","exportBtn","document","getElementById","warn","originalText","textContent","disabled","setTimeout","exportUrl","open","alert","message"],"mappings":"oIAOO,SAASA,iBAAgBC,IAAEA,EAAGC,eAAEA,EAAcC,iBAAEA,IACrD,MAAMC,MAAEA,GAAUH,EAElBI,eAAeC,qBAEb,MAAMC,EAAOL,IACb,IAAKK,EAEH,OADAC,QAAQC,MAAM,iDACP,KAGT,MAAMC,EAAiBH,EAAKI,MAAsB,qBAAdJ,EAAKI,KAA8BJ,EAAKI,KAAO,GAC7EC,QAAaT,EAAiBO,GAEpC,OAAOG,EAAuBN,EAAM,CAClCO,0BAA2BV,EAAMU,0BACjCC,wBAAyBX,EAAMW,wBAC/BC,iBAAkBJ,EAAKK,UACvBC,qBAAsBd,EAAMc,qBAC5BC,oBAAqBf,EAAMe,oBAC3BC,aAAchB,EAAMgB,aACpBC,yBAA0BT,EAAKU,QAC/BC,wBAAyBnB,EAAMmB,wBAC/BC,IAAKpB,EAAMqB,QACXC,cAAetB,EAAMsB,cACrBC,kBAAmBvB,EAAMuB,kBACzBC,eAAgBC,OAAOC,SAASC,MAEpC,CAEA1B,eAAe2B,kBACb,IACE,MAAMC,QAAmB3B,qBACzB,IAAK2B,EAAY,OAAO,KACxB,MAAMC,EAAS,IAAIC,gBAMnB,OALAC,OAAOC,QAAQJ,GAAYK,QAAQ,EAAEC,EAAKC,MACpCA,SAAmD,eAAVA,GAAoC,cAAVA,GACrEN,EAAOO,OAAOF,EAAKC,KAGhB,GAAGE,EAAmBC,mBAAmBT,EAAOU,YACzD,CAAE,MAAOnC,GAEP,OADAD,QAAQC,MAAM,+BAAgCA,GACvC,IACT,CACF,CA2CA,MAAO,CAAEH,sCAAoB0B,gCAAiBa,kBAzC9CxC,iBACE,IACE,MAAMyC,EAAYC,SAASC,eAAe,iBAE1C,GAAI5C,EAAMuB,kBAAmB,CAE3B,GADAnB,QAAQyC,KAAK,2DACTH,EAAW,CACb,MAAMI,EAAeJ,EAAUK,YAC/BL,EAAUK,YAAc,0BACxBL,EAAUM,UAAW,EACrBC,WAAW,KACTP,EAAUK,YAAcD,EACxBJ,EAAUM,UAAW,GACpB,IACL,CACA,MACF,CAEA,GAAIN,EAAW,CACb,MAAMI,EAAeJ,EAAUK,YAC/BL,EAAUK,YAAc,aACxBL,EAAUM,UAAW,EACrBC,WAAW,KACTP,EAAUK,YAAcD,EACxBJ,EAAUM,UAAW,GACpB,IACL,CAEA,MAAME,QAAkBtB,kBACpBsB,EACFzB,OAAO0B,KAAKD,EAAW,WAEvB9C,QAAQC,MAAM,iCACd+C,MAAM,yDAEV,CAAE,MAAO/C,GACPD,QAAQC,MAAM,gCAAiCA,GAC/C+C,MAAM,kBAAkB/C,EAAMgD,UAChC,CACF,EAGF"}
@@ -1,2 +1,2 @@
1
- import{computeManualOverrideNOI as t,resolveCapRateProvenance as a}from"../financial/capRate.js";import{calculateFinancials as e}from"../financial/calculateFinancials.js";import{FINANCIAL_CONSTANTS as i}from"../../config/financial.js";import{normalizeWhitespace as n}from"../../formatting/text.js";const r=["name","price","capRate","contact","phone","listingDate"];function isValidListingShape(t){return!(!t||"object"!=typeof t)&&r.every(a=>"string"==typeof t[a])}function createFinance({ctx:p,adapter:c,render:l}){const{state:o,updateState:s}=p;function applyCapRate(t){const{isDefault:e,estimated:n,num:r,displayCap:p}=a(t.capRate,100*i.DEFAULT_CAP_RATE);if(t.capRate=p,e)return o.originalCapRate||s({originalCapRate:p}),o.originalMultifamilyCapRate||s({originalMultifamilyCapRate:`${r}%`}),void s({currentEstimatedCapRate:r,isUsingEstimatedCapRate:!0,originalEstimatedCapRate:r});s({isUsingEstimatedCapRate:n}),n&&null!==r&&s({currentEstimatedCapRate:r}),o.originalCapRate||s({originalCapRate:p}),o.originalMultifamilyCapRate||null===r||s({originalMultifamilyCapRate:`${r}%`})}return{applyCapRate:applyCapRate,handlePropertyTypeChange:function(){const t=document.getElementById("ln-property-type");if(!t)return;const a=t.value;return s({currentPropertyType:a}),"str"!==a&&s({cachedSTRData:null}),s({baseNOI:null}),a},recalculateFinancials:async function(){const a=document.getElementById("prop-price");if(document.getElementById("prop-name"),!a)return;const i=l.getCurrentPrice()||a.textContent;let n;if(o.isUsingEstimatedCapRate)n=`${o.currentEstimatedCapRate}%*`;else{const t=document.getElementById("prop-cap");n=t?t.textContent:"8%"}if("str"===o.currentPropertyType&&s({cachedSTRData:null}),o.capManuallySet){const a=t(o.originalPrice||i,o.currentEstimatedCapRate);null!=a&&s({baseNOI:a})}const r=await e(p,i,n,o.currentPropertyType);l.applyFinancials(r),l.updateActiveCapDisplay()},scrapeAndApply:function(){const t=c.scrape();if(!isValidListingShape(t))return null;for(const a of r)t[a]=n(t[a]);const a=t.priceWasDefaulted??"Not found"===t.price;return s({priceWasDefaulted:a}),a||o.originalPrice||s({originalPrice:t.originalPrice??t.price}),applyCapRate(t),t}}}export{createFinance,isValidListingShape};
1
+ import{computeManualOverrideNOI as t,resolveCapRateProvenance as a}from"../financial/capRate.js";import{calculateFinancials as e}from"../financial/calculateFinancials.js";import{FINANCIAL_CONSTANTS as i}from"../../config/financial.js";import{normalizeWhitespace as n}from"../../formatting/text.js";const r=["name","price","capRate","contact","phone","listingDate"];function isValidListingShape(t){return!(!t||"object"!=typeof t)&&r.every(a=>"string"==typeof t[a])}function createFinance({ctx:p,adapter:c,render:l}){const{state:o,updateState:s}=p;function applyCapRate(t){const{isDefault:e,estimated:n,num:r,displayCap:p}=a(t.capRate,100*i.DEFAULT_CAP_RATE);if(t.capRate=p,e)return o.originalCapRate||s({originalCapRate:p}),o.originalMultifamilyCapRate||s({originalMultifamilyCapRate:`${r}%`}),void s({currentEstimatedCapRate:r,isUsingEstimatedCapRate:!0,originalEstimatedCapRate:r});s({isUsingEstimatedCapRate:n}),n&&null!==r&&s({currentEstimatedCapRate:r}),o.originalCapRate||s({originalCapRate:p}),o.originalMultifamilyCapRate||null===r||s({originalMultifamilyCapRate:`${r}%`})}return{applyCapRate:applyCapRate,handlePropertyTypeChange:function(){const t=document.getElementById("ln-property-type");if(!t)return;const a=t.value;return s({currentPropertyType:a}),"str"!==a&&s({cachedSTRData:null}),s({baseNOI:null}),a},recalculateFinancials:async function(){const a=document.getElementById("prop-price");if(document.getElementById("prop-name"),!a)return;const i=l.getCurrentPrice()||a.textContent;let n;if(o.isUsingEstimatedCapRate)n=`${o.currentEstimatedCapRate}%*`;else{const t=document.getElementById("prop-cap");n=t?t.textContent:"8%"}if("str"===o.currentPropertyType&&s({cachedSTRData:null}),o.capManuallySet){const a=t(o.originalPrice||i,o.currentEstimatedCapRate);null!=a&&s({baseNOI:a})}const r=await e(p,i,n,o.currentPropertyType);l.applyFinancials(r),l.updateActiveCapDisplay(),l.updateEquityDisplay()},scrapeAndApply:function(){const t=c.scrape();if(!isValidListingShape(t))return null;for(const a of r)t[a]=n(t[a]);const a=t.priceWasDefaulted??"Not found"===t.price;return s({priceWasDefaulted:a}),a||o.originalPrice||s({originalPrice:t.originalPrice??t.price}),applyCapRate(t),t}}}export{createFinance,isValidListingShape};
2
2
  //# sourceMappingURL=finance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"finance.js","sources":["../../../src/browser/widget/finance.js"],"sourcesContent":["// Finance unit (orchestration): applies cap-rate provenance to ctx, applies the scrape-derived\r\n// state, and recomputes the financial metrics. The pure rules live in financial/capRate.js; this\r\n// module is the thin state/DOM glue around them. Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { computeManualOverrideNOI, resolveCapRateProvenance } from \"../financial/capRate.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\nimport { normalizeWhitespace } from \"../../formatting/text.js\";\r\n\r\n// The Listing fields every scraper must return as strings (default \"Not found\"). A missing or\r\n// non-string field means the scraper broke; the engine refuses to render/export rather than\r\n// letting `undefined` flow into the NOI/COCR math and silently paint wrong numbers (fail-loud).\r\nconst LISTING_CONTRACT_FIELDS = [\"name\", \"price\", \"capRate\", \"contact\", \"phone\", \"listingDate\"];\r\n\r\nexport function isValidListingShape(data) {\r\n if (!data || typeof data !== \"object\") return false;\r\n return LISTING_CONTRACT_FIELDS.every((field) => typeof data[field] === \"string\");\r\n}\r\n\r\nexport function createFinance({ ctx, adapter, render }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Resolve the cap-rate provenance from the scraped string and write the cap state the\r\n // financial calc + discount handlers read. The default (DEFAULT_CAP_RATE * 100 = 5,\r\n // whole-number percent) fixes the latent no-cap bug where the decimal 0.05 was stored\r\n // and then divided by 100, computing NOI at 0.05%.\r\n function applyCapRate(listing) {\r\n const { isDefault, estimated, num, displayCap } = resolveCapRateProvenance(\r\n listing.capRate,\r\n FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100\r\n );\r\n listing.capRate = displayCap;\r\n\r\n if (isDefault) {\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate) updateState({ originalMultifamilyCapRate: `${num}%` });\r\n updateState({\r\n currentEstimatedCapRate: num,\r\n isUsingEstimatedCapRate: true,\r\n originalEstimatedCapRate: num,\r\n });\r\n return;\r\n }\r\n\r\n updateState({ isUsingEstimatedCapRate: estimated });\r\n if (estimated && num !== null) updateState({ currentEstimatedCapRate: num });\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate && num !== null) {\r\n updateState({ originalMultifamilyCapRate: `${num}%` });\r\n }\r\n }\r\n\r\n // Scrape the page and apply the universal scrape-derived state (was extractData's side\r\n // effects). Returns the resolved listing (capRate normalized to a display string) or null\r\n // when the scrape is malformed.\r\n function scrapeAndApply() {\r\n const listing = adapter.scrape();\r\n if (!isValidListingShape(listing)) return null;\r\n\r\n // Normalize whitespace on every contract string field centrally, so adapters stay pure\r\n // scrapers and no consumer (panel or export) ever sees the interior newlines/tabs that\r\n // site markup splits text across (e.g. a broker name on two lines). \"Not found\" is\r\n // unchanged. This is the single enforcement point for the data contract's \"normalize text\".\r\n for (const field of LISTING_CONTRACT_FIELDS) {\r\n listing[field] = normalizeWhitespace(listing[field]);\r\n }\r\n\r\n const priceWasDefaulted = listing.priceWasDefaulted ?? (listing.price === \"Not found\");\r\n updateState({ priceWasDefaulted });\r\n\r\n if (!priceWasDefaulted && !state.originalPrice) {\r\n updateState({ originalPrice: listing.originalPrice ?? listing.price });\r\n }\r\n\r\n applyCapRate(listing);\r\n return listing;\r\n }\r\n\r\n function handlePropertyTypeChange() {\r\n const dropdown = document.getElementById(\"ln-property-type\");\r\n if (!dropdown) return;\r\n const newType = dropdown.value;\r\n updateState({ currentPropertyType: newType });\r\n if (newType !== \"str\") updateState({ cachedSTRData: null });\r\n updateState({ baseNOI: null });\r\n return newType;\r\n }\r\n\r\n async function recalculateFinancials() {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const addressElement = document.getElementById(\"prop-name\");\r\n if (!priceElement) return;\r\n\r\n const priceText = render.getCurrentPrice() || priceElement.textContent;\r\n const address = addressElement?.textContent || \"\";\r\n let capRateText;\r\n if (state.isUsingEstimatedCapRate) {\r\n capRateText = `${state.currentEstimatedCapRate}%*`;\r\n } else {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n capRateText = capElement ? capElement.textContent : \"8%\";\r\n }\r\n\r\n if (state.currentPropertyType === \"str\") updateState({ cachedSTRData: null });\r\n\r\n // Manual cap override: clicking the cap rate sets NOI = original price x cap for EVERY type\r\n // (analyst intent), so the active cap moves with the click even for STR/assisted whose NOI\r\n // is otherwise the type estimate / bedroom value. Pre-seed baseNOI so calculateFinancials\r\n // uses it instead of recomputing from the type model.\r\n if (state.capManuallySet) {\r\n const noi = computeManualOverrideNOI(state.originalPrice || priceText, state.currentEstimatedCapRate);\r\n if (noi != null) updateState({ baseNOI: noi });\r\n }\r\n\r\n const financials = await calculateFinancials(ctx, priceText, capRateText, state.currentPropertyType, address);\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n }\r\n\r\n return { applyCapRate, handlePropertyTypeChange, recalculateFinancials, scrapeAndApply };\r\n}\r\n"],"names":["LISTING_CONTRACT_FIELDS","isValidListingShape","data","every","field","createFinance","ctx","adapter","render","state","updateState","applyCapRate","listing","isDefault","estimated","num","displayCap","resolveCapRateProvenance","capRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","originalCapRate","originalMultifamilyCapRate","currentEstimatedCapRate","isUsingEstimatedCapRate","originalEstimatedCapRate","handlePropertyTypeChange","dropdown","document","getElementById","newType","value","currentPropertyType","cachedSTRData","baseNOI","recalculateFinancials","async","priceElement","priceText","getCurrentPrice","textContent","capRateText","capElement","capManuallySet","noi","computeManualOverrideNOI","originalPrice","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","scrapeAndApply","scrape","normalizeWhitespace","priceWasDefaulted","price"],"mappings":"0SAYA,MAAMA,EAA0B,CAAC,OAAQ,QAAS,UAAW,UAAW,QAAS,eAE1E,SAASC,oBAAoBC,GAClC,SAAKA,GAAwB,iBAATA,IACbF,EAAwBG,MAAOC,GAAiC,iBAAhBF,EAAKE,GAC9D,CAEO,SAASC,eAAcC,IAAEA,EAAGC,QAAEA,EAAOC,OAAEA,IAC5C,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBJ,EAM/B,SAASK,aAAaC,GACpB,MAAMC,UAAEA,EAASC,UAAEA,EAASC,IAAEA,EAAGC,WAAEA,GAAeC,EAChDL,EAAQM,QAC+B,IAAvCC,EAAoBC,kBAItB,GAFAR,EAAQM,QAAUF,EAEdH,EAQF,OAPKJ,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAA4BZ,EAAY,CAAEY,2BAA4B,GAAGP,YACpFL,EAAY,CACVa,wBAAyBR,EACzBS,yBAAyB,EACzBC,yBAA0BV,IAK9BL,EAAY,CAAEc,wBAAyBV,IACnCA,GAAqB,OAARC,GAAcL,EAAY,CAAEa,wBAAyBR,IACjEN,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAAsC,OAARP,GACvCL,EAAY,CAAEY,2BAA4B,GAAGP,MAEjD,CAqEA,MAAO,CAAEJ,0BAAce,yBAzCvB,WACE,MAAMC,EAAWC,SAASC,eAAe,oBACzC,IAAKF,EAAU,OACf,MAAMG,EAAUH,EAASI,MAIzB,OAHArB,EAAY,CAAEsB,oBAAqBF,IACnB,QAAZA,GAAmBpB,EAAY,CAAEuB,cAAe,OACpDvB,EAAY,CAAEwB,QAAS,OAChBJ,CACT,EAiCiDK,sBA/BjDC,iBACE,MAAMC,EAAeT,SAASC,eAAe,cAE7C,GADuBD,SAASC,eAAe,cAC1CQ,EAAc,OAEnB,MAAMC,EAAY9B,EAAO+B,mBAAqBF,EAAaG,YAE3D,IAAIC,EACJ,GAAIhC,EAAMe,wBACRiB,EAAc,GAAGhC,EAAMc,gCAClB,CACL,MAAMmB,EAAad,SAASC,eAAe,YAC3CY,EAAcC,EAAaA,EAAWF,YAAc,IACtD,CAQA,GANkC,QAA9B/B,EAAMuB,qBAA+BtB,EAAY,CAAEuB,cAAe,OAMlExB,EAAMkC,eAAgB,CACxB,MAAMC,EAAMC,EAAyBpC,EAAMqC,eAAiBR,EAAW7B,EAAMc,yBAClE,MAAPqB,GAAalC,EAAY,CAAEwB,QAASU,GAC1C,CAEA,MAAMG,QAAmBC,EAAoB1C,EAAKgC,EAAWG,EAAahC,EAAMuB,qBAChFxB,EAAOyC,gBAAgBF,GACvBvC,EAAO0C,wBACT,EAEwEC,eAhExE,WACE,MAAMvC,EAAUL,EAAQ6C,SACxB,IAAKnD,oBAAoBW,GAAU,OAAO,KAM1C,IAAK,MAAMR,KAASJ,EAClBY,EAAQR,GAASiD,EAAoBzC,EAAQR,IAG/C,MAAMkD,EAAoB1C,EAAQ0C,mBAAwC,cAAlB1C,EAAQ2C,MAQhE,OAPA7C,EAAY,CAAE4C,sBAETA,GAAsB7C,EAAMqC,eAC/BpC,EAAY,CAAEoC,cAAelC,EAAQkC,eAAiBlC,EAAQ2C,QAGhE5C,aAAaC,GACNA,CACT,EA4CF"}
1
+ {"version":3,"file":"finance.js","sources":["../../../src/browser/widget/finance.js"],"sourcesContent":["// Finance unit (orchestration): applies cap-rate provenance to ctx, applies the scrape-derived\r\n// state, and recomputes the financial metrics. The pure rules live in financial/capRate.js; this\r\n// module is the thin state/DOM glue around them. Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { computeManualOverrideNOI, resolveCapRateProvenance } from \"../financial/capRate.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\nimport { normalizeWhitespace } from \"../../formatting/text.js\";\r\n\r\n// The Listing fields every scraper must return as strings (default \"Not found\"). A missing or\r\n// non-string field means the scraper broke; the engine refuses to render/export rather than\r\n// letting `undefined` flow into the NOI/COCR math and silently paint wrong numbers (fail-loud).\r\nconst LISTING_CONTRACT_FIELDS = [\"name\", \"price\", \"capRate\", \"contact\", \"phone\", \"listingDate\"];\r\n\r\nexport function isValidListingShape(data) {\r\n if (!data || typeof data !== \"object\") return false;\r\n return LISTING_CONTRACT_FIELDS.every((field) => typeof data[field] === \"string\");\r\n}\r\n\r\nexport function createFinance({ ctx, adapter, render }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Resolve the cap-rate provenance from the scraped string and write the cap state the\r\n // financial calc + discount handlers read. The default (DEFAULT_CAP_RATE * 100 = 5,\r\n // whole-number percent) fixes the latent no-cap bug where the decimal 0.05 was stored\r\n // and then divided by 100, computing NOI at 0.05%.\r\n function applyCapRate(listing) {\r\n const { isDefault, estimated, num, displayCap } = resolveCapRateProvenance(\r\n listing.capRate,\r\n FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100\r\n );\r\n listing.capRate = displayCap;\r\n\r\n if (isDefault) {\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate) updateState({ originalMultifamilyCapRate: `${num}%` });\r\n updateState({\r\n currentEstimatedCapRate: num,\r\n isUsingEstimatedCapRate: true,\r\n originalEstimatedCapRate: num,\r\n });\r\n return;\r\n }\r\n\r\n updateState({ isUsingEstimatedCapRate: estimated });\r\n if (estimated && num !== null) updateState({ currentEstimatedCapRate: num });\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate && num !== null) {\r\n updateState({ originalMultifamilyCapRate: `${num}%` });\r\n }\r\n }\r\n\r\n // Scrape the page and apply the universal scrape-derived state (was extractData's side\r\n // effects). Returns the resolved listing (capRate normalized to a display string) or null\r\n // when the scrape is malformed.\r\n function scrapeAndApply() {\r\n const listing = adapter.scrape();\r\n if (!isValidListingShape(listing)) return null;\r\n\r\n // Normalize whitespace on every contract string field centrally, so adapters stay pure\r\n // scrapers and no consumer (panel or export) ever sees the interior newlines/tabs that\r\n // site markup splits text across (e.g. a broker name on two lines). \"Not found\" is\r\n // unchanged. This is the single enforcement point for the data contract's \"normalize text\".\r\n for (const field of LISTING_CONTRACT_FIELDS) {\r\n listing[field] = normalizeWhitespace(listing[field]);\r\n }\r\n\r\n const priceWasDefaulted = listing.priceWasDefaulted ?? (listing.price === \"Not found\");\r\n updateState({ priceWasDefaulted });\r\n\r\n if (!priceWasDefaulted && !state.originalPrice) {\r\n updateState({ originalPrice: listing.originalPrice ?? listing.price });\r\n }\r\n\r\n applyCapRate(listing);\r\n return listing;\r\n }\r\n\r\n function handlePropertyTypeChange() {\r\n const dropdown = document.getElementById(\"ln-property-type\");\r\n if (!dropdown) return;\r\n const newType = dropdown.value;\r\n updateState({ currentPropertyType: newType });\r\n if (newType !== \"str\") updateState({ cachedSTRData: null });\r\n updateState({ baseNOI: null });\r\n return newType;\r\n }\r\n\r\n async function recalculateFinancials() {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const addressElement = document.getElementById(\"prop-name\");\r\n if (!priceElement) return;\r\n\r\n const priceText = render.getCurrentPrice() || priceElement.textContent;\r\n const address = addressElement?.textContent || \"\";\r\n let capRateText;\r\n if (state.isUsingEstimatedCapRate) {\r\n capRateText = `${state.currentEstimatedCapRate}%*`;\r\n } else {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n capRateText = capElement ? capElement.textContent : \"8%\";\r\n }\r\n\r\n if (state.currentPropertyType === \"str\") updateState({ cachedSTRData: null });\r\n\r\n // Manual cap override: clicking the cap rate sets NOI = original price x cap for EVERY type\r\n // (analyst intent), so the active cap moves with the click even for STR/assisted whose NOI\r\n // is otherwise the type estimate / bedroom value. Pre-seed baseNOI so calculateFinancials\r\n // uses it instead of recomputing from the type model.\r\n if (state.capManuallySet) {\r\n const noi = computeManualOverrideNOI(state.originalPrice || priceText, state.currentEstimatedCapRate);\r\n if (noi != null) updateState({ baseNOI: noi });\r\n }\r\n\r\n const financials = await calculateFinancials(ctx, priceText, capRateText, state.currentPropertyType, address);\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n render.updateEquityDisplay();\r\n }\r\n\r\n return { applyCapRate, handlePropertyTypeChange, recalculateFinancials, scrapeAndApply };\r\n}\r\n"],"names":["LISTING_CONTRACT_FIELDS","isValidListingShape","data","every","field","createFinance","ctx","adapter","render","state","updateState","applyCapRate","listing","isDefault","estimated","num","displayCap","resolveCapRateProvenance","capRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","originalCapRate","originalMultifamilyCapRate","currentEstimatedCapRate","isUsingEstimatedCapRate","originalEstimatedCapRate","handlePropertyTypeChange","dropdown","document","getElementById","newType","value","currentPropertyType","cachedSTRData","baseNOI","recalculateFinancials","async","priceElement","priceText","getCurrentPrice","textContent","capRateText","capElement","capManuallySet","noi","computeManualOverrideNOI","originalPrice","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","updateEquityDisplay","scrapeAndApply","scrape","normalizeWhitespace","priceWasDefaulted","price"],"mappings":"0SAYA,MAAMA,EAA0B,CAAC,OAAQ,QAAS,UAAW,UAAW,QAAS,eAE1E,SAASC,oBAAoBC,GAClC,SAAKA,GAAwB,iBAATA,IACbF,EAAwBG,MAAOC,GAAiC,iBAAhBF,EAAKE,GAC9D,CAEO,SAASC,eAAcC,IAAEA,EAAGC,QAAEA,EAAOC,OAAEA,IAC5C,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBJ,EAM/B,SAASK,aAAaC,GACpB,MAAMC,UAAEA,EAASC,UAAEA,EAASC,IAAEA,EAAGC,WAAEA,GAAeC,EAChDL,EAAQM,QAC+B,IAAvCC,EAAoBC,kBAItB,GAFAR,EAAQM,QAAUF,EAEdH,EAQF,OAPKJ,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAA4BZ,EAAY,CAAEY,2BAA4B,GAAGP,YACpFL,EAAY,CACVa,wBAAyBR,EACzBS,yBAAyB,EACzBC,yBAA0BV,IAK9BL,EAAY,CAAEc,wBAAyBV,IACnCA,GAAqB,OAARC,GAAcL,EAAY,CAAEa,wBAAyBR,IACjEN,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAAsC,OAARP,GACvCL,EAAY,CAAEY,2BAA4B,GAAGP,MAEjD,CAsEA,MAAO,CAAEJ,0BAAce,yBA1CvB,WACE,MAAMC,EAAWC,SAASC,eAAe,oBACzC,IAAKF,EAAU,OACf,MAAMG,EAAUH,EAASI,MAIzB,OAHArB,EAAY,CAAEsB,oBAAqBF,IACnB,QAAZA,GAAmBpB,EAAY,CAAEuB,cAAe,OACpDvB,EAAY,CAAEwB,QAAS,OAChBJ,CACT,EAkCiDK,sBAhCjDC,iBACE,MAAMC,EAAeT,SAASC,eAAe,cAE7C,GADuBD,SAASC,eAAe,cAC1CQ,EAAc,OAEnB,MAAMC,EAAY9B,EAAO+B,mBAAqBF,EAAaG,YAE3D,IAAIC,EACJ,GAAIhC,EAAMe,wBACRiB,EAAc,GAAGhC,EAAMc,gCAClB,CACL,MAAMmB,EAAad,SAASC,eAAe,YAC3CY,EAAcC,EAAaA,EAAWF,YAAc,IACtD,CAQA,GANkC,QAA9B/B,EAAMuB,qBAA+BtB,EAAY,CAAEuB,cAAe,OAMlExB,EAAMkC,eAAgB,CACxB,MAAMC,EAAMC,EAAyBpC,EAAMqC,eAAiBR,EAAW7B,EAAMc,yBAClE,MAAPqB,GAAalC,EAAY,CAAEwB,QAASU,GAC1C,CAEA,MAAMG,QAAmBC,EAAoB1C,EAAKgC,EAAWG,EAAahC,EAAMuB,qBAChFxB,EAAOyC,gBAAgBF,GACvBvC,EAAO0C,yBACP1C,EAAO2C,qBACT,EAEwEC,eAjExE,WACE,MAAMxC,EAAUL,EAAQ8C,SACxB,IAAKpD,oBAAoBW,GAAU,OAAO,KAM1C,IAAK,MAAMR,KAASJ,EAClBY,EAAQR,GAASkD,EAAoB1C,EAAQR,IAG/C,MAAMmD,EAAoB3C,EAAQ2C,mBAAwC,cAAlB3C,EAAQ4C,MAQhE,OAPA9C,EAAY,CAAE6C,sBAETA,GAAsB9C,EAAMqC,eAC/BpC,EAAY,CAAEoC,cAAelC,EAAQkC,eAAiBlC,EAAQ4C,QAGhE7C,aAAaC,GACNA,CACT,EA6CF"}
@@ -1,2 +1,2 @@
1
- import{createNavigationGuard as e}from"./createNavigationGuard.js";import{createPanel as t}from"./createPanel.js";import{runReveals as a}from"./runReveals.js";import{setupPriceClickHandler as n,setupCapRateClickHandler as r,setupDownPaymentClickHandler as o,setupDiscountButtonHandler as c}from"../ui/click-handlers.js";import{calculateFinancials as i}from"../financial/calculateFinancials.js";import{calculateDOM as l}from"../../date/utilities.js";function createPipeline({adapter:s,config:p,ctx:u,exportOps:d,finance:m,render:y,resolveCssUrls:f,services:g}){const{state:E,updateState:b}=u,listingId=()=>s.getListingId(window.location.href);async function updateFooterData(){const t=e(listingId);if(t.capture(),p.reveals?.length&&(await a(p.reveals),t.isStale()))return;const s=m.scrapeAndApply();if(!s)return console.error("❌ Malformed listing data — missing a contract field, refusing to render"),void y.updateElement("prop-name","Data error — see console");const d=s.unitCount??4;b({numberOfUnits:d});const f=document.getElementById("ln-units-input");if(f&&(f.value=d),d>11){const e=document.getElementById("ln-interest-rate-type");e&&"dscr_commercial"!==e.value&&(e.value="dscr_commercial",b({currentInterestRateType:"dscr_commercial"}))}y.updateElement("prop-name",s.name),y.updateElement("prop-price",E.priceWasDefaulted?"No price":s.price),y.updateElement("prop-contact",s.contact),y.updateElement("prop-phone",s.phone),y.updateElement("prop-dom",l(s.listingDate)),y.updatePriceLabel(),y.updateCapRateLabel(),y.syncUnitsFieldForType(E.currentPropertyType,s.bedroomCount),function(e){const t=document.getElementById("prop-name");t&&e.name&&"Not found"!==e.name&&(t.style.cursor="pointer",t.style.textDecoration="underline",t.onclick=()=>{const t=`https://www.google.com/maps/search/${encodeURIComponent(e.name)}`;window.open(t,"_blank")});const a={getCurrentPrice:y.getCurrentPrice,recalculateFinancials:m.recalculateFinancials,state:E,updatePercentageLabels:y.updatePercentageLabels,updatePriceLabel:y.updatePriceLabel,updateState:b},i=document.getElementById("prop-price");n(i,i?.closest(".metric")?.querySelector(".metric-label"),a);const l=document.getElementById("prop-cap");r(l,l?.closest(".metric")?.querySelector(".metric-label"),a);const s=document.getElementById("prop-down");o(s,s?.closest(".metric")?.querySelector(".metric-label"),a),c(document.getElementById("ln-discount-btn"),a)}(s);const C=E.isUsingEstimatedCapRate?`${E.currentEstimatedCapRate}%`:s.capRate,P=await i(u,s.price,C,E.currentPropertyType,s.name);if(t.isStale())return;y.applyFinancials(P),y.updateActiveCapDisplay();const w=await g.loadLeadStatus(s.name);if(t.isStale())return;y.updateElement("prop-lead-status",w.leadStatus),y.updateLeadStatusTooltip(w);const S=await g.loadStrValue(s.name,t);if(t.isStale())return;if(S&&"str"===E.currentPropertyType&&(b({baseNOI:null}),await m.recalculateFinancials(),t.isStale()))return;const F=await g.loadEquity(s.name,t);t.isStale()||y.updateElement("prop-equity",F)}let C=!1,P=null;return{runPipeline:function(){C=!1,P&&(P.disconnect(),P=null),t({callbacks:{onExportClick:d.handleExportClick,onInterestRateTypeChange:()=>m.recalculateFinancials(),onPropertyTypeChange:()=>{m.handlePropertyTypeChange(),y.updateCapRateLabel();const e=s.scrape();y.syncUnitsFieldForType(E.currentPropertyType,e?.bedroomCount),m.recalculateFinancials()},state:E,updateState:b},cssUrls:f(p.cssFiles),defaultPropertyType:p.defaultPropertyType});const runUpdateOnce=async()=>{C||(C=!0,await updateFooterData())},tryImmediateUpdate=()=>{const e=document.getElementById("prop-name"),t=document.getElementById("prop-price");return!!(e&&t&&e.textContent.trim()&&t.textContent.trim())&&(runUpdateOnce(),!0)};tryImmediateUpdate()||(P=new MutationObserver((e,t)=>{tryImmediateUpdate()&&(t.disconnect(),P=null)}),P.observe(document.body,{childList:!0,subtree:!0}),"complete"!==document.readyState&&window.addEventListener("load",()=>{setTimeout(()=>{C||(runUpdateOnce(),P&&(P.disconnect(),P=null))},5e3)}))},updateFooterData:updateFooterData}}export{createPipeline};
1
+ import{createNavigationGuard as e}from"./createNavigationGuard.js";import{createPanel as t}from"./createPanel.js";import{runReveals as a}from"./runReveals.js";import{setupPriceClickHandler as n,setupCapRateClickHandler as r,setupDownPaymentClickHandler as o,setupDiscountButtonHandler as c}from"../ui/click-handlers.js";import{calculateFinancials as l}from"../financial/calculateFinancials.js";import{calculateDOM as i}from"../../date/utilities.js";function createPipeline({adapter:s,config:p,ctx:u,exportOps:d,finance:m,render:y,resolveCssUrls:g,services:f}){const{state:b,updateState:E}=u,listingId=()=>s.getListingId(window.location.href);async function updateFooterData(){const t=e(listingId);if(t.capture(),p.reveals?.length&&(await a(p.reveals),t.isStale()))return;const s=m.scrapeAndApply();if(!s)return console.error("❌ Malformed listing data — missing a contract field, refusing to render"),void y.updateElement("prop-name","Data error — see console");const d=s.unitCount??4;E({numberOfUnits:d});const g=document.getElementById("ln-units-input");if(g&&(g.value=d),d>11){const e=document.getElementById("ln-interest-rate-type");e&&"dscr_commercial"!==e.value&&(e.value="dscr_commercial",E({currentInterestRateType:"dscr_commercial"}))}y.updateElement("prop-name",s.name),y.updateElement("prop-price",b.priceWasDefaulted?"No price":s.price),y.updateElement("prop-contact",s.contact),y.updateElement("prop-phone",s.phone),y.updateElement("prop-dom",i(s.listingDate)),y.updatePriceLabel(),y.updateCapRateLabel(),y.syncUnitsFieldForType(b.currentPropertyType,s.bedroomCount),function(e){const t=document.getElementById("prop-name");t&&e.name&&"Not found"!==e.name&&(t.style.cursor="pointer",t.style.textDecoration="underline",t.onclick=()=>{const t=`https://www.google.com/maps/search/${encodeURIComponent(e.name)}`;window.open(t,"_blank")});const a={getCurrentPrice:y.getCurrentPrice,recalculateFinancials:m.recalculateFinancials,state:b,updatePercentageLabels:y.updatePercentageLabels,updatePriceLabel:y.updatePriceLabel,updateState:E},l=document.getElementById("prop-price");n(l,l?.closest(".metric")?.querySelector(".metric-label"),a);const i=document.getElementById("prop-cap");r(i,i?.closest(".metric")?.querySelector(".metric-label"),a);const s=document.getElementById("prop-down");o(s,s?.closest(".metric")?.querySelector(".metric-label"),a),c(document.getElementById("ln-discount-btn"),a)}(s);const C=b.isUsingEstimatedCapRate?`${b.currentEstimatedCapRate}%`:s.capRate,P=await l(u,s.price,C,b.currentPropertyType,s.name);if(t.isStale())return;y.applyFinancials(P),y.updateActiveCapDisplay();const w=await f.loadLeadStatus(s.name);if(t.isStale())return;y.updateElement("prop-lead-status",w.leadStatus),y.updateLeadStatusTooltip(w);const S=await f.loadStrValue(s.name,t);t.isStale()||S&&"str"===b.currentPropertyType&&(E({baseNOI:null}),await m.recalculateFinancials(),t.isStale())||(await f.loadDebt(s.name,t),t.isStale()||y.updateEquityDisplay())}let C=!1,P=null;return{runPipeline:function(){C=!1,P&&(P.disconnect(),P=null),t({callbacks:{onExportClick:d.handleExportClick,onInterestRateTypeChange:()=>m.recalculateFinancials(),onPropertyTypeChange:()=>{m.handlePropertyTypeChange(),y.updateCapRateLabel();const e=s.scrape();y.syncUnitsFieldForType(b.currentPropertyType,e?.bedroomCount),m.recalculateFinancials()},state:b,updateState:E},cssUrls:g(p.cssFiles),defaultPropertyType:p.defaultPropertyType});const runUpdateOnce=async()=>{C||(C=!0,await updateFooterData())},tryImmediateUpdate=()=>{const e=document.getElementById("prop-name"),t=document.getElementById("prop-price");return!!(e&&t&&e.textContent.trim()&&t.textContent.trim())&&(runUpdateOnce(),!0)};tryImmediateUpdate()||(P=new MutationObserver((e,t)=>{tryImmediateUpdate()&&(t.disconnect(),P=null)}),P.observe(document.body,{childList:!0,subtree:!0}),"complete"!==document.readyState&&window.addEventListener("load",()=>{setTimeout(()=>{C||(runUpdateOnce(),P&&(P.disconnect(),P=null))},5e3)}))},updateFooterData:updateFooterData}}export{createPipeline};
2
2
  //# sourceMappingURL=pipeline.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.js","sources":["../../../src/browser/widget/pipeline.js"],"sourcesContent":["// Pipeline unit: the re-runnable footer pipeline — builds the shared panel, wires the clickable\r\n// elements, runs the main async update (scrape -> financials -> lead status -> STR -> equity with\r\n// the navigation guard between awaits), and drives the immediate/observer/load entry points.\r\n// Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createNavigationGuard } from \"./createNavigationGuard.js\";\r\nimport { createPanel } from \"./createPanel.js\";\r\nimport { runReveals } from \"./runReveals.js\";\r\nimport {\r\n setupCapRateClickHandler,\r\n setupDiscountButtonHandler,\r\n setupDownPaymentClickHandler,\r\n setupPriceClickHandler,\r\n} from \"../ui/click-handlers.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { calculateDOM } from \"../../date/utilities.js\";\r\n\r\nexport function createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services }) {\r\n const { state, updateState } = ctx;\r\n const listingId = () => adapter.getListingId(window.location.href);\r\n\r\n function setupClickableElements(data) {\r\n const nameElement = document.getElementById(\"prop-name\");\r\n if (nameElement && data.name && data.name !== \"Not found\") {\r\n nameElement.style.cursor = \"pointer\";\r\n nameElement.style.textDecoration = \"underline\";\r\n nameElement.onclick = () => {\r\n const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(data.name)}`;\r\n window.open(searchUrl, \"_blank\");\r\n };\r\n }\r\n\r\n // The shared click-handlers read callbacks.state / callbacks.updateState — the engine's\r\n // ctx is injected here (no global-state coupling).\r\n const callbacks = {\r\n getCurrentPrice: render.getCurrentPrice,\r\n recalculateFinancials: finance.recalculateFinancials,\r\n state,\r\n updatePercentageLabels: render.updatePercentageLabels,\r\n updatePriceLabel: render.updatePriceLabel,\r\n updateState,\r\n };\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n setupPriceClickHandler(priceElement, priceElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const capElement = document.getElementById(\"prop-cap\");\r\n setupCapRateClickHandler(capElement, capElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const downElement = document.getElementById(\"prop-down\");\r\n setupDownPaymentClickHandler(downElement, downElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n setupDiscountButtonHandler(document.getElementById(\"ln-discount-btn\"), callbacks);\r\n }\r\n\r\n async function updateFooterData() {\r\n // The listing this run is for. On an SPA the page can navigate mid-flight; after each\r\n // await we drop out if the identity changed, so a stale run never writes onto another\r\n // listing's panel. On a full-reload site getListingId is stable, so isStale() is always\r\n // false and this is a no-op.\r\n const guard = createNavigationGuard(listingId);\r\n guard.capture();\r\n\r\n // Click-to-reveal any data gated behind a button (broker phone/email, OM access) so the\r\n // pure scrape() below reads it. Platform-declared (config.reveals); a no-op when absent.\r\n if (config.reveals?.length) {\r\n await runReveals(config.reveals);\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const data = finance.scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — missing a contract field, refusing to render\");\r\n render.updateElement(\"prop-name\", \"Data error — see console\");\r\n return;\r\n }\r\n\r\n const unitCount = data.unitCount ?? 4;\r\n updateState({ numberOfUnits: unitCount });\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) unitsInput.value = unitCount;\r\n\r\n if (unitCount > 11) {\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n }\r\n }\r\n\r\n render.updateElement(\"prop-name\", data.name);\r\n // Display guard (H2): a defaulted price shows \"No price\"; the metrics fall through to N/A.\r\n render.updateElement(\"prop-price\", state.priceWasDefaulted ? \"No price\" : data.price);\r\n render.updateElement(\"prop-contact\", data.contact);\r\n render.updateElement(\"prop-phone\", data.phone);\r\n render.updateElement(\"prop-dom\", calculateDOM(data.listingDate));\r\n\r\n render.updatePriceLabel();\r\n render.updateCapRateLabel();\r\n render.syncUnitsFieldForType(state.currentPropertyType, data.bedroomCount);\r\n setupClickableElements(data);\r\n\r\n const calculationCapRate = state.isUsingEstimatedCapRate ? `${state.currentEstimatedCapRate}%` : data.capRate;\r\n const financials = await calculateFinancials(ctx, data.price, calculationCapRate, state.currentPropertyType, data.name);\r\n if (guard.isStale()) return;\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n\r\n const loiData = await services.loadLeadStatus(data.name);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-lead-status\", loiData.leadStatus);\r\n render.updateLeadStatusTooltip(loiData);\r\n\r\n // STR revenue seam: the footer already shows the 5.5%-of-price estimate. If the backend\r\n // returns real data, recompute the STR NOI with it. Dormant until that backend ships.\r\n const strResult = await services.loadStrValue(data.name, guard);\r\n if (guard.isStale()) return;\r\n if (strResult && state.currentPropertyType === \"str\") {\r\n updateState({ baseNOI: null });\r\n await finance.recalculateFinancials();\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const equity = await services.loadEquity(data.name, guard);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-equity\", equity);\r\n }\r\n\r\n // One running pipeline at a time. Re-runnable so the SPA watcher can rebuild per listing;\r\n // the observer is tracked so a re-run detaches the previous one.\r\n let footerUpdated = false;\r\n let pipelineObserver = null;\r\n\r\n function runPipeline() {\r\n footerUpdated = false;\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n\r\n createPanel({\r\n callbacks: {\r\n onExportClick: exportOps.handleExportClick,\r\n onInterestRateTypeChange: () => finance.recalculateFinancials(),\r\n onPropertyTypeChange: () => {\r\n finance.handlePropertyTypeChange();\r\n render.updateCapRateLabel();\r\n const listing = adapter.scrape();\r\n render.syncUnitsFieldForType(state.currentPropertyType, listing?.bedroomCount);\r\n finance.recalculateFinancials();\r\n },\r\n state,\r\n updateState,\r\n },\r\n cssUrls: resolveCssUrls(config.cssFiles),\r\n defaultPropertyType: config.defaultPropertyType,\r\n });\r\n\r\n const runUpdateOnce = async () => {\r\n if (footerUpdated) return;\r\n footerUpdated = true;\r\n await updateFooterData();\r\n };\r\n\r\n const tryImmediateUpdate = () => {\r\n const nameEl = document.getElementById(\"prop-name\");\r\n const priceEl = document.getElementById(\"prop-price\");\r\n if (nameEl && priceEl && nameEl.textContent.trim() && priceEl.textContent.trim()) {\r\n runUpdateOnce();\r\n return true;\r\n }\r\n return false;\r\n };\r\n\r\n if (tryImmediateUpdate()) return;\r\n\r\n pipelineObserver = new MutationObserver((mutations, obs) => {\r\n if (tryImmediateUpdate()) {\r\n obs.disconnect();\r\n pipelineObserver = null;\r\n }\r\n });\r\n pipelineObserver.observe(document.body, { childList: true, subtree: true });\r\n\r\n // Safety fallback before the page has loaded; SPA navigations fire after load, so the\r\n // observer (not load) drives those.\r\n if (document.readyState !== \"complete\") {\r\n window.addEventListener(\"load\", () => {\r\n setTimeout(() => {\r\n if (!footerUpdated) {\r\n runUpdateOnce();\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n }\r\n }, 5000);\r\n });\r\n }\r\n }\r\n\r\n return { runPipeline, updateFooterData };\r\n}\r\n"],"names":["createPipeline","adapter","config","ctx","exportOps","finance","render","resolveCssUrls","services","state","updateState","listingId","getListingId","window","location","href","async","updateFooterData","guard","createNavigationGuard","capture","reveals","length","runReveals","isStale","data","scrapeAndApply","console","error","updateElement","unitCount","numberOfUnits","unitsInput","document","getElementById","value","irDropdown","currentInterestRateType","name","priceWasDefaulted","price","contact","phone","calculateDOM","listingDate","updatePriceLabel","updateCapRateLabel","syncUnitsFieldForType","currentPropertyType","bedroomCount","nameElement","style","cursor","textDecoration","onclick","searchUrl","encodeURIComponent","open","callbacks","getCurrentPrice","recalculateFinancials","updatePercentageLabels","priceElement","setupPriceClickHandler","closest","querySelector","capElement","setupCapRateClickHandler","downElement","setupDownPaymentClickHandler","setupDiscountButtonHandler","setupClickableElements","calculationCapRate","isUsingEstimatedCapRate","currentEstimatedCapRate","capRate","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","loiData","loadLeadStatus","leadStatus","updateLeadStatusTooltip","strResult","loadStrValue","baseNOI","equity","loadEquity","footerUpdated","pipelineObserver","runPipeline","disconnect","createPanel","onExportClick","handleExportClick","onInterestRateTypeChange","onPropertyTypeChange","handlePropertyTypeChange","listing","scrape","cssUrls","cssFiles","defaultPropertyType","runUpdateOnce","tryImmediateUpdate","nameEl","priceEl","textContent","trim","MutationObserver","mutations","obs","observe","body","childList","subtree","readyState","addEventListener","setTimeout"],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.js","sources":["../../../src/browser/widget/pipeline.js"],"sourcesContent":["// Pipeline unit: the re-runnable footer pipeline — builds the shared panel, wires the clickable\r\n// elements, runs the main async update (scrape -> financials -> lead status -> STR -> equity with\r\n// the navigation guard between awaits), and drives the immediate/observer/load entry points.\r\n// Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createNavigationGuard } from \"./createNavigationGuard.js\";\r\nimport { createPanel } from \"./createPanel.js\";\r\nimport { runReveals } from \"./runReveals.js\";\r\nimport {\r\n setupCapRateClickHandler,\r\n setupDiscountButtonHandler,\r\n setupDownPaymentClickHandler,\r\n setupPriceClickHandler,\r\n} from \"../ui/click-handlers.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { calculateDOM } from \"../../date/utilities.js\";\r\n\r\nexport function createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services }) {\r\n const { state, updateState } = ctx;\r\n const listingId = () => adapter.getListingId(window.location.href);\r\n\r\n function setupClickableElements(data) {\r\n const nameElement = document.getElementById(\"prop-name\");\r\n if (nameElement && data.name && data.name !== \"Not found\") {\r\n nameElement.style.cursor = \"pointer\";\r\n nameElement.style.textDecoration = \"underline\";\r\n nameElement.onclick = () => {\r\n const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(data.name)}`;\r\n window.open(searchUrl, \"_blank\");\r\n };\r\n }\r\n\r\n // The shared click-handlers read callbacks.state / callbacks.updateState — the engine's\r\n // ctx is injected here (no global-state coupling).\r\n const callbacks = {\r\n getCurrentPrice: render.getCurrentPrice,\r\n recalculateFinancials: finance.recalculateFinancials,\r\n state,\r\n updatePercentageLabels: render.updatePercentageLabels,\r\n updatePriceLabel: render.updatePriceLabel,\r\n updateState,\r\n };\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n setupPriceClickHandler(priceElement, priceElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const capElement = document.getElementById(\"prop-cap\");\r\n setupCapRateClickHandler(capElement, capElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const downElement = document.getElementById(\"prop-down\");\r\n setupDownPaymentClickHandler(downElement, downElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n setupDiscountButtonHandler(document.getElementById(\"ln-discount-btn\"), callbacks);\r\n }\r\n\r\n async function updateFooterData() {\r\n // The listing this run is for. On an SPA the page can navigate mid-flight; after each\r\n // await we drop out if the identity changed, so a stale run never writes onto another\r\n // listing's panel. On a full-reload site getListingId is stable, so isStale() is always\r\n // false and this is a no-op.\r\n const guard = createNavigationGuard(listingId);\r\n guard.capture();\r\n\r\n // Click-to-reveal any data gated behind a button (broker phone/email, OM access) so the\r\n // pure scrape() below reads it. Platform-declared (config.reveals); a no-op when absent.\r\n if (config.reveals?.length) {\r\n await runReveals(config.reveals);\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const data = finance.scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — missing a contract field, refusing to render\");\r\n render.updateElement(\"prop-name\", \"Data error — see console\");\r\n return;\r\n }\r\n\r\n const unitCount = data.unitCount ?? 4;\r\n updateState({ numberOfUnits: unitCount });\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) unitsInput.value = unitCount;\r\n\r\n if (unitCount > 11) {\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n }\r\n }\r\n\r\n render.updateElement(\"prop-name\", data.name);\r\n // Display guard (H2): a defaulted price shows \"No price\"; the metrics fall through to N/A.\r\n render.updateElement(\"prop-price\", state.priceWasDefaulted ? \"No price\" : data.price);\r\n render.updateElement(\"prop-contact\", data.contact);\r\n render.updateElement(\"prop-phone\", data.phone);\r\n render.updateElement(\"prop-dom\", calculateDOM(data.listingDate));\r\n\r\n render.updatePriceLabel();\r\n render.updateCapRateLabel();\r\n render.syncUnitsFieldForType(state.currentPropertyType, data.bedroomCount);\r\n setupClickableElements(data);\r\n\r\n const calculationCapRate = state.isUsingEstimatedCapRate ? `${state.currentEstimatedCapRate}%` : data.capRate;\r\n const financials = await calculateFinancials(ctx, data.price, calculationCapRate, state.currentPropertyType, data.name);\r\n if (guard.isStale()) return;\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n\r\n const loiData = await services.loadLeadStatus(data.name);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-lead-status\", loiData.leadStatus);\r\n render.updateLeadStatusTooltip(loiData);\r\n\r\n // STR revenue seam: the footer already shows the 5.5%-of-price estimate. If the backend\r\n // returns real data, recompute the STR NOI with it. Dormant until that backend ships.\r\n const strResult = await services.loadStrValue(data.name, guard);\r\n if (guard.isStale()) return;\r\n if (strResult && state.currentPropertyType === \"str\") {\r\n updateState({ baseNOI: null });\r\n await finance.recalculateFinancials();\r\n if (guard.isStale()) return;\r\n }\r\n\r\n await services.loadDebt(data.name, guard);\r\n if (guard.isStale()) return;\r\n render.updateEquityDisplay();\r\n }\r\n\r\n // One running pipeline at a time. Re-runnable so the SPA watcher can rebuild per listing;\r\n // the observer is tracked so a re-run detaches the previous one.\r\n let footerUpdated = false;\r\n let pipelineObserver = null;\r\n\r\n function runPipeline() {\r\n footerUpdated = false;\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n\r\n createPanel({\r\n callbacks: {\r\n onExportClick: exportOps.handleExportClick,\r\n onInterestRateTypeChange: () => finance.recalculateFinancials(),\r\n onPropertyTypeChange: () => {\r\n finance.handlePropertyTypeChange();\r\n render.updateCapRateLabel();\r\n const listing = adapter.scrape();\r\n render.syncUnitsFieldForType(state.currentPropertyType, listing?.bedroomCount);\r\n finance.recalculateFinancials();\r\n },\r\n state,\r\n updateState,\r\n },\r\n cssUrls: resolveCssUrls(config.cssFiles),\r\n defaultPropertyType: config.defaultPropertyType,\r\n });\r\n\r\n const runUpdateOnce = async () => {\r\n if (footerUpdated) return;\r\n footerUpdated = true;\r\n await updateFooterData();\r\n };\r\n\r\n const tryImmediateUpdate = () => {\r\n const nameEl = document.getElementById(\"prop-name\");\r\n const priceEl = document.getElementById(\"prop-price\");\r\n if (nameEl && priceEl && nameEl.textContent.trim() && priceEl.textContent.trim()) {\r\n runUpdateOnce();\r\n return true;\r\n }\r\n return false;\r\n };\r\n\r\n if (tryImmediateUpdate()) return;\r\n\r\n pipelineObserver = new MutationObserver((mutations, obs) => {\r\n if (tryImmediateUpdate()) {\r\n obs.disconnect();\r\n pipelineObserver = null;\r\n }\r\n });\r\n pipelineObserver.observe(document.body, { childList: true, subtree: true });\r\n\r\n // Safety fallback before the page has loaded; SPA navigations fire after load, so the\r\n // observer (not load) drives those.\r\n if (document.readyState !== \"complete\") {\r\n window.addEventListener(\"load\", () => {\r\n setTimeout(() => {\r\n if (!footerUpdated) {\r\n runUpdateOnce();\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n }\r\n }, 5000);\r\n });\r\n }\r\n }\r\n\r\n return { runPipeline, updateFooterData };\r\n}\r\n"],"names":["createPipeline","adapter","config","ctx","exportOps","finance","render","resolveCssUrls","services","state","updateState","listingId","getListingId","window","location","href","async","updateFooterData","guard","createNavigationGuard","capture","reveals","length","runReveals","isStale","data","scrapeAndApply","console","error","updateElement","unitCount","numberOfUnits","unitsInput","document","getElementById","value","irDropdown","currentInterestRateType","name","priceWasDefaulted","price","contact","phone","calculateDOM","listingDate","updatePriceLabel","updateCapRateLabel","syncUnitsFieldForType","currentPropertyType","bedroomCount","nameElement","style","cursor","textDecoration","onclick","searchUrl","encodeURIComponent","open","callbacks","getCurrentPrice","recalculateFinancials","updatePercentageLabels","priceElement","setupPriceClickHandler","closest","querySelector","capElement","setupCapRateClickHandler","downElement","setupDownPaymentClickHandler","setupDiscountButtonHandler","setupClickableElements","calculationCapRate","isUsingEstimatedCapRate","currentEstimatedCapRate","capRate","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","loiData","loadLeadStatus","leadStatus","updateLeadStatusTooltip","strResult","loadStrValue","baseNOI","loadDebt","updateEquityDisplay","footerUpdated","pipelineObserver","runPipeline","disconnect","createPanel","onExportClick","handleExportClick","onInterestRateTypeChange","onPropertyTypeChange","handlePropertyTypeChange","listing","scrape","cssUrls","cssFiles","defaultPropertyType","runUpdateOnce","tryImmediateUpdate","nameEl","priceEl","textContent","trim","MutationObserver","mutations","obs","observe","body","childList","subtree","readyState","addEventListener","setTimeout"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{hasTooltip as e,attachTooltip as t,updateTooltipContent as n}from"../ui/tooltip-manager.js";import{generateDownPaymentTooltipHTML as r,generateCashFlowTooltipHTML as o}from"../financial/tooltip-content-generators.js";import{parseFinancialData as c,parseCashFlowData as s}from"../financial/tooltip-calculations.js";import{parsePriceNumber as i,computeActiveCapDisplay as a,parseReportedCap as l}from"../financial/capRate.js";const p=["prop-noi","prop-down","prop-net","prop-seller-fi","prop-cocr-30","prop-cocr-15","prop-assignment","prop-dscr","prop-sf","prop-cashflow"];function createRender({ctx:u}){const{state:d,updateState:m}=u;function getCurrentPrice(){if(d.originalPrice&&d.currentPriceDiscount>0){const e=parseFloat(d.originalPrice.replace(/[$,]/g,""))*(1-d.currentPriceDiscount/100);return`$${Math.round(e).toLocaleString()}`}return d.originalPrice}function updatePercentageLabels(){const e=document.querySelector("#prop-down")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=`Down (${d.currentDownPaymentPercent}%)`);const t=document.querySelector("#prop-seller-fi")?.closest(".metric")?.querySelector(".metric-label");t&&(t.textContent=`Seller FI (${d.currentSellerFiPercent}%)`);const n=document.querySelector("#prop-dscr")?.closest(".metric")?.querySelector(".metric-label");n&&(n.textContent=`DSCR (${d.currentDSCRPercent}%)`)}function updateElement(e,t){const n=document.getElementById(e);n&&(n.textContent=t)}return{applyFinancials:function(i){let a=!1;if(i){const l={"prop-noi":i.noi,"prop-down":i.down,"prop-net":i.netToBuyer,"prop-seller-fi":i.sellerFi,"prop-cocr-30":i.cocr30,"prop-cocr-15":i.priceForCOCR15,"prop-assignment":i.assignment,"prop-dscr":i.dscr,"prop-sf":i.sfPayment,"prop-cashflow":i.cashFlow};a=i.rawCashFlow<0;for(const[e,t]of Object.entries(l))updateElement(e,t);setTimeout(()=>{!function(){const o=document.getElementById("prop-down"),s=document.getElementById("prop-price"),i=document.getElementById("prop-noi");if(!o||!s||!i)return;const a=c(s.textContent,i.textContent);if(a){const c=r(a.price,a.noi,d.currentDownPaymentPercent,d.currentDSCRPercent,d.currentSellerFiPercent),s=o.closest(".metric");s&&(e(s)?n(s,c):t(s,c))}}(),function(){const r=document.getElementById("prop-cashflow"),c=document.getElementById("prop-price");if(!r||!c)return;const i=s(c.textContent,r.textContent);if(i){const c=o(i.price,i.monthlyCashFlow),s=r.closest(".metric");if(s){const r=s.querySelector(".metric-label");r&&r.classList.add("has-tooltip"),e(s)?n(s,c):t(s,c)}}}()},100)}else p.forEach(e=>updateElement(e,"N/A"));updatePercentageLabels();const l=document.getElementById("ln-footer");l&&l.classList.toggle("negative",a)},getCurrentPrice:getCurrentPrice,syncUnitsFieldForType:function(e,t){const n=document.querySelector(".units-inline-label");if(n&&(n.textContent="assisted"===e?"beds":"units"),"assisted"===e&&null!=t){const e=document.getElementById("ln-units-input");e&&(e.value=String(t)),m({numberOfUnits:t})}},updateActiveCapDisplay:function(){const r=document.getElementById("prop-cap");if(!r)return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o);r.textContent=a(d.baseNOI,c);const s=l(d.originalCapRate,d.isUsingEstimatedCapRate),p=`${"<strong>Reported cap rate:</strong> "+(null!=s?`${s}%`:"N/A")}${d.isUsingEstimatedCapRate?"<hr><em>Click the cap rate to increase by 1%; click the label to reset</em>":""}`,u=r.closest(".metric");if(u){const r=u.querySelector(".metric-label");e(u)?n(u,p):(t(u,p),r&&r.classList.add("has-tooltip"))}},updateCapRateLabel:function(){const e=document.querySelector("#prop-cap")?.closest(".metric")?.querySelector(".metric-label");if(e)switch(d.currentPropertyType){case"str":e.textContent="Cap Rate (STR)";break;case"assisted":e.textContent="Cap Rate (Assisted)";break;default:e.textContent="Cap Rate"}},updateElement:updateElement,updateLeadStatusTooltip:function(r){const o=document.getElementById("prop-lead-status");if(!o)return;let c="No LOI data returned";r&&(c=`\n <strong>Contact:</strong> ${r.contactName} <br>\n ${r.opportunityAddress}\n `);const s=o.closest(".metric");if(s)if(e(s))n(s,c);else{t(s,c);const e=s.querySelector(".metric-label");e&&e.classList.add("has-tooltip")}},updatePercentageLabels:updatePercentageLabels,updatePriceLabel:function(){const e=document.querySelector("#prop-price")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=d.currentPriceDiscount>0?`Price (${d.currentPriceDiscount}%)`:"Price")}}}export{createRender};
1
+ import{hasTooltip as e,attachTooltip as t,updateTooltipContent as n}from"../ui/tooltip-manager.js";import{generateDownPaymentTooltipHTML as r,generateCashFlowTooltipHTML as o}from"../financial/tooltip-content-generators.js";import{parseFinancialData as c,parseCashFlowData as s}from"../financial/tooltip-calculations.js";import{parsePriceNumber as i,computeActiveCapDisplay as a,parseReportedCap as l}from"../financial/capRate.js";import{equityPercentFromDebt as p}from"../../financial/calculations.js";const u=["prop-noi","prop-down","prop-net","prop-seller-fi","prop-cocr-30","prop-cocr-15","prop-assignment","prop-dscr","prop-sf","prop-cashflow"];function createRender({ctx:d}){const{state:m,updateState:g}=d;function getCurrentPrice(){if(m.originalPrice&&m.currentPriceDiscount>0){const e=parseFloat(m.originalPrice.replace(/[$,]/g,""))*(1-m.currentPriceDiscount/100);return`$${Math.round(e).toLocaleString()}`}return m.originalPrice}function updatePercentageLabels(){const e=document.querySelector("#prop-down")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=`Down (${m.currentDownPaymentPercent}%)`);const t=document.querySelector("#prop-seller-fi")?.closest(".metric")?.querySelector(".metric-label");t&&(t.textContent=`Seller FI (${m.currentSellerFiPercent}%)`);const n=document.querySelector("#prop-dscr")?.closest(".metric")?.querySelector(".metric-label");n&&(n.textContent=`DSCR (${m.currentDSCRPercent}%)`)}function updateElement(e,t){const n=document.getElementById(e);n&&(n.textContent=t)}return{applyFinancials:function(i){let a=!1;if(i){const l={"prop-noi":i.noi,"prop-down":i.down,"prop-net":i.netToBuyer,"prop-seller-fi":i.sellerFi,"prop-cocr-30":i.cocr30,"prop-cocr-15":i.priceForCOCR15,"prop-assignment":i.assignment,"prop-dscr":i.dscr,"prop-sf":i.sfPayment,"prop-cashflow":i.cashFlow};a=i.rawCashFlow<0;for(const[e,t]of Object.entries(l))updateElement(e,t);setTimeout(()=>{!function(){const o=document.getElementById("prop-down"),s=document.getElementById("prop-price"),i=document.getElementById("prop-noi");if(!o||!s||!i)return;const a=c(s.textContent,i.textContent);if(a){const c=r(a.price,a.noi,m.currentDownPaymentPercent,m.currentDSCRPercent,m.currentSellerFiPercent),s=o.closest(".metric");s&&(e(s)?n(s,c):t(s,c))}}(),function(){const r=document.getElementById("prop-cashflow"),c=document.getElementById("prop-price");if(!r||!c)return;const i=s(c.textContent,r.textContent);if(i){const c=o(i.price,i.monthlyCashFlow),s=r.closest(".metric");if(s){const r=s.querySelector(".metric-label");r&&r.classList.add("has-tooltip"),e(s)?n(s,c):t(s,c)}}}()},100)}else u.forEach(e=>updateElement(e,"N/A"));updatePercentageLabels();const l=document.getElementById("ln-footer");l&&l.classList.toggle("negative",a)},getCurrentPrice:getCurrentPrice,syncUnitsFieldForType:function(e,t){const n=document.querySelector(".units-inline-label");if(n&&(n.textContent="assisted"===e?"beds":"units"),"assisted"===e&&null!=t){const e=document.getElementById("ln-units-input");e&&(e.value=String(t)),g({numberOfUnits:t})}},updateActiveCapDisplay:function(){const r=document.getElementById("prop-cap");if(!r)return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o);r.textContent=a(m.baseNOI,c);const s=l(m.originalCapRate,m.isUsingEstimatedCapRate),p=`${"<strong>Reported cap rate:</strong> "+(null!=s?`${s}%`:"N/A")}${m.isUsingEstimatedCapRate?"<hr><em>Click the cap rate to increase by 1%; click the label to reset</em>":""}`,u=r.closest(".metric");if(u){const r=u.querySelector(".metric-label");e(u)?n(u,p):(t(u,p),r&&r.classList.add("has-tooltip"))}},updateCapRateLabel:function(){const e=document.querySelector("#prop-cap")?.closest(".metric")?.querySelector(".metric-label");if(e)switch(m.currentPropertyType){case"str":e.textContent="Cap Rate (STR)";break;case"assisted":e.textContent="Cap Rate (Assisted)";break;default:e.textContent="Cap Rate"}},updateElement:updateElement,updateEquityDisplay:function(){const r=document.getElementById("prop-equity");if(!r)return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o),s=m.cachedDebtBalance,a=null==s,l=p(c,s);r.textContent=`${Math.round(100*l)}%${a?"*":""}`;const fmtUSD=e=>Number.isFinite(Number(e))?`$${Math.round(Number(e)).toLocaleString()}`:"N/A",u=a?"Estimated — no debt data, assuming 100% equity":"scraped"===m.equitySource?"Public records (API)":m.equitySource;let d=`<strong>Debt owing:</strong> ${a?"Unknown":fmtUSD(s)}`;d+=`<br><strong>Source:</strong> ${u}`,m.cachedDebtAddress&&(d+=`<br><strong>Matched:</strong> ${m.cachedDebtAddress}`);const g=Array.isArray(m.cachedMortgages)?m.cachedMortgages:[];g.length&&(d+="<hr>"+g.map(e=>{const t=null!=e.interestRate&&""!==e.interestRate?` @ ${e.interestRate}%`:"";return`<strong>${e.position||"Lien"}:</strong> ${fmtUSD(e.amount)} ${e.loanType||""}${t} (${e.lenderName||"?"})`}).join("<br>"));const f=r.closest(".metric");if(f){const r=f.querySelector(".metric-label");e(f)?n(f,d):(t(f,d),r&&r.classList.add("has-tooltip"))}},updateLeadStatusTooltip:function(r){const o=document.getElementById("prop-lead-status");if(!o)return;let c="No LOI data returned";r&&(c=`\n <strong>Contact:</strong> ${r.contactName} <br>\n ${r.opportunityAddress}\n `);const s=o.closest(".metric");if(s)if(e(s))n(s,c);else{t(s,c);const e=s.querySelector(".metric-label");e&&e.classList.add("has-tooltip")}},updatePercentageLabels:updatePercentageLabels,updatePriceLabel:function(){const e=document.querySelector("#prop-price")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=m.currentPriceDiscount>0?`Price (${m.currentPriceDiscount}%)`:"Price")}}}export{createRender};
2
2
  //# sourceMappingURL=render.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sources":["../../../src/browser/widget/render.js"],"sourcesContent":["// Render unit: the panel's DOM-painting helpers — price/label updates, the financial-metric\r\n// paint, the active-cap display, and the hover tooltips. Everything here reads ctx.state and\r\n// writes the DOM; the finance math it needs comes from the pure capRate helpers. Extracted\r\n// verbatim from createAnalyzer's render closures (T12 decompose).\r\n\r\nimport { attachTooltip, hasTooltip, updateTooltipContent } from \"../ui/tooltip-manager.js\";\r\nimport {\r\n generateCashFlowTooltipHTML,\r\n generateDownPaymentTooltipHTML,\r\n} from \"../financial/tooltip-content-generators.js\";\r\nimport { parseCashFlowData, parseFinancialData } from \"../financial/tooltip-calculations.js\";\r\nimport { computeActiveCapDisplay, parsePriceNumber, parseReportedCap } from \"../financial/capRate.js\";\r\n\r\nconst FINANCIAL_ELEMENT_IDS = [\r\n \"prop-noi\", \"prop-down\", \"prop-net\", \"prop-seller-fi\", \"prop-cocr-30\",\r\n \"prop-cocr-15\", \"prop-assignment\", \"prop-dscr\", \"prop-sf\", \"prop-cashflow\",\r\n];\r\n\r\nexport function createRender({ ctx }) {\r\n const { state, updateState } = ctx;\r\n\r\n function getCurrentPrice() {\r\n if (state.originalPrice && state.currentPriceDiscount > 0) {\r\n const numericPrice = parseFloat(state.originalPrice.replace(/[$,]/g, \"\"));\r\n const discountedPrice = numericPrice * (1 - state.currentPriceDiscount / 100);\r\n return `$${Math.round(discountedPrice).toLocaleString()}`;\r\n }\r\n return state.originalPrice;\r\n }\r\n\r\n function updatePriceLabel() {\r\n const priceLabelElement = document.querySelector(\"#prop-price\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (priceLabelElement) {\r\n priceLabelElement.textContent = state.currentPriceDiscount > 0 ? `Price (${state.currentPriceDiscount}%)` : \"Price\";\r\n }\r\n }\r\n\r\n function updatePercentageLabels() {\r\n const downLabelElement = document.querySelector(\"#prop-down\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (downLabelElement) downLabelElement.textContent = `Down (${state.currentDownPaymentPercent}%)`;\r\n\r\n const sellerFiLabelElement = document.querySelector(\"#prop-seller-fi\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (sellerFiLabelElement) sellerFiLabelElement.textContent = `Seller FI (${state.currentSellerFiPercent}%)`;\r\n\r\n const dscrLabelElement = document.querySelector(\"#prop-dscr\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (dscrLabelElement) dscrLabelElement.textContent = `DSCR (${state.currentDSCRPercent}%)`;\r\n }\r\n\r\n function updateElement(id, value) {\r\n const element = document.getElementById(id);\r\n if (element) element.textContent = value;\r\n }\r\n\r\n function updateCapRateLabel() {\r\n const capLabelElement = document.querySelector(\"#prop-cap\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (!capLabelElement) return;\r\n switch (state.currentPropertyType) {\r\n case \"str\": capLabelElement.textContent = \"Cap Rate (STR)\"; break;\r\n case \"assisted\": capLabelElement.textContent = \"Cap Rate (Assisted)\"; break;\r\n default: capLabelElement.textContent = \"Cap Rate\"; break;\r\n }\r\n }\r\n\r\n // The panel shows the ACTIVE cap rate = NOI / current price (discount-aware via\r\n // getCurrentPrice), so the displayed cap is always internally consistent with the NOI metric\r\n // — including STR/assisted, where NOI is the type estimate/bedroom value and the listed cap\r\n // never drove it. The REPORTED cap (the scraped value, only when it was a real non-estimated\r\n // cap) is shown on hover; \"N/A\" when none was reported. When the cap is an estimate the\r\n // tooltip also keeps the click-to-cycle hint (clicking the cap is a manual NOI override).\r\n function updateActiveCapDisplay() {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n if (!capElement) return;\r\n\r\n const priceText = getCurrentPrice() || document.getElementById(\"prop-price\")?.textContent || \"\";\r\n const price = parsePriceNumber(priceText);\r\n capElement.textContent = computeActiveCapDisplay(state.baseNOI, price);\r\n\r\n const reported = parseReportedCap(state.originalCapRate, state.isUsingEstimatedCapRate);\r\n const reportedLine = `<strong>Reported cap rate:</strong> ${reported != null ? `${reported}%` : \"N/A\"}`;\r\n const cycleHint = state.isUsingEstimatedCapRate\r\n ? \"<hr><em>Click the cap rate to increase by 1%; click the label to reset</em>\"\r\n : \"\";\r\n const tooltipContent = `${reportedLine}${cycleHint}`;\r\n\r\n const metric = capElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function syncUnitsFieldForType(propertyType, bedroomCount) {\r\n const label = document.querySelector(\".units-inline-label\");\r\n if (label) label.textContent = propertyType === \"assisted\" ? \"beds\" : \"units\";\r\n if (propertyType === \"assisted\" && bedroomCount != null) {\r\n const input = document.getElementById(\"ln-units-input\");\r\n if (input) input.value = String(bedroomCount);\r\n updateState({ numberOfUnits: bedroomCount });\r\n }\r\n }\r\n\r\n function updateLeadStatusTooltip(loiData) {\r\n const leadElement = document.getElementById(\"prop-lead-status\");\r\n if (!leadElement) return;\r\n let tooltipContent = \"No LOI data returned\";\r\n if (loiData) {\r\n tooltipContent = `\r\n <strong>Contact:</strong> ${loiData.contactName} <br>\r\n ${loiData.opportunityAddress}\r\n `;\r\n }\r\n const metric = leadElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateDownHoverTooltip() {\r\n const downElement = document.getElementById(\"prop-down\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!downElement || !priceElement || !noiElement) return;\r\n\r\n const financialData = parseFinancialData(priceElement.textContent, noiElement.textContent);\r\n if (financialData) {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n financialData.price,\r\n financialData.noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent\r\n );\r\n const metric = downElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateCashFlowHoverTooltip() {\r\n const cashFlowElement = document.getElementById(\"prop-cashflow\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n if (!cashFlowElement || !priceElement) return;\r\n\r\n const cashFlowData = parseCashFlowData(priceElement.textContent, cashFlowElement.textContent);\r\n if (cashFlowData) {\r\n const tooltipContent = generateCashFlowTooltipHTML(cashFlowData.price, cashFlowData.monthlyCashFlow);\r\n const metric = cashFlowElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function applyFinancials(financials) {\r\n let isCashFlowNegative = false;\r\n if (financials) {\r\n const elements = {\r\n \"prop-noi\": financials.noi,\r\n \"prop-down\": financials.down,\r\n \"prop-net\": financials.netToBuyer,\r\n \"prop-seller-fi\": financials.sellerFi,\r\n \"prop-cocr-30\": financials.cocr30,\r\n \"prop-cocr-15\": financials.priceForCOCR15,\r\n \"prop-assignment\": financials.assignment,\r\n \"prop-dscr\": financials.dscr,\r\n \"prop-sf\": financials.sfPayment,\r\n \"prop-cashflow\": financials.cashFlow,\r\n };\r\n isCashFlowNegative = financials.rawCashFlow < 0;\r\n for (const [id, value] of Object.entries(elements)) updateElement(id, value);\r\n setTimeout(() => {\r\n updateDownHoverTooltip();\r\n updateCashFlowHoverTooltip();\r\n }, 100);\r\n } else {\r\n FINANCIAL_ELEMENT_IDS.forEach((id) => updateElement(id, \"N/A\"));\r\n }\r\n\r\n updatePercentageLabels();\r\n const footer = document.getElementById(\"ln-footer\");\r\n if (footer) footer.classList.toggle(\"negative\", isCashFlowNegative);\r\n }\r\n\r\n return {\r\n applyFinancials,\r\n getCurrentPrice,\r\n syncUnitsFieldForType,\r\n updateActiveCapDisplay,\r\n updateCapRateLabel,\r\n updateElement,\r\n updateLeadStatusTooltip,\r\n updatePercentageLabels,\r\n updatePriceLabel,\r\n };\r\n}\r\n"],"names":["FINANCIAL_ELEMENT_IDS","createRender","ctx","state","updateState","getCurrentPrice","originalPrice","currentPriceDiscount","discountedPrice","parseFloat","replace","Math","round","toLocaleString","updatePercentageLabels","downLabelElement","document","querySelector","closest","textContent","currentDownPaymentPercent","sellerFiLabelElement","currentSellerFiPercent","dscrLabelElement","currentDSCRPercent","updateElement","id","value","element","getElementById","applyFinancials","financials","isCashFlowNegative","elements","noi","down","netToBuyer","sellerFi","cocr30","priceForCOCR15","assignment","dscr","sfPayment","cashFlow","rawCashFlow","Object","entries","setTimeout","downElement","priceElement","noiElement","financialData","parseFinancialData","tooltipContent","generateDownPaymentTooltipHTML","price","metric","hasTooltip","updateTooltipContent","attachTooltip","updateDownHoverTooltip","cashFlowElement","cashFlowData","parseCashFlowData","generateCashFlowTooltipHTML","monthlyCashFlow","label","classList","add","updateCashFlowHoverTooltip","forEach","footer","toggle","syncUnitsFieldForType","propertyType","bedroomCount","input","String","numberOfUnits","updateActiveCapDisplay","capElement","priceText","parsePriceNumber","computeActiveCapDisplay","baseNOI","reported","parseReportedCap","originalCapRate","isUsingEstimatedCapRate","updateCapRateLabel","capLabelElement","currentPropertyType","updateLeadStatusTooltip","loiData","leadElement","contactName","opportunityAddress","updatePriceLabel","priceLabelElement"],"mappings":"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"}
1
+ {"version":3,"file":"render.js","sources":["../../../src/browser/widget/render.js"],"sourcesContent":["// Render unit: the panel's DOM-painting helpers — price/label updates, the financial-metric\r\n// paint, the active-cap display, and the hover tooltips. Everything here reads ctx.state and\r\n// writes the DOM; the finance math it needs comes from the pure capRate helpers. Extracted\r\n// verbatim from createAnalyzer's render closures (T12 decompose).\r\n\r\nimport { attachTooltip, hasTooltip, updateTooltipContent } from \"../ui/tooltip-manager.js\";\r\nimport {\r\n generateCashFlowTooltipHTML,\r\n generateDownPaymentTooltipHTML,\r\n} from \"../financial/tooltip-content-generators.js\";\r\nimport { parseCashFlowData, parseFinancialData } from \"../financial/tooltip-calculations.js\";\r\nimport { computeActiveCapDisplay, parsePriceNumber, parseReportedCap } from \"../financial/capRate.js\";\r\nimport { equityPercentFromDebt } from \"../../financial/calculations.js\";\r\n\r\nconst FINANCIAL_ELEMENT_IDS = [\r\n \"prop-noi\", \"prop-down\", \"prop-net\", \"prop-seller-fi\", \"prop-cocr-30\",\r\n \"prop-cocr-15\", \"prop-assignment\", \"prop-dscr\", \"prop-sf\", \"prop-cashflow\",\r\n];\r\n\r\nexport function createRender({ ctx }) {\r\n const { state, updateState } = ctx;\r\n\r\n function getCurrentPrice() {\r\n if (state.originalPrice && state.currentPriceDiscount > 0) {\r\n const numericPrice = parseFloat(state.originalPrice.replace(/[$,]/g, \"\"));\r\n const discountedPrice = numericPrice * (1 - state.currentPriceDiscount / 100);\r\n return `$${Math.round(discountedPrice).toLocaleString()}`;\r\n }\r\n return state.originalPrice;\r\n }\r\n\r\n function updatePriceLabel() {\r\n const priceLabelElement = document.querySelector(\"#prop-price\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (priceLabelElement) {\r\n priceLabelElement.textContent = state.currentPriceDiscount > 0 ? `Price (${state.currentPriceDiscount}%)` : \"Price\";\r\n }\r\n }\r\n\r\n function updatePercentageLabels() {\r\n const downLabelElement = document.querySelector(\"#prop-down\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (downLabelElement) downLabelElement.textContent = `Down (${state.currentDownPaymentPercent}%)`;\r\n\r\n const sellerFiLabelElement = document.querySelector(\"#prop-seller-fi\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (sellerFiLabelElement) sellerFiLabelElement.textContent = `Seller FI (${state.currentSellerFiPercent}%)`;\r\n\r\n const dscrLabelElement = document.querySelector(\"#prop-dscr\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (dscrLabelElement) dscrLabelElement.textContent = `DSCR (${state.currentDSCRPercent}%)`;\r\n }\r\n\r\n function updateElement(id, value) {\r\n const element = document.getElementById(id);\r\n if (element) element.textContent = value;\r\n }\r\n\r\n function updateCapRateLabel() {\r\n const capLabelElement = document.querySelector(\"#prop-cap\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (!capLabelElement) return;\r\n switch (state.currentPropertyType) {\r\n case \"str\": capLabelElement.textContent = \"Cap Rate (STR)\"; break;\r\n case \"assisted\": capLabelElement.textContent = \"Cap Rate (Assisted)\"; break;\r\n default: capLabelElement.textContent = \"Cap Rate\"; break;\r\n }\r\n }\r\n\r\n // The panel shows the ACTIVE cap rate = NOI / current price (discount-aware via\r\n // getCurrentPrice), so the displayed cap is always internally consistent with the NOI metric\r\n // — including STR/assisted, where NOI is the type estimate/bedroom value and the listed cap\r\n // never drove it. The REPORTED cap (the scraped value, only when it was a real non-estimated\r\n // cap) is shown on hover; \"N/A\" when none was reported. When the cap is an estimate the\r\n // tooltip also keeps the click-to-cycle hint (clicking the cap is a manual NOI override).\r\n function updateActiveCapDisplay() {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n if (!capElement) return;\r\n\r\n const priceText = getCurrentPrice() || document.getElementById(\"prop-price\")?.textContent || \"\";\r\n const price = parsePriceNumber(priceText);\r\n capElement.textContent = computeActiveCapDisplay(state.baseNOI, price);\r\n\r\n const reported = parseReportedCap(state.originalCapRate, state.isUsingEstimatedCapRate);\r\n const reportedLine = `<strong>Reported cap rate:</strong> ${reported != null ? `${reported}%` : \"N/A\"}`;\r\n const cycleHint = state.isUsingEstimatedCapRate\r\n ? \"<hr><em>Click the cap rate to increase by 1%; click the label to reset</em>\"\r\n : \"\";\r\n const tooltipContent = `${reportedLine}${cycleHint}`;\r\n\r\n const metric = capElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n // Equity is DERIVED, not fetched: (price - debt) / price against the current (discount-aware)\r\n // price, so it recomputes on every price edit. No debt figure => 100% (\"estimated\", marked *).\r\n // The hover reveals the $ debt, the recorded liens, the matched address, and how it was acquired.\r\n function updateEquityDisplay() {\r\n const equityElement = document.getElementById(\"prop-equity\");\r\n if (!equityElement) return;\r\n\r\n const priceText = getCurrentPrice() || document.getElementById(\"prop-price\")?.textContent || \"\";\r\n const price = parsePriceNumber(priceText);\r\n const debt = state.cachedDebtBalance;\r\n const estimated = debt === null || debt === undefined;\r\n const equity = equityPercentFromDebt(price, debt);\r\n equityElement.textContent = `${Math.round(equity * 100)}%${estimated ? \"*\" : \"\"}`;\r\n\r\n const fmtUSD = (n) => (Number.isFinite(Number(n)) ? `$${Math.round(Number(n)).toLocaleString()}` : \"N/A\");\r\n const sourceLabel = estimated\r\n ? \"Estimated — no debt data, assuming 100% equity\"\r\n : (state.equitySource === \"scraped\" ? \"Public records (API)\" : state.equitySource);\r\n\r\n let tooltip = `<strong>Debt owing:</strong> ${estimated ? \"Unknown\" : fmtUSD(debt)}`;\r\n tooltip += `<br><strong>Source:</strong> ${sourceLabel}`;\r\n if (state.cachedDebtAddress) tooltip += `<br><strong>Matched:</strong> ${state.cachedDebtAddress}`;\r\n\r\n const mortgages = Array.isArray(state.cachedMortgages) ? state.cachedMortgages : [];\r\n if (mortgages.length) {\r\n tooltip += \"<hr>\" + mortgages.map((m) => {\r\n const rate = (m.interestRate != null && m.interestRate !== \"\") ? ` @ ${m.interestRate}%` : \"\";\r\n return `<strong>${m.position || \"Lien\"}:</strong> ${fmtUSD(m.amount)} ${m.loanType || \"\"}${rate} (${m.lenderName || \"?\"})`;\r\n }).join(\"<br>\");\r\n }\r\n\r\n const metric = equityElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltip);\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltip);\r\n }\r\n }\r\n }\r\n\r\n function syncUnitsFieldForType(propertyType, bedroomCount) {\r\n const label = document.querySelector(\".units-inline-label\");\r\n if (label) label.textContent = propertyType === \"assisted\" ? \"beds\" : \"units\";\r\n if (propertyType === \"assisted\" && bedroomCount != null) {\r\n const input = document.getElementById(\"ln-units-input\");\r\n if (input) input.value = String(bedroomCount);\r\n updateState({ numberOfUnits: bedroomCount });\r\n }\r\n }\r\n\r\n function updateLeadStatusTooltip(loiData) {\r\n const leadElement = document.getElementById(\"prop-lead-status\");\r\n if (!leadElement) return;\r\n let tooltipContent = \"No LOI data returned\";\r\n if (loiData) {\r\n tooltipContent = `\r\n <strong>Contact:</strong> ${loiData.contactName} <br>\r\n ${loiData.opportunityAddress}\r\n `;\r\n }\r\n const metric = leadElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateDownHoverTooltip() {\r\n const downElement = document.getElementById(\"prop-down\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!downElement || !priceElement || !noiElement) return;\r\n\r\n const financialData = parseFinancialData(priceElement.textContent, noiElement.textContent);\r\n if (financialData) {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n financialData.price,\r\n financialData.noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent\r\n );\r\n const metric = downElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateCashFlowHoverTooltip() {\r\n const cashFlowElement = document.getElementById(\"prop-cashflow\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n if (!cashFlowElement || !priceElement) return;\r\n\r\n const cashFlowData = parseCashFlowData(priceElement.textContent, cashFlowElement.textContent);\r\n if (cashFlowData) {\r\n const tooltipContent = generateCashFlowTooltipHTML(cashFlowData.price, cashFlowData.monthlyCashFlow);\r\n const metric = cashFlowElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function applyFinancials(financials) {\r\n let isCashFlowNegative = false;\r\n if (financials) {\r\n const elements = {\r\n \"prop-noi\": financials.noi,\r\n \"prop-down\": financials.down,\r\n \"prop-net\": financials.netToBuyer,\r\n \"prop-seller-fi\": financials.sellerFi,\r\n \"prop-cocr-30\": financials.cocr30,\r\n \"prop-cocr-15\": financials.priceForCOCR15,\r\n \"prop-assignment\": financials.assignment,\r\n \"prop-dscr\": financials.dscr,\r\n \"prop-sf\": financials.sfPayment,\r\n \"prop-cashflow\": financials.cashFlow,\r\n };\r\n isCashFlowNegative = financials.rawCashFlow < 0;\r\n for (const [id, value] of Object.entries(elements)) updateElement(id, value);\r\n setTimeout(() => {\r\n updateDownHoverTooltip();\r\n updateCashFlowHoverTooltip();\r\n }, 100);\r\n } else {\r\n FINANCIAL_ELEMENT_IDS.forEach((id) => updateElement(id, \"N/A\"));\r\n }\r\n\r\n updatePercentageLabels();\r\n const footer = document.getElementById(\"ln-footer\");\r\n if (footer) footer.classList.toggle(\"negative\", isCashFlowNegative);\r\n }\r\n\r\n return {\r\n applyFinancials,\r\n getCurrentPrice,\r\n syncUnitsFieldForType,\r\n updateActiveCapDisplay,\r\n updateCapRateLabel,\r\n updateElement,\r\n updateEquityDisplay,\r\n updateLeadStatusTooltip,\r\n updatePercentageLabels,\r\n updatePriceLabel,\r\n };\r\n}\r\n"],"names":["FINANCIAL_ELEMENT_IDS","createRender","ctx","state","updateState","getCurrentPrice","originalPrice","currentPriceDiscount","discountedPrice","parseFloat","replace","Math","round","toLocaleString","updatePercentageLabels","downLabelElement","document","querySelector","closest","textContent","currentDownPaymentPercent","sellerFiLabelElement","currentSellerFiPercent","dscrLabelElement","currentDSCRPercent","updateElement","id","value","element","getElementById","applyFinancials","financials","isCashFlowNegative","elements","noi","down","netToBuyer","sellerFi","cocr30","priceForCOCR15","assignment","dscr","sfPayment","cashFlow","rawCashFlow","Object","entries","setTimeout","downElement","priceElement","noiElement","financialData","parseFinancialData","tooltipContent","generateDownPaymentTooltipHTML","price","metric","hasTooltip","updateTooltipContent","attachTooltip","updateDownHoverTooltip","cashFlowElement","cashFlowData","parseCashFlowData","generateCashFlowTooltipHTML","monthlyCashFlow","label","classList","add","updateCashFlowHoverTooltip","forEach","footer","toggle","syncUnitsFieldForType","propertyType","bedroomCount","input","String","numberOfUnits","updateActiveCapDisplay","capElement","priceText","parsePriceNumber","computeActiveCapDisplay","baseNOI","reported","parseReportedCap","originalCapRate","isUsingEstimatedCapRate","updateCapRateLabel","capLabelElement","currentPropertyType","updateEquityDisplay","equityElement","debt","cachedDebtBalance","estimated","equity","equityPercentFromDebt","fmtUSD","n","Number","isFinite","sourceLabel","equitySource","tooltip","cachedDebtAddress","mortgages","Array","isArray","cachedMortgages","length","map","m","rate","interestRate","position","amount","loanType","lenderName","join","updateLeadStatusTooltip","loiData","leadElement","contactName","opportunityAddress","updatePriceLabel","priceLabelElement"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{fetchEquity as t}from"../services/equity.js";import{fetchStrRevenue as e}from"../services/str-revenue.js";import{lookupLOI as a}from"../../services/loi-lookup.js";import{MATCH_TYPES as o,LOI_SENT_STATUS as c}from"../../config/loi-lookup.js";function createServices({ctx:r}){const{state:n,updateState:i}=r;return{ensureEquityLoaded:async function(e){if(n.cachedEquity)return n.cachedEquity;try{const a=new Promise(t=>setTimeout(()=>t("100%"),3e3)),o=t(e).then(t=>t??"100%").catch(()=>"100%");return await Promise.race([o,a])}catch(t){return console.error("❌ Error ensuring equity loaded:",t),"100%"}},loadEquity:async function(e,a){if(n.cachedEquity)return n.cachedEquity;i({equityLoadingStartTime:Date.now()});const o=document.getElementById("prop-equity");o&&(o.classList.add("loading"),o.textContent="");let c=null;try{c=await t(e)}catch(t){console.error("Error fetching equity:",t),c=null}const r=Date.now()-n.equityLoadingStartTime;return await new Promise(t=>setTimeout(t,Math.max(0,2e3-r))),a.isStale()?null:(o&&o.classList.remove("loading"),null!=c?(i({equitySource:"scraped",cachedEquity:c}),c):(i({cachedEquity:"100%*"}),n.cachedEquity))},loadLeadStatus:async function(t){if(n.cachedLoiData||i({cachedLoiData:{}}),n.cachedLoiData.leadStatus)return{leadStatus:n.cachedLoiData.leadStatus,contactName:n.cachedLoiData.contactName,opportunityAddress:n.cachedLoiData.opportunityAddress};try{const e=await a(t);let r="New Lead";const n=e?.data||{};switch(e?.matchType){case o.NO_RESPONSE:n.contactName="LOI lookup failed",n.opportunityAddress="to supply a response";break;case o.NO_MATCH:n.contactName="LOI lookup replied with",n.opportunityAddress="no match found";break;case o.EXACT:case o.FUZZY:r=c;break;default:n.contactName="LOI lookup failed",n.opportunityAddress="unknown match type"}const s={leadStatus:r,contactName:n.contactName||"No contact available",opportunityAddress:n.opportunityAddress||"(Unknown)"};return i({cachedLoiData:s}),s}catch(t){return console.error("💥 Error fetching lead status:",t),{leadStatus:"New Lead*",contactName:"No contact available",opportunityAddress:"Error fetching address"}}},loadStrValue:async function(t,a){if(n.cachedStrValue)return n.cachedStrValue;const o=document.getElementById("prop-noi");o&&o.classList.add("loading");try{const o=await e(t);return a.isStale()?null:o&&Number.isFinite(o.value)&&("noi"===o.type||"gross"===o.type)?(i({cachedStrValue:o}),o):null}catch(t){return console.error("Error fetching STR revenue:",t),null}finally{!a.isStale()&&o&&o.classList.remove("loading")}}}}export{createServices};
1
+ import{fetchDebt as e}from"../../services/debt.js";import{fetchStrRevenue as t}from"../services/str-revenue.js";import{lookupLOI as a}from"../../services/loi-lookup.js";import{MATCH_TYPES as r,LOI_SENT_STATUS as o}from"../../config/loi-lookup.js";function createServices({ctx:c}){const{state:s,updateState:n}=c;function applyDebtResult(e,t){e&&null!==e.estimatedMortgageBalance?n({cachedDebtAddress:e.address,cachedDebtBalance:e.estimatedMortgageBalance,cachedMortgages:Array.isArray(e.currentMortgages)?e.currentMortgages:[],debtLoaded:!0,equitySource:"scraped"}):n({cachedDebtAddress:e?.address??t,cachedDebtBalance:null,cachedMortgages:Array.isArray(e?.currentMortgages)?e.currentMortgages:[],debtLoaded:!0,equitySource:"estimated"})}return{ensureDebtLoaded:async function(t){if(!s.debtLoaded)try{const a=new Promise(e=>setTimeout(()=>e(null),3e3)),r=e(t).catch(()=>null);applyDebtResult(await Promise.race([r,a]),t)}catch(e){console.error("❌ Error ensuring debt loaded:",e),applyDebtResult(null,t)}return{address:s.cachedDebtAddress,balance:s.cachedDebtBalance,mortgages:s.cachedMortgages,source:s.equitySource}},loadDebt:async function(t,a){if(s.debtLoaded)return;n({equityLoadingStartTime:Date.now()});const r=document.getElementById("prop-equity");r&&(r.classList.add("loading"),r.textContent="");let o=null;try{o=await e(t)}catch(e){console.error("Error fetching debt:",e),o=null}const c=Date.now()-s.equityLoadingStartTime;await new Promise(e=>setTimeout(e,Math.max(0,2e3-c))),a.isStale()||(r&&r.classList.remove("loading"),applyDebtResult(o,t))},loadLeadStatus:async function(e){if(s.cachedLoiData||n({cachedLoiData:{}}),s.cachedLoiData.leadStatus)return{leadStatus:s.cachedLoiData.leadStatus,contactName:s.cachedLoiData.contactName,opportunityAddress:s.cachedLoiData.opportunityAddress};try{const t=await a(e);let c="New Lead";const s=t?.data||{};switch(t?.matchType){case r.NO_RESPONSE:s.contactName="LOI lookup failed",s.opportunityAddress="to supply a response";break;case r.NO_MATCH:s.contactName="LOI lookup replied with",s.opportunityAddress="no match found";break;case r.EXACT:case r.FUZZY:c=o;break;default:s.contactName="LOI lookup failed",s.opportunityAddress="unknown match type"}const d={leadStatus:c,contactName:s.contactName||"No contact available",opportunityAddress:s.opportunityAddress||"(Unknown)"};return n({cachedLoiData:d}),d}catch(e){return console.error("💥 Error fetching lead status:",e),{leadStatus:"New Lead*",contactName:"No contact available",opportunityAddress:"Error fetching address"}}},loadStrValue:async function(e,a){if(s.cachedStrValue)return s.cachedStrValue;const r=document.getElementById("prop-noi");r&&r.classList.add("loading");try{const r=await t(e);return a.isStale()?null:r&&Number.isFinite(r.value)&&("noi"===r.type||"gross"===r.type)?(n({cachedStrValue:r}),r):null}catch(e){return console.error("Error fetching STR revenue:",e),null}finally{!a.isStale()&&r&&r.classList.remove("loading")}}}}export{createServices};
2
2
  //# sourceMappingURL=services.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"services.js","sources":["../../../src/browser/widget/services.js"],"sourcesContent":["// Services unit: the orchestration around the agnostic fetchers (caching, the panel loading\r\n// indicator, the 2s minimum loading, the stale-drop via the pipeline guard, the provenance +\r\n// fallbacks). The fetchers themselves stay pure IO in ../services. Extracted verbatim (T12).\r\n\r\nimport { fetchEquity } from \"../services/equity.js\";\r\nimport { fetchStrRevenue } from \"../services/str-revenue.js\";\r\nimport { lookupLOI } from \"../../services/loi-lookup.js\";\r\nimport { LOI_SENT_STATUS, MATCH_TYPES } from \"../../config/loi-lookup.js\";\r\n\r\nexport function createServices({ ctx }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Caching + match-type mapping for the LOI lead status. lookupLOI is the agnostic call; the\r\n // engine owns the cache (ctx.state.cachedLoiData) and the mapping.\r\n async function loadLeadStatus(address) {\r\n if (!state.cachedLoiData) updateState({ cachedLoiData: {} });\r\n if (state.cachedLoiData.leadStatus) {\r\n return {\r\n leadStatus: state.cachedLoiData.leadStatus,\r\n contactName: state.cachedLoiData.contactName,\r\n opportunityAddress: state.cachedLoiData.opportunityAddress,\r\n };\r\n }\r\n try {\r\n const result = await lookupLOI(address);\r\n let leadStatus = \"New Lead\";\r\n const data = result?.data || {};\r\n switch (result?.matchType) {\r\n case MATCH_TYPES.NO_RESPONSE:\r\n data.contactName = \"LOI lookup failed\";\r\n data.opportunityAddress = \"to supply a response\";\r\n break;\r\n case MATCH_TYPES.NO_MATCH:\r\n data.contactName = \"LOI lookup replied with\";\r\n data.opportunityAddress = \"no match found\";\r\n break;\r\n case MATCH_TYPES.EXACT:\r\n case MATCH_TYPES.FUZZY:\r\n leadStatus = LOI_SENT_STATUS;\r\n break;\r\n default:\r\n data.contactName = \"LOI lookup failed\";\r\n data.opportunityAddress = \"unknown match type\";\r\n break;\r\n }\r\n const loiData = {\r\n leadStatus,\r\n contactName: data.contactName || \"No contact available\",\r\n opportunityAddress: data.opportunityAddress || \"(Unknown)\",\r\n };\r\n updateState({ cachedLoiData: loiData });\r\n return loiData;\r\n } catch (error) {\r\n console.error(\"💥 Error fetching lead status:\", error);\r\n return {\r\n leadStatus: \"New Lead*\",\r\n contactName: \"No contact available\",\r\n opportunityAddress: \"Error fetching address\",\r\n };\r\n }\r\n }\r\n\r\n // Equity orchestration: cache-first, panel loading indicator, 2s minimum loading, stale-drop\r\n // via the pipeline guard, provenance + fallback.\r\n async function loadEquity(address, guard) {\r\n if (state.cachedEquity) return state.cachedEquity;\r\n\r\n updateState({ equityLoadingStartTime: Date.now() });\r\n const equityElement = document.getElementById(\"prop-equity\");\r\n if (equityElement) {\r\n equityElement.classList.add(\"loading\");\r\n equityElement.textContent = \"\";\r\n }\r\n\r\n let value = null;\r\n try {\r\n value = await fetchEquity(address);\r\n } catch (error) {\r\n console.error(\"Error fetching equity:\", error);\r\n value = null;\r\n }\r\n\r\n const elapsed = Date.now() - state.equityLoadingStartTime;\r\n await new Promise((resolve) => setTimeout(resolve, Math.max(0, 2000 - elapsed)));\r\n\r\n if (guard.isStale()) return null;\r\n if (equityElement) equityElement.classList.remove(\"loading\");\r\n\r\n if (value != null) {\r\n updateState({ equitySource: \"scraped\", cachedEquity: value });\r\n return value;\r\n }\r\n updateState({ cachedEquity: \"100%*\" });\r\n return state.cachedEquity;\r\n }\r\n\r\n // STR-revenue orchestration: cache-first, NOI loading indicator, stale-drop, payload\r\n // validation. Returns null until the backend ships.\r\n async function loadStrValue(address, guard) {\r\n if (state.cachedStrValue) return state.cachedStrValue;\r\n\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (noiElement) noiElement.classList.add(\"loading\");\r\n\r\n try {\r\n const data = await fetchStrRevenue(address);\r\n if (guard.isStale()) return null;\r\n if (data && Number.isFinite(data.value) && (data.type === \"noi\" || data.type === \"gross\")) {\r\n updateState({ cachedStrValue: data });\r\n return data;\r\n }\r\n return null;\r\n } catch (error) {\r\n console.error(\"Error fetching STR revenue:\", error);\r\n return null;\r\n } finally {\r\n if (!guard.isStale() && noiElement) noiElement.classList.remove(\"loading\");\r\n }\r\n }\r\n\r\n async function ensureEquityLoaded(address) {\r\n if (state.cachedEquity) return state.cachedEquity;\r\n try {\r\n const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(\"100%\"), 3000));\r\n const equityPromise = fetchEquity(address).then((v) => v ?? \"100%\").catch(() => \"100%\");\r\n return await Promise.race([equityPromise, timeoutPromise]);\r\n } catch (error) {\r\n console.error(\"❌ Error ensuring equity loaded:\", error);\r\n return \"100%\";\r\n }\r\n }\r\n\r\n return { ensureEquityLoaded, loadEquity, loadLeadStatus, loadStrValue };\r\n}\r\n"],"names":["createServices","ctx","state","updateState","ensureEquityLoaded","async","address","cachedEquity","timeoutPromise","Promise","resolve","setTimeout","equityPromise","fetchEquity","then","v","catch","race","error","console","loadEquity","guard","equityLoadingStartTime","Date","now","equityElement","document","getElementById","classList","add","textContent","value","elapsed","Math","max","isStale","remove","equitySource","loadLeadStatus","cachedLoiData","leadStatus","contactName","opportunityAddress","result","lookupLOI","data","matchType","MATCH_TYPES","NO_RESPONSE","NO_MATCH","EXACT","FUZZY","LOI_SENT_STATUS","loiData","loadStrValue","cachedStrValue","noiElement","fetchStrRevenue","Number","isFinite","type"],"mappings":"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"}
1
+ {"version":3,"file":"services.js","sources":["../../../src/browser/widget/services.js"],"sourcesContent":["// Services unit: the orchestration around the agnostic fetchers (caching, the panel loading\r\n// indicator, the 2s minimum loading, the stale-drop via the pipeline guard, the provenance +\r\n// fallbacks). The fetchers themselves stay pure IO in ../services. Extracted verbatim (T12).\r\n\r\nimport { fetchDebt } from \"../../services/debt.js\";\r\nimport { fetchStrRevenue } from \"../services/str-revenue.js\";\r\nimport { lookupLOI } from \"../../services/loi-lookup.js\";\r\nimport { LOI_SENT_STATUS, MATCH_TYPES } from \"../../config/loi-lookup.js\";\r\n\r\nexport function createServices({ ctx }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Caching + match-type mapping for the LOI lead status. lookupLOI is the agnostic call; the\r\n // engine owns the cache (ctx.state.cachedLoiData) and the mapping.\r\n async function loadLeadStatus(address) {\r\n if (!state.cachedLoiData) updateState({ cachedLoiData: {} });\r\n if (state.cachedLoiData.leadStatus) {\r\n return {\r\n leadStatus: state.cachedLoiData.leadStatus,\r\n contactName: state.cachedLoiData.contactName,\r\n opportunityAddress: state.cachedLoiData.opportunityAddress,\r\n };\r\n }\r\n try {\r\n const result = await lookupLOI(address);\r\n let leadStatus = \"New Lead\";\r\n const data = result?.data || {};\r\n switch (result?.matchType) {\r\n case MATCH_TYPES.NO_RESPONSE:\r\n data.contactName = \"LOI lookup failed\";\r\n data.opportunityAddress = \"to supply a response\";\r\n break;\r\n case MATCH_TYPES.NO_MATCH:\r\n data.contactName = \"LOI lookup replied with\";\r\n data.opportunityAddress = \"no match found\";\r\n break;\r\n case MATCH_TYPES.EXACT:\r\n case MATCH_TYPES.FUZZY:\r\n leadStatus = LOI_SENT_STATUS;\r\n break;\r\n default:\r\n data.contactName = \"LOI lookup failed\";\r\n data.opportunityAddress = \"unknown match type\";\r\n break;\r\n }\r\n const loiData = {\r\n leadStatus,\r\n contactName: data.contactName || \"No contact available\",\r\n opportunityAddress: data.opportunityAddress || \"(Unknown)\",\r\n };\r\n updateState({ cachedLoiData: loiData });\r\n return loiData;\r\n } catch (error) {\r\n console.error(\"💥 Error fetching lead status:\", error);\r\n return {\r\n leadStatus: \"New Lead*\",\r\n contactName: \"No contact available\",\r\n opportunityAddress: \"Error fetching address\",\r\n };\r\n }\r\n }\r\n\r\n // Apply a fetchDebt result (or a null/failed fetch) to ctx. A numeric balance is provenance\r\n // \"scraped\"; no number means the service had no figure, so equity falls back to 100%\r\n // (\"estimated\"). debtLoaded guards against re-fetching when the balance is legitimately null.\r\n function applyDebtResult(result, address) {\r\n if (result && result.estimatedMortgageBalance !== null) {\r\n updateState({\r\n cachedDebtAddress: result.address,\r\n cachedDebtBalance: result.estimatedMortgageBalance,\r\n cachedMortgages: Array.isArray(result.currentMortgages) ? result.currentMortgages : [],\r\n debtLoaded: true,\r\n equitySource: \"scraped\",\r\n });\r\n } else {\r\n updateState({\r\n cachedDebtAddress: result?.address ?? address,\r\n cachedDebtBalance: null,\r\n cachedMortgages: Array.isArray(result?.currentMortgages) ? result.currentMortgages : [],\r\n debtLoaded: true,\r\n equitySource: \"estimated\",\r\n });\r\n }\r\n }\r\n\r\n // Debt orchestration: cache-first, panel loading indicator, 2s minimum loading, stale-drop\r\n // via the pipeline guard. Stores the debt; the panel DERIVES equity from it vs the current\r\n // price (render.updateEquityDisplay), so equity tracks user price edits.\r\n async function loadDebt(address, guard) {\r\n if (state.debtLoaded) return;\r\n\r\n updateState({ equityLoadingStartTime: Date.now() });\r\n const equityElement = document.getElementById(\"prop-equity\");\r\n if (equityElement) {\r\n equityElement.classList.add(\"loading\");\r\n equityElement.textContent = \"\";\r\n }\r\n\r\n let result = null;\r\n try {\r\n result = await fetchDebt(address);\r\n } catch (error) {\r\n console.error(\"Error fetching debt:\", error);\r\n result = null;\r\n }\r\n\r\n const elapsed = Date.now() - state.equityLoadingStartTime;\r\n await new Promise((resolve) => setTimeout(resolve, Math.max(0, 2000 - elapsed)));\r\n\r\n if (guard.isStale()) return;\r\n if (equityElement) equityElement.classList.remove(\"loading\");\r\n\r\n applyDebtResult(result, address);\r\n }\r\n\r\n // STR-revenue orchestration: cache-first, NOI loading indicator, stale-drop, payload\r\n // validation. Returns null until the backend ships.\r\n async function loadStrValue(address, guard) {\r\n if (state.cachedStrValue) return state.cachedStrValue;\r\n\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (noiElement) noiElement.classList.add(\"loading\");\r\n\r\n try {\r\n const data = await fetchStrRevenue(address);\r\n if (guard.isStale()) return null;\r\n if (data && Number.isFinite(data.value) && (data.type === \"noi\" || data.type === \"gross\")) {\r\n updateState({ cachedStrValue: data });\r\n return data;\r\n }\r\n return null;\r\n } catch (error) {\r\n console.error(\"Error fetching STR revenue:\", error);\r\n return null;\r\n } finally {\r\n if (!guard.isStale() && noiElement) noiElement.classList.remove(\"loading\");\r\n }\r\n }\r\n\r\n // Export-time guarantee: ensure debt is loaded (cache-first, 3s timeout => estimated), then\r\n // return the debt snapshot so the export carries the balance + mortgages and a price-derived\r\n // equity. Never throws; a failure becomes the \"estimated\" case.\r\n async function ensureDebtLoaded(address) {\r\n if (!state.debtLoaded) {\r\n try {\r\n const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 3000));\r\n const debtPromise = fetchDebt(address).catch(() => null);\r\n const result = await Promise.race([debtPromise, timeoutPromise]);\r\n applyDebtResult(result, address);\r\n } catch (error) {\r\n console.error(\"❌ Error ensuring debt loaded:\", error);\r\n applyDebtResult(null, address);\r\n }\r\n }\r\n return {\r\n address: state.cachedDebtAddress,\r\n balance: state.cachedDebtBalance,\r\n mortgages: state.cachedMortgages,\r\n source: state.equitySource,\r\n };\r\n }\r\n\r\n return { ensureDebtLoaded, loadDebt, loadLeadStatus, loadStrValue };\r\n}\r\n"],"names":["createServices","ctx","state","updateState","applyDebtResult","result","address","estimatedMortgageBalance","cachedDebtAddress","cachedDebtBalance","cachedMortgages","Array","isArray","currentMortgages","debtLoaded","equitySource","ensureDebtLoaded","async","timeoutPromise","Promise","resolve","setTimeout","debtPromise","fetchDebt","catch","race","error","console","balance","mortgages","source","loadDebt","guard","equityLoadingStartTime","Date","now","equityElement","document","getElementById","classList","add","textContent","elapsed","Math","max","isStale","remove","loadLeadStatus","cachedLoiData","leadStatus","contactName","opportunityAddress","lookupLOI","data","matchType","MATCH_TYPES","NO_RESPONSE","NO_MATCH","EXACT","FUZZY","LOI_SENT_STATUS","loiData","loadStrValue","cachedStrValue","noiElement","fetchStrRevenue","Number","isFinite","value","type"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- function mapPropertyType(e){return{assisted:"assisted",business:"business",mixed_use:"mixed_use",multifamily:"mfr",rv_park:"rv_park",str:"str"}[e]||"mfr"}function createExportObjectCore(e,t={}){const{cachedEquity:n=null,currentDownPaymentPercent:r,currentInterestRateType:a="dscr_residential",currentPriceDiscount:o=0,currentPropertyType:c="str",equitySource:i="scraped",isUsingEstimatedCapRate:u=!1,noi:s=null,numberOfUnits:p=4,priceWasDefaulted:d=!1,windowLocation:l=""}=t;if(d)return null;const f={};if(e.name&&"Property Details"!==e.name&&"Not found"!==e.name&&(f.address=e.name),e.capRate&&"Loading..."!==e.capRate&&"Not found"!==e.capRate){const t=e.capRate.match(/[\d.]+/);if(t){const n=parseFloat(t[0]);e.capRate.includes("%")||n>1?f.capRate=Math.round(n/100*1e6)/1e6:f.capRate=Math.round(1e6*n)/1e6}}if(f.capRateSource=u?"estimated":"scraped",e.contact&&"Not found"!==e.contact&&(f.contact=e.contact),e.listingDate&&"Not found"!==e.listingDate&&(f.dateListed=e.listingDate),e.price&&"Loading..."!==e.price&&"Not found"!==e.price){const t=e.price.match(/[\d,]+/);if(t){const e=parseFloat(t[0].replace(/,/g,""));if(o>0){const t=e/(1-o/100);f.price=Math.round(t)}else f.price=e}}if(Number.isFinite(s)&&s>0&&(f.noi=Math.round(s)),void 0!==r&&(f.downPaymentPercent=Math.round(r/100*1e6)/1e6),n&&"Loading..."!==n){const e=n.match(/[\d.]+/);e&&(f.equityPercent=Math.round(parseFloat(e[0])/100*1e6)/1e6)}f.equitySource=i,f.numberOfUnits=p,e.phone&&"Not found"!==e.phone&&(f.phone=e.phone),f.priceDiscountPercent=o>0?Math.round(o/100*1e6)/1e6:0,f.interestRateType=a,f.propertyType=mapPropertyType(c),f.url=l,console.log("exportData",f);const m={};return Object.keys(f).sort().forEach(e=>{m[e]=f[e]}),m}function calculateOriginalPrice(e,t){if(t>0){return e/(1-t/100)}return e}function convertCapRateToDecimal(e){if(!e||"Loading..."===e||"Not found"===e)return null;const t=e.match(/[\d.]+/);if(t){const n=parseFloat(t[0]);return e.includes("%")||n>1?Math.round(n/100*1e6)/1e6:Math.round(1e6*n)/1e6}return null}function formatDownPaymentPercent(e){return Math.round(e/100*1e6)/1e6}export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType};
1
+ import{equityPercentFromDebt as e}from"../financial/calculations.js";function mapPropertyType(e){return{assisted:"assisted",business:"business",mixed_use:"mixed_use",multifamily:"mfr",rv_park:"rv_park",str:"str"}[e]||"mfr"}function createExportObjectCore(t,r={}){const{currentDownPaymentPercent:n,currentInterestRateType:a="dscr_residential",currentMortgages:o=[],currentPriceDiscount:i=0,currentPropertyType:c="str",equitySource:s="scraped",estimatedMortgageBalance:u=null,isUsingEstimatedCapRate:p=!1,noi:d=null,numberOfUnits:l=4,priceWasDefaulted:m=!1,windowLocation:f=""}=r;if(m)return null;const y={};if(t.name&&"Property Details"!==t.name&&"Not found"!==t.name&&(y.address=t.name),t.capRate&&"Loading..."!==t.capRate&&"Not found"!==t.capRate){const e=t.capRate.match(/[\d.]+/);if(e){const r=parseFloat(e[0]);t.capRate.includes("%")||r>1?y.capRate=Math.round(r/100*1e6)/1e6:y.capRate=Math.round(1e6*r)/1e6}}if(y.capRateSource=p?"estimated":"scraped",t.contact&&"Not found"!==t.contact&&(y.contact=t.contact),t.listingDate&&"Not found"!==t.listingDate&&(y.dateListed=t.listingDate),t.price&&"Loading..."!==t.price&&"Not found"!==t.price){const e=t.price.match(/[\d,]+/);if(e){const t=parseFloat(e[0].replace(/,/g,""));if(i>0){const e=t/(1-i/100);y.price=Math.round(e)}else y.price=t}}if(Number.isFinite(d)&&d>0&&(y.noi=Math.round(d)),void 0!==n&&(y.downPaymentPercent=Math.round(n/100*1e6)/1e6),Number.isFinite(y.price)&&y.price>0){const t=e(y.price,u),r=Math.max(0,Math.min(1,t));y.equityPercent=Math.round(1e6*r)/1e6}y.equitySource=s,Number.isFinite(u)&&(y.estimatedMortgageBalance=Math.round(u)),Array.isArray(o)&&o.length>0&&(y.currentMortgages=JSON.stringify(o)),y.numberOfUnits=l,t.phone&&"Not found"!==t.phone&&(y.phone=t.phone),y.priceDiscountPercent=i>0?Math.round(i/100*1e6)/1e6:0,y.interestRateType=a,y.propertyType=mapPropertyType(c),y.url=f,console.log("exportData",y);const h={};return Object.keys(y).sort().forEach(e=>{h[e]=y[e]}),h}function calculateOriginalPrice(e,t){if(t>0){return e/(1-t/100)}return e}function convertCapRateToDecimal(e){if(!e||"Loading..."===e||"Not found"===e)return null;const t=e.match(/[\d.]+/);if(t){const r=parseFloat(t[0]);return e.includes("%")||r>1?Math.round(r/100*1e6)/1e6:Math.round(1e6*r)/1e6}return null}function formatDownPaymentPercent(e){return Math.round(e/100*1e6)/1e6}export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType};
2
2
  //# sourceMappingURL=export-logic.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"export-logic.js","sources":["../../src/export/export-logic.js"],"sourcesContent":["// Map the on-screen property type to the dashboard's DB enum. Only \"multifamily\" -> \"mfr\"\r\n// actually differs; the rest pass through. Mirrors property-dashboard/validation/property.js\r\n// mapPropertyType (same table, same unknown -> \"mfr\" default) so the export URL carries the\r\n// real enum instead of relying on the server to convert it.\r\nexport function mapPropertyType(type) {\r\n const typeMap = {\r\n assisted: \"assisted\",\r\n business: \"business\",\r\n mixed_use: \"mixed_use\",\r\n multifamily: \"mfr\",\r\n rv_park: \"rv_park\",\r\n str: \"str\",\r\n };\r\n return typeMap[type] || \"mfr\";\r\n}\r\n\r\n// Pure business logic for data export - no DOM, no Chrome APIs.\r\n// Returns null to REFUSE export when the price was defaulted (no real price found):\r\n// a fabricated price would flow into NOI and silently land garbage in the dashboard.\r\nexport function createExportObjectCore(data, options = {}) {\r\n const {\r\n cachedEquity = null,\r\n currentDownPaymentPercent,\r\n currentInterestRateType = \"dscr_residential\",\r\n currentPriceDiscount = 0,\r\n currentPropertyType = \"str\",\r\n equitySource = \"scraped\",\r\n isUsingEstimatedCapRate = false,\r\n noi = null,\r\n numberOfUnits = 4,\r\n priceWasDefaulted = false,\r\n windowLocation = \"\",\r\n } = options;\r\n\r\n if (priceWasDefaulted) return null;\r\n\r\n const exportData = {};\r\n\r\n // 1. Address\r\n if (data.name && data.name !== \"Property Details\" && data.name !== \"Not found\") {\r\n exportData.address = data.name;\r\n }\r\n\r\n // 2. Cap Rate - convert to decimal\r\n if (data.capRate && data.capRate !== \"Loading...\" && data.capRate !== \"Not found\") {\r\n const capMatch = data.capRate.match(/[\\d.]+/);\r\n if (capMatch) {\r\n const numericValue = parseFloat(capMatch[0]);\r\n\r\n // If the original string contains %, it's a percentage that needs conversion\r\n // If it's already a small decimal (< 1), it's likely already in decimal format\r\n if (data.capRate.includes(\"%\") || numericValue > 1) {\r\n // Percentage format - convert to decimal\r\n exportData.capRate = Math.round((numericValue / 100) * 1000000) / 1000000;\r\n } else {\r\n // Already in decimal format - use as-is\r\n exportData.capRate = Math.round(numericValue * 1000000) / 1000000;\r\n }\r\n }\r\n }\r\n\r\n // 3. Cap Rate Source\r\n exportData.capRateSource = isUsingEstimatedCapRate ? \"estimated\" : \"scraped\";\r\n\r\n // 4. Contact name\r\n if (data.contact && data.contact !== \"Not found\") {\r\n exportData.contact = data.contact;\r\n }\r\n\r\n // 5. Date Listed\r\n if (data.listingDate && data.listingDate !== \"Not found\") {\r\n exportData.dateListed = data.listingDate;\r\n }\r\n\r\n // 6. Price - calculate original price if discount applied\r\n if (data.price && data.price !== \"Loading...\" && data.price !== \"Not found\") {\r\n const priceMatch = data.price.match(/[\\d,]+/);\r\n if (priceMatch) {\r\n const displayedPrice = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n\r\n if (currentPriceDiscount > 0) {\r\n const discountDecimal = currentPriceDiscount / 100;\r\n const originalPrice = displayedPrice / (1 - discountDecimal);\r\n exportData.price = Math.round(originalPrice);\r\n } else {\r\n exportData.price = displayedPrice;\r\n }\r\n }\r\n }\r\n\r\n // 6b. NOI - the computed net operating income (additive; the dashboard stores it in the noi\r\n // column and derives the active cap rate as noi/price). The reported cap rate is carried\r\n // separately in capRate, unchanged. Omitted when no NOI was computed.\r\n if (Number.isFinite(noi) && noi > 0) {\r\n exportData.noi = Math.round(noi);\r\n }\r\n\r\n // 7. Down Payment Percent (user-controlled value)\r\n if (currentDownPaymentPercent !== undefined) {\r\n exportData.downPaymentPercent = Math.round((currentDownPaymentPercent / 100) * 1000000) / 1000000;\r\n }\r\n\r\n // 8. Equity Percent\r\n if (cachedEquity && cachedEquity !== \"Loading...\") {\r\n const equityMatch = cachedEquity.match(/[\\d.]+/);\r\n if (equityMatch) {\r\n exportData.equityPercent = Math.round((parseFloat(equityMatch[0]) / 100) * 1000000) / 1000000;\r\n }\r\n }\r\n\r\n // 9. Equity Source\r\n exportData.equitySource = equitySource;\r\n\r\n // 10. Number of Units\r\n exportData.numberOfUnits = numberOfUnits;\r\n\r\n // 11. Phone number\r\n if (data.phone && data.phone !== \"Not found\") {\r\n exportData.phone = data.phone;\r\n }\r\n\r\n // 11. Price Discount Percent\r\n if (currentPriceDiscount > 0) {\r\n exportData.priceDiscountPercent = Math.round((currentPriceDiscount / 100) * 1000000) / 1000000;\r\n } else {\r\n exportData.priceDiscountPercent = 0;\r\n }\r\n\r\n // 12. Interest Rate Type\r\n exportData.interestRateType = currentInterestRateType;\r\n\r\n // 13. Property Type - mapped to the DB enum (multifamily -> mfr; rest pass through)\r\n exportData.propertyType = mapPropertyType(currentPropertyType);\r\n\r\n // 13. URL\r\n exportData.url = windowLocation;\r\n\r\n console.log(\"exportData\", exportData);\r\n\r\n // Alphabetize keys\r\n const alphabetized = {};\r\n Object.keys(exportData).sort().forEach(key => {\r\n alphabetized[key] = exportData[key];\r\n });\r\n\r\n return alphabetized;\r\n}\r\n\r\n// Pure calculation functions\r\nexport function calculateOriginalPrice(displayedPrice, discountPercent) {\r\n if (discountPercent > 0) {\r\n const discountDecimal = discountPercent / 100;\r\n return displayedPrice / (1 - discountDecimal);\r\n }\r\n return displayedPrice;\r\n}\r\n\r\nexport function convertCapRateToDecimal(capRateString) {\r\n if (!capRateString || capRateString === \"Loading...\" || capRateString === \"Not found\") {\r\n return null;\r\n }\r\n\r\n const capMatch = capRateString.match(/[\\d.]+/);\r\n if (capMatch) {\r\n const numericValue = parseFloat(capMatch[0]);\r\n\r\n if (capRateString.includes(\"%\") || numericValue > 1) {\r\n return Math.round((numericValue / 100) * 1000000) / 1000000;\r\n } else {\r\n return Math.round(numericValue * 1000000) / 1000000;\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\nexport function formatDownPaymentPercent(percentage) {\r\n return Math.round((percentage / 100) * 1000000) / 1000000;\r\n}\r\n"],"names":["mapPropertyType","type","assisted","business","mixed_use","multifamily","rv_park","str","createExportObjectCore","data","options","cachedEquity","currentDownPaymentPercent","currentInterestRateType","currentPriceDiscount","currentPropertyType","equitySource","isUsingEstimatedCapRate","noi","numberOfUnits","priceWasDefaulted","windowLocation","exportData","name","address","capRate","capMatch","match","numericValue","parseFloat","includes","Math","round","capRateSource","contact","listingDate","dateListed","price","priceMatch","displayedPrice","replace","originalPrice","Number","isFinite","undefined","downPaymentPercent","equityMatch","equityPercent","phone","priceDiscountPercent","interestRateType","propertyType","url","console","log","alphabetized","Object","keys","sort","forEach","key","calculateOriginalPrice","discountPercent","convertCapRateToDecimal","capRateString","formatDownPaymentPercent","percentage"],"mappings":"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"}
1
+ {"version":3,"file":"export-logic.js","sources":["../../src/export/export-logic.js"],"sourcesContent":["import { equityPercentFromDebt } from \"../financial/calculations.js\";\r\n\r\n// Map the on-screen property type to the dashboard's DB enum. Only \"multifamily\" -> \"mfr\"\r\n// actually differs; the rest pass through. Mirrors property-dashboard/validation/property.js\r\n// mapPropertyType (same table, same unknown -> \"mfr\" default) so the export URL carries the\r\n// real enum instead of relying on the server to convert it.\r\nexport function mapPropertyType(type) {\r\n const typeMap = {\r\n assisted: \"assisted\",\r\n business: \"business\",\r\n mixed_use: \"mixed_use\",\r\n multifamily: \"mfr\",\r\n rv_park: \"rv_park\",\r\n str: \"str\",\r\n };\r\n return typeMap[type] || \"mfr\";\r\n}\r\n\r\n// Pure business logic for data export - no DOM, no Chrome APIs.\r\n// Returns null to REFUSE export when the price was defaulted (no real price found):\r\n// a fabricated price would flow into NOI and silently land garbage in the dashboard.\r\nexport function createExportObjectCore(data, options = {}) {\r\n const {\r\n currentDownPaymentPercent,\r\n currentInterestRateType = \"dscr_residential\",\r\n currentMortgages = [],\r\n currentPriceDiscount = 0,\r\n currentPropertyType = \"str\",\r\n equitySource = \"scraped\",\r\n estimatedMortgageBalance = null,\r\n isUsingEstimatedCapRate = false,\r\n noi = null,\r\n numberOfUnits = 4,\r\n priceWasDefaulted = false,\r\n windowLocation = \"\",\r\n } = options;\r\n\r\n if (priceWasDefaulted) return null;\r\n\r\n const exportData = {};\r\n\r\n // 1. Address\r\n if (data.name && data.name !== \"Property Details\" && data.name !== \"Not found\") {\r\n exportData.address = data.name;\r\n }\r\n\r\n // 2. Cap Rate - convert to decimal\r\n if (data.capRate && data.capRate !== \"Loading...\" && data.capRate !== \"Not found\") {\r\n const capMatch = data.capRate.match(/[\\d.]+/);\r\n if (capMatch) {\r\n const numericValue = parseFloat(capMatch[0]);\r\n\r\n // If the original string contains %, it's a percentage that needs conversion\r\n // If it's already a small decimal (< 1), it's likely already in decimal format\r\n if (data.capRate.includes(\"%\") || numericValue > 1) {\r\n // Percentage format - convert to decimal\r\n exportData.capRate = Math.round((numericValue / 100) * 1000000) / 1000000;\r\n } else {\r\n // Already in decimal format - use as-is\r\n exportData.capRate = Math.round(numericValue * 1000000) / 1000000;\r\n }\r\n }\r\n }\r\n\r\n // 3. Cap Rate Source\r\n exportData.capRateSource = isUsingEstimatedCapRate ? \"estimated\" : \"scraped\";\r\n\r\n // 4. Contact name\r\n if (data.contact && data.contact !== \"Not found\") {\r\n exportData.contact = data.contact;\r\n }\r\n\r\n // 5. Date Listed\r\n if (data.listingDate && data.listingDate !== \"Not found\") {\r\n exportData.dateListed = data.listingDate;\r\n }\r\n\r\n // 6. Price - calculate original price if discount applied\r\n if (data.price && data.price !== \"Loading...\" && data.price !== \"Not found\") {\r\n const priceMatch = data.price.match(/[\\d,]+/);\r\n if (priceMatch) {\r\n const displayedPrice = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n\r\n if (currentPriceDiscount > 0) {\r\n const discountDecimal = currentPriceDiscount / 100;\r\n const originalPrice = displayedPrice / (1 - discountDecimal);\r\n exportData.price = Math.round(originalPrice);\r\n } else {\r\n exportData.price = displayedPrice;\r\n }\r\n }\r\n }\r\n\r\n // 6b. NOI - the computed net operating income (additive; the dashboard stores it in the noi\r\n // column and derives the active cap rate as noi/price). The reported cap rate is carried\r\n // separately in capRate, unchanged. Omitted when no NOI was computed.\r\n if (Number.isFinite(noi) && noi > 0) {\r\n exportData.noi = Math.round(noi);\r\n }\r\n\r\n // 7. Down Payment Percent (user-controlled value)\r\n if (currentDownPaymentPercent !== undefined) {\r\n exportData.downPaymentPercent = Math.round((currentDownPaymentPercent / 100) * 1000000) / 1000000;\r\n }\r\n\r\n // 8. Equity Percent — DERIVED from scraped debt vs the export price (no debt => 100%).\r\n // Clamped to [0,1] for the dashboard's equity_percent CHECK; the live panel shows reality.\r\n if (Number.isFinite(exportData.price) && exportData.price > 0) {\r\n const equity = equityPercentFromDebt(exportData.price, estimatedMortgageBalance);\r\n const clamped = Math.max(0, Math.min(1, equity));\r\n exportData.equityPercent = Math.round(clamped * 1000000) / 1000000;\r\n }\r\n\r\n // 9. Equity Source\r\n exportData.equitySource = equitySource;\r\n\r\n // 9b. Scraped debt (additive; dashboard stores estimated_debt_balance + scraped_mortgages,\r\n // distinct from the loan_1/2/3 due-diligence slots). Omitted when there is no figure.\r\n if (Number.isFinite(estimatedMortgageBalance)) {\r\n exportData.estimatedMortgageBalance = Math.round(estimatedMortgageBalance);\r\n }\r\n if (Array.isArray(currentMortgages) && currentMortgages.length > 0) {\r\n exportData.currentMortgages = JSON.stringify(currentMortgages);\r\n }\r\n\r\n // 10. Number of Units\r\n exportData.numberOfUnits = numberOfUnits;\r\n\r\n // 11. Phone number\r\n if (data.phone && data.phone !== \"Not found\") {\r\n exportData.phone = data.phone;\r\n }\r\n\r\n // 11. Price Discount Percent\r\n if (currentPriceDiscount > 0) {\r\n exportData.priceDiscountPercent = Math.round((currentPriceDiscount / 100) * 1000000) / 1000000;\r\n } else {\r\n exportData.priceDiscountPercent = 0;\r\n }\r\n\r\n // 12. Interest Rate Type\r\n exportData.interestRateType = currentInterestRateType;\r\n\r\n // 13. Property Type - mapped to the DB enum (multifamily -> mfr; rest pass through)\r\n exportData.propertyType = mapPropertyType(currentPropertyType);\r\n\r\n // 13. URL\r\n exportData.url = windowLocation;\r\n\r\n console.log(\"exportData\", exportData);\r\n\r\n // Alphabetize keys\r\n const alphabetized = {};\r\n Object.keys(exportData).sort().forEach(key => {\r\n alphabetized[key] = exportData[key];\r\n });\r\n\r\n return alphabetized;\r\n}\r\n\r\n// Pure calculation functions\r\nexport function calculateOriginalPrice(displayedPrice, discountPercent) {\r\n if (discountPercent > 0) {\r\n const discountDecimal = discountPercent / 100;\r\n return displayedPrice / (1 - discountDecimal);\r\n }\r\n return displayedPrice;\r\n}\r\n\r\nexport function convertCapRateToDecimal(capRateString) {\r\n if (!capRateString || capRateString === \"Loading...\" || capRateString === \"Not found\") {\r\n return null;\r\n }\r\n\r\n const capMatch = capRateString.match(/[\\d.]+/);\r\n if (capMatch) {\r\n const numericValue = parseFloat(capMatch[0]);\r\n\r\n if (capRateString.includes(\"%\") || numericValue > 1) {\r\n return Math.round((numericValue / 100) * 1000000) / 1000000;\r\n } else {\r\n return Math.round(numericValue * 1000000) / 1000000;\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\nexport function formatDownPaymentPercent(percentage) {\r\n return Math.round((percentage / 100) * 1000000) / 1000000;\r\n}\r\n"],"names":["mapPropertyType","type","assisted","business","mixed_use","multifamily","rv_park","str","createExportObjectCore","data","options","currentDownPaymentPercent","currentInterestRateType","currentMortgages","currentPriceDiscount","currentPropertyType","equitySource","estimatedMortgageBalance","isUsingEstimatedCapRate","noi","numberOfUnits","priceWasDefaulted","windowLocation","exportData","name","address","capRate","capMatch","match","numericValue","parseFloat","includes","Math","round","capRateSource","contact","listingDate","dateListed","price","priceMatch","displayedPrice","replace","originalPrice","Number","isFinite","undefined","downPaymentPercent","equity","equityPercentFromDebt","clamped","max","min","equityPercent","Array","isArray","length","JSON","stringify","phone","priceDiscountPercent","interestRateType","propertyType","url","console","log","alphabetized","Object","keys","sort","forEach","key","calculateOriginalPrice","discountPercent","convertCapRateToDecimal","capRateString","formatDownPaymentPercent","percentage"],"mappings":"qEAMO,SAASA,gBAAgBC,GAS9B,MARgB,CACdC,SAAU,WACVC,SAAU,WACVC,UAAW,YACXC,YAAa,MACbC,QAAS,UACTC,IAAK,OAEQN,IAAS,KAC1B,CAKO,SAASO,uBAAuBC,EAAMC,EAAU,IACrD,MAAMC,0BACJA,EAAyBC,wBACzBA,EAA0B,mBAAkBC,iBAC5CA,EAAmB,GAAEC,qBACrBA,EAAuB,EAACC,oBACxBA,EAAsB,MAAKC,aAC3BA,EAAe,UAASC,yBACxBA,EAA2B,KAAIC,wBAC/BA,GAA0B,EAAKC,IAC/BA,EAAM,KAAIC,cACVA,EAAgB,EAACC,kBACjBA,GAAoB,EAAKC,eACzBA,EAAiB,IACfZ,EAEJ,GAAIW,EAAmB,OAAO,KAE9B,MAAME,EAAa,CAAA,EAQnB,GALId,EAAKe,MAAsB,qBAAdf,EAAKe,MAA6C,cAAdf,EAAKe,OACxDD,EAAWE,QAAUhB,EAAKe,MAIxBf,EAAKiB,SAA4B,eAAjBjB,EAAKiB,SAA6C,cAAjBjB,EAAKiB,QAAyB,CACjF,MAAMC,EAAWlB,EAAKiB,QAAQE,MAAM,UACpC,GAAID,EAAU,CACZ,MAAME,EAAeC,WAAWH,EAAS,IAIrClB,EAAKiB,QAAQK,SAAS,MAAQF,EAAe,EAE/CN,EAAWG,QAAUM,KAAKC,MAAOJ,EAAe,IAAO,KAAW,IAGlEN,EAAWG,QAAUM,KAAKC,MAAqB,IAAfJ,GAA0B,GAE9D,CACF,CAgBA,GAbAN,EAAWW,cAAgBhB,EAA0B,YAAc,UAG/DT,EAAK0B,SAA4B,cAAjB1B,EAAK0B,UACvBZ,EAAWY,QAAU1B,EAAK0B,SAIxB1B,EAAK2B,aAAoC,cAArB3B,EAAK2B,cAC3Bb,EAAWc,WAAa5B,EAAK2B,aAI3B3B,EAAK6B,OAAwB,eAAf7B,EAAK6B,OAAyC,cAAf7B,EAAK6B,MAAuB,CAC3E,MAAMC,EAAa9B,EAAK6B,MAAMV,MAAM,UACpC,GAAIW,EAAY,CACd,MAAMC,EAAiBV,WAAWS,EAAW,GAAGE,QAAQ,KAAM,KAE9D,GAAI3B,EAAuB,EAAG,CAC5B,MACM4B,EAAgBF,GAAkB,EADhB1B,EAAuB,KAE/CS,EAAWe,MAAQN,KAAKC,MAAMS,EAChC,MACEnB,EAAWe,MAAQE,CAEvB,CACF,CAgBA,GAXIG,OAAOC,SAASzB,IAAQA,EAAM,IAChCI,EAAWJ,IAAMa,KAAKC,MAAMd,SAII0B,IAA9BlC,IACFY,EAAWuB,mBAAqBd,KAAKC,MAAOtB,EAA4B,IAAO,KAAW,KAKxFgC,OAAOC,SAASrB,EAAWe,QAAUf,EAAWe,MAAQ,EAAG,CAC7D,MAAMS,EAASC,EAAsBzB,EAAWe,MAAOrB,GACjDgC,EAAUjB,KAAKkB,IAAI,EAAGlB,KAAKmB,IAAI,EAAGJ,IACxCxB,EAAW6B,cAAgBpB,KAAKC,MAAgB,IAAVgB,GAAqB,GAC7D,CAGA1B,EAAWP,aAAeA,EAItB2B,OAAOC,SAAS3B,KAClBM,EAAWN,yBAA2Be,KAAKC,MAAMhB,IAE/CoC,MAAMC,QAAQzC,IAAqBA,EAAiB0C,OAAS,IAC/DhC,EAAWV,iBAAmB2C,KAAKC,UAAU5C,IAI/CU,EAAWH,cAAgBA,EAGvBX,EAAKiD,OAAwB,cAAfjD,EAAKiD,QACrBnC,EAAWmC,MAAQjD,EAAKiD,OAKxBnC,EAAWoC,qBADT7C,EAAuB,EACSkB,KAAKC,MAAOnB,EAAuB,IAAO,KAAW,IAErD,EAIpCS,EAAWqC,iBAAmBhD,EAG9BW,EAAWsC,aAAe7D,gBAAgBe,GAG1CQ,EAAWuC,IAAMxC,EAEjByC,QAAQC,IAAI,aAAczC,GAG1B,MAAM0C,EAAe,CAAA,EAKrB,OAJAC,OAAOC,KAAK5C,GAAY6C,OAAOC,QAAQC,IACrCL,EAAaK,GAAO/C,EAAW+C,KAG1BL,CACT,CAGO,SAASM,uBAAuB/B,EAAgBgC,GACrD,GAAIA,EAAkB,EAAG,CAEvB,OAAOhC,GAAkB,EADDgC,EAAkB,IAE5C,CACA,OAAOhC,CACT,CAEO,SAASiC,wBAAwBC,GACtC,IAAKA,GAAmC,eAAlBA,GAAoD,cAAlBA,EACtD,OAAO,KAGT,MAAM/C,EAAW+C,EAAc9C,MAAM,UACrC,GAAID,EAAU,CACZ,MAAME,EAAeC,WAAWH,EAAS,IAEzC,OAAI+C,EAAc3C,SAAS,MAAQF,EAAe,EACzCG,KAAKC,MAAOJ,EAAe,IAAO,KAAW,IAE7CG,KAAKC,MAAqB,IAAfJ,GAA0B,GAEhD,CAEA,OAAO,IACT,CAEO,SAAS8C,yBAAyBC,GACvC,OAAO5C,KAAKC,MAAO2C,EAAa,IAAO,KAAW,GACpD"}
@@ -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 a,PROPERTY_TYPE_CONSTANTS as r}from"../config/property-types.js";const c=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,c=12*a;return e*(r*Math.pow(1+r,c))/(Math.pow(1+r,c)-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 calculatePriceForCOCR(a,r=.15,n={}){const{downPercent:l=100*e.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:u=100*e.DEFAULT_DSCR_PERCENTAGE,dscrRate:o=c.rate,dscrTerm:i=c.amortization,maxIterations:s=t.MAX_ITERATIONS,tolerance:E=t.CALCULATION_TOLERANCE}=n;try{let e=a/.08,c=0;for(;c<s;){const n=e*(l/100),s=12*calculatePMT(e*(u/100),o,i),T=(a-s)/n;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),c++}return e<t.MINIMUM_COCR15_PRICE&&(e=t.MINIMUM_COCR15_PRICE),e}catch(e){return 0}}function calculateCOCRAtPercent(e,t,a,r={}){const{dscrRate:n=c.rate,dscrTerm:l=c.amortization}=r;try{const r=e*(a/100),c=12*calculatePMT(e-r,n,l);return(t-c)/r*100}catch(e){return 0}}function calculateSTRNOI(e,t=null,a={}){const{grossRate:c=r.STR.ESTIMATED_GROSS_RATE,noiPercentage:n=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*n}return!Number.isFinite(e)||e<=0?0:e*c*n}catch(e){return 0}}function calculateNOIByType(e,t,c=a.MULTIFAMILY,n={}){const{strApiResult:l=null,strGrossIncomeMultiplier:u=r.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:o=r.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:i=r.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:s=r.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch(c.toLowerCase()){case a.STR:return calculateSTRNOI(e,l,{grossRate:u,noiPercentage:o});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:c,propertyType:n=a.MULTIFAMILY,reportedCapRate:l=null,strApiResult:u=null}={}){const o=null!=l&&Number.isFinite(l),i=o?l:r,s=u&&Number.isFinite(u.value)&&u.value>=0&&("noi"===u.type||"gross"===u.type);let E,T;if(null!=t&&Number.isFinite(t)&&t>=0)E=t,T="confirmed";else{E=calculateNOIByType(c,i,n,{bedroomCount:e,strApiResult:u});const t=(n||"").toLowerCase();T=t===a.STR?s?"measured":"estimate":t===a.ASSISTED_LIVING?"bedrooms":o?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(c)&&c>0?E/c:null,noi:E,noiSource:T,reportedCapRate:o?l:null}}function calculateAssignmentFee(e,a=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(a/100)}catch(e){return 0}}function calculateNetToBuyer(a,r={}){const{buyerCostPercent:c=100*t.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*t.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*t.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*t.REHAB_RATE,additionalCostFinancing:o=100*t.HARD_MONEY_RATE,dscrLtvPercent:i=100*e.DEFAULT_DSCR_PERCENTAGE}=r;try{return a*(c/100)-a*((n+l)/100)-a*(u/100)-o/100*(a-a*(i/100))}catch(e){return 0}}function calculateBalloonBalance(t,a,r,c=e.DEFAULT_BALLOON_PERIOD_YEARS){try{if(t<=0||a<0||r<=0||c<=0)return 0;if(c>=r)return 0;if(0===a){const e=12*r;return t*(e-12*c)/e}const e=a/12,n=12*r,l=12*c,u=Math.pow(1+e,n),o=t*(u-Math.pow(1+e,l))/(u-1);return Math.max(0,o)}catch(e){return 0}}function calculateAppreciatedValue(t,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,n={}){const{appreciationRate:l=e.APPRECIATION_RATE,balloonYears:u=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:o=c.rate,dscrTerm:i=c.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=n;try{const c=calculateAppreciatedValue(t,l,u),n=calculateBalloonBalance(a,o,i,u),T=calculateBalloonBalance(r,e.SELLER_FI_INTEREST_RATE,s,u);return c*(E/100)-(n+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,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,resolveListingFinancials,safePercentage};
1
+ import{FINANCIAL_CONSTANTS as e}from"../config/financial.js";import{BUSINESS_CONSTANTS as t}from"../config/business.js";import{PROPERTY_TYPES as r,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";const c=e.INTEREST_RATE_TIERS[e.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(e,t,r){if(0===t)return e/(12*r);const a=t/12,c=12*r;return e*(a*Math.pow(1+a,c))/(Math.pow(1+a,c)-1)}function calculateCOCR30(e,t){try{const r=.3*e,a=12*calculatePMT(.7*e,.075,30);return(t-a)/r*100}catch(e){return 0}}function calculateCashFlowYield(e,t){if(!t||t<=0)return 0;return 12*e/t*100}function equityPercentFromDebt(e,t){const r=Number(e),a=Number(t);return!Number.isFinite(r)||r<=0?1:Number.isFinite(a)?(r-a)/r:1}function calculatePriceForCOCR(r,a=.15,n={}){const{downPercent:l=100*e.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:u=100*e.DEFAULT_DSCR_PERCENTAGE,dscrRate:o=c.rate,dscrTerm:i=c.amortization,maxIterations:s=t.MAX_ITERATIONS,tolerance:E=t.CALCULATION_TOLERANCE}=n;try{let e=r/.08,c=0;for(;c<s;){const n=e*(l/100),s=12*calculatePMT(e*(u/100),o,i),T=(r-s)/n;if(Math.abs(T-a)<E)break;const R=T-a,A=R*t.ADJUSTMENT_FACTOR;e*=R>0?1+Math.abs(A):1-Math.abs(A),e<1e3&&(e=1e3),e>r*t.MAX_COCR15_PRICE_MULTIPLIER&&(e=r*t.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),c++}return e<t.MINIMUM_COCR15_PRICE&&(e=t.MINIMUM_COCR15_PRICE),e}catch(e){return 0}}function calculateCOCRAtPercent(e,t,r,a={}){const{dscrRate:n=c.rate,dscrTerm:l=c.amortization}=a;try{const a=e*(r/100),c=12*calculatePMT(e-a,n,l);return(t-c)/a*100}catch(e){return 0}}function calculateSTRNOI(e,t=null,r={}){const{grossRate:c=a.STR.ESTIMATED_GROSS_RATE,noiPercentage:n=a.STR.NOI_PERCENTAGE}=r;try{if(t&&Number.isFinite(t.value)&&t.value>=0){if("noi"===t.type)return t.value;if("gross"===t.type)return t.value*n}return!Number.isFinite(e)||e<=0?0:e*c*n}catch(e){return 0}}function calculateNOIByType(e,t,c=r.MULTIFAMILY,n={}){const{strApiResult:l=null,strGrossIncomeMultiplier:u=a.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:o=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:i=a.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:s=a.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch(c.toLowerCase()){case r.STR:return calculateSTRNOI(e,l,{grossRate:u,noiPercentage:o});case r.ASSISTED_LIVING:return s*i*12;case r.MULTIFAMILY:default:return e*t}}catch(e){return 0}}function resolveListingFinancials({bedroomCount:e=null,confirmedNOI:t=null,estimatedCapRate:a,price:c,propertyType:n=r.MULTIFAMILY,reportedCapRate:l=null,strApiResult:u=null}={}){const o=null!=l&&Number.isFinite(l),i=o?l:a,s=u&&Number.isFinite(u.value)&&u.value>=0&&("noi"===u.type||"gross"===u.type);let E,T;if(null!=t&&Number.isFinite(t)&&t>=0)E=t,T="confirmed";else{E=calculateNOIByType(c,i,n,{bedroomCount:e,strApiResult:u});const t=(n||"").toLowerCase();T=t===r.STR?s?"measured":"estimate":t===r.ASSISTED_LIVING?"bedrooms":o?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(c)&&c>0?E/c:null,noi:E,noiSource:T,reportedCapRate:o?l:null}}function calculateAssignmentFee(e,r=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(r/100)}catch(e){return 0}}function calculateNetToBuyer(r,a={}){const{buyerCostPercent:c=100*t.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*t.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*t.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*t.REHAB_RATE,additionalCostFinancing:o=100*t.HARD_MONEY_RATE,dscrLtvPercent:i=100*e.DEFAULT_DSCR_PERCENTAGE}=a;try{return r*(c/100)-r*((n+l)/100)-r*(u/100)-o/100*(r-r*(i/100))}catch(e){return 0}}function calculateBalloonBalance(t,r,a,c=e.DEFAULT_BALLOON_PERIOD_YEARS){try{if(t<=0||r<0||a<=0||c<=0)return 0;if(c>=a)return 0;if(0===r){const e=12*a;return t*(e-12*c)/e}const e=r/12,n=12*a,l=12*c,u=Math.pow(1+e,n),o=t*(u-Math.pow(1+e,l))/(u-1);return Math.max(0,o)}catch(e){return 0}}function calculateAppreciatedValue(t,r=e.APPRECIATION_RATE,a=e.DEFAULT_BALLOON_PERIOD_YEARS){try{return t<=0||r<0||a<0?t:t*Math.pow(1+r,a)}catch(e){return t}}function calculateCashOutAfterRefi(t,r,a,n={}){const{appreciationRate:l=e.APPRECIATION_RATE,balloonYears:u=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:o=c.rate,dscrTerm:i=c.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=n;try{const c=calculateAppreciatedValue(t,l,u),n=calculateBalloonBalance(r,o,i,u),T=calculateBalloonBalance(a,e.SELLER_FI_INTEREST_RATE,s,u);return c*(E/100)-(n+T)}catch(e){return 0}}function calculateCashFlow(e,t,r){return e-(t+r)}function calculateDiscountFromPrice(e,t){return!e||e<=0?0:(e-t)/e}function calculatePriceFromDiscount(e,t){return!e||e<=0?0:e*(1-t)}function safePercentage(e,t=100){return null==e||isNaN(e)?t:100*e}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage};
2
2
  //# sourceMappingURL=calculations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate STR (short-term rental) NOI - the single source of STR NOI math.\r\n *\r\n * Resolution order:\r\n * 1. apiResult type 'noi' -> value is already net, return as-is\r\n * 2. apiResult type 'gross' -> apply NOI margin (value * noiPercentage)\r\n * 3. no/invalid apiResult -> estimate from price (price * grossRate * noiPercentage)\r\n *\r\n * apiResult comes from api.archerjessop.com/str-revenue: { value, type }.\r\n * Pass null while that backend is not live (the price estimate is used).\r\n *\r\n * @param {number} askingPrice - Property asking price\r\n * @param {{value:number, type:'noi'|'gross'}|null} apiResult - STR revenue API result\r\n * @param {Object} options - Rate overrides (default to STR config constants)\r\n * @returns {number} Annual NOI (0 on invalid input)\r\n */\r\nexport function calculateSTRNOI(askingPrice, apiResult = null, options = {}) {\r\n const {\r\n grossRate = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n noiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE\r\n } = options;\r\n\r\n try {\r\n if (apiResult && Number.isFinite(apiResult.value) && apiResult.value >= 0) {\r\n if (apiResult.type === \"noi\") return apiResult.value;\r\n if (apiResult.type === \"gross\") return apiResult.value * noiPercentage;\r\n }\r\n\r\n if (!Number.isFinite(askingPrice) || askingPrice <= 0) return 0;\r\n return askingPrice * grossRate * noiPercentage;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strApiResult = null,\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n return calculateSTRNOI(askingPrice, strApiResult, {\r\n grossRate: strGrossIncomeMultiplier,\r\n noiPercentage: strNoiPercentage\r\n });\r\n\r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Resolve a listing's canonical NOI and the cap rates derived from it.\r\n *\r\n * NOI is the source of truth; each property type computes it by its own model\r\n * (delegated to calculateNOIByType, unchanged). The active (displayed) cap rate is\r\n * always derived as NOI / price. The reported cap rate is carried through as\r\n * provenance (shown on hover; null => the UI shows \"N/A\"); it only DRIVES the NOI for\r\n * multifamily, where the cap rate is the model input.\r\n *\r\n * NOI precedence: an analyst-confirmed NOI overrides the per-type model; otherwise STR\r\n * uses measured 3rd-party revenue when present and the price-based estimate otherwise;\r\n * assisted uses bedroom count; multifamily uses the reported cap (or estimatedCapRate\r\n * when none was reported).\r\n *\r\n * @param {Object} input\r\n * @param {number|null} [input.bedroomCount] - Bedroom count for assisted living\r\n * @param {number|null} [input.confirmedNOI] - Analyst-confirmed/edited NOI; overrides the per-type model\r\n * @param {number} [input.estimatedCapRate] - Fallback cap (decimal) used for multifamily NOI when none was reported\r\n * @param {number} input.price - Asking price\r\n * @param {string} [input.propertyType] - One of PROPERTY_TYPES\r\n * @param {number|null} [input.reportedCapRate] - Real scraped/listed cap rate as a decimal (e.g. 0.0486); null when none was reported\r\n * @param {{value:number,type:'noi'|'gross'}|null} [input.strApiResult] - Measured STR revenue (3rd-party); null until that backend ships\r\n * @returns {{activeCapRate:(number|null), noi:number, noiSource:string, reportedCapRate:(number|null)}}\r\n */\r\nexport function resolveListingFinancials({\r\n bedroomCount = null,\r\n confirmedNOI = null,\r\n estimatedCapRate,\r\n price,\r\n propertyType = PROPERTY_TYPES.MULTIFAMILY,\r\n reportedCapRate = null,\r\n strApiResult = null,\r\n} = {}) {\r\n const hasReported = reportedCapRate != null && Number.isFinite(reportedCapRate);\r\n const capForNOI = hasReported ? reportedCapRate : estimatedCapRate;\r\n const measured = strApiResult && Number.isFinite(strApiResult.value) && strApiResult.value >= 0 &&\r\n (strApiResult.type === \"noi\" || strApiResult.type === \"gross\");\r\n\r\n let noi;\r\n let noiSource;\r\n if (confirmedNOI != null && Number.isFinite(confirmedNOI) && confirmedNOI >= 0) {\r\n noi = confirmedNOI;\r\n noiSource = \"confirmed\";\r\n } else {\r\n noi = calculateNOIByType(price, capForNOI, propertyType, { bedroomCount, strApiResult });\r\n const type = (propertyType || \"\").toLowerCase();\r\n if (type === PROPERTY_TYPES.STR) {\r\n noiSource = measured ? \"measured\" : \"estimate\";\r\n } else if (type === PROPERTY_TYPES.ASSISTED_LIVING) {\r\n noiSource = \"bedrooms\";\r\n } else {\r\n noiSource = hasReported ? \"cap\" : \"estimate\";\r\n }\r\n }\r\n\r\n if (!Number.isFinite(noi)) noi = 0;\r\n const activeCapRate = Number.isFinite(price) && price > 0 ? noi / price : null;\r\n\r\n return {\r\n activeCapRate,\r\n noi,\r\n noiSource,\r\n reportedCapRate: hasReported ? reportedCapRate : null,\r\n };\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","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","Number","isFinite","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","price","reportedCapRate","hasReported","capForNOI","measured","noiSource","activeCapRate","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","fallback","isNaN"],"mappings":"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"}
1
+ {"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n/**\r\n * Equity as a decimal fraction of price, derived from outstanding debt.\r\n * equity = (price - debtBalance) / price. Recompute whenever the user edits price.\r\n * Falls back to 1 (100% equity) when debt is unavailable or price is non-positive,\r\n * matching the \"estimated = 100%\" rule when the debt service returns no number.\r\n * @param {number} price - Listing/asking/offered price.\r\n * @param {number} debtBalance - Outstanding mortgage balance owing.\r\n * @returns {number} Equity as a decimal (e.g. 0.59 for 59%); 1 when debt is unknown.\r\n */\r\nexport function equityPercentFromDebt(price, debtBalance) {\r\n const p = Number(price);\r\n const d = Number(debtBalance);\r\n if (!Number.isFinite(p) || p <= 0) return 1;\r\n if (!Number.isFinite(d)) return 1;\r\n return (p - d) / p;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate STR (short-term rental) NOI - the single source of STR NOI math.\r\n *\r\n * Resolution order:\r\n * 1. apiResult type 'noi' -> value is already net, return as-is\r\n * 2. apiResult type 'gross' -> apply NOI margin (value * noiPercentage)\r\n * 3. no/invalid apiResult -> estimate from price (price * grossRate * noiPercentage)\r\n *\r\n * apiResult comes from api.archerjessop.com/str-revenue: { value, type }.\r\n * Pass null while that backend is not live (the price estimate is used).\r\n *\r\n * @param {number} askingPrice - Property asking price\r\n * @param {{value:number, type:'noi'|'gross'}|null} apiResult - STR revenue API result\r\n * @param {Object} options - Rate overrides (default to STR config constants)\r\n * @returns {number} Annual NOI (0 on invalid input)\r\n */\r\nexport function calculateSTRNOI(askingPrice, apiResult = null, options = {}) {\r\n const {\r\n grossRate = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n noiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE\r\n } = options;\r\n\r\n try {\r\n if (apiResult && Number.isFinite(apiResult.value) && apiResult.value >= 0) {\r\n if (apiResult.type === \"noi\") return apiResult.value;\r\n if (apiResult.type === \"gross\") return apiResult.value * noiPercentage;\r\n }\r\n\r\n if (!Number.isFinite(askingPrice) || askingPrice <= 0) return 0;\r\n return askingPrice * grossRate * noiPercentage;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strApiResult = null,\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n return calculateSTRNOI(askingPrice, strApiResult, {\r\n grossRate: strGrossIncomeMultiplier,\r\n noiPercentage: strNoiPercentage\r\n });\r\n\r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Resolve a listing's canonical NOI and the cap rates derived from it.\r\n *\r\n * NOI is the source of truth; each property type computes it by its own model\r\n * (delegated to calculateNOIByType, unchanged). The active (displayed) cap rate is\r\n * always derived as NOI / price. The reported cap rate is carried through as\r\n * provenance (shown on hover; null => the UI shows \"N/A\"); it only DRIVES the NOI for\r\n * multifamily, where the cap rate is the model input.\r\n *\r\n * NOI precedence: an analyst-confirmed NOI overrides the per-type model; otherwise STR\r\n * uses measured 3rd-party revenue when present and the price-based estimate otherwise;\r\n * assisted uses bedroom count; multifamily uses the reported cap (or estimatedCapRate\r\n * when none was reported).\r\n *\r\n * @param {Object} input\r\n * @param {number|null} [input.bedroomCount] - Bedroom count for assisted living\r\n * @param {number|null} [input.confirmedNOI] - Analyst-confirmed/edited NOI; overrides the per-type model\r\n * @param {number} [input.estimatedCapRate] - Fallback cap (decimal) used for multifamily NOI when none was reported\r\n * @param {number} input.price - Asking price\r\n * @param {string} [input.propertyType] - One of PROPERTY_TYPES\r\n * @param {number|null} [input.reportedCapRate] - Real scraped/listed cap rate as a decimal (e.g. 0.0486); null when none was reported\r\n * @param {{value:number,type:'noi'|'gross'}|null} [input.strApiResult] - Measured STR revenue (3rd-party); null until that backend ships\r\n * @returns {{activeCapRate:(number|null), noi:number, noiSource:string, reportedCapRate:(number|null)}}\r\n */\r\nexport function resolveListingFinancials({\r\n bedroomCount = null,\r\n confirmedNOI = null,\r\n estimatedCapRate,\r\n price,\r\n propertyType = PROPERTY_TYPES.MULTIFAMILY,\r\n reportedCapRate = null,\r\n strApiResult = null,\r\n} = {}) {\r\n const hasReported = reportedCapRate != null && Number.isFinite(reportedCapRate);\r\n const capForNOI = hasReported ? reportedCapRate : estimatedCapRate;\r\n const measured = strApiResult && Number.isFinite(strApiResult.value) && strApiResult.value >= 0 &&\r\n (strApiResult.type === \"noi\" || strApiResult.type === \"gross\");\r\n\r\n let noi;\r\n let noiSource;\r\n if (confirmedNOI != null && Number.isFinite(confirmedNOI) && confirmedNOI >= 0) {\r\n noi = confirmedNOI;\r\n noiSource = \"confirmed\";\r\n } else {\r\n noi = calculateNOIByType(price, capForNOI, propertyType, { bedroomCount, strApiResult });\r\n const type = (propertyType || \"\").toLowerCase();\r\n if (type === PROPERTY_TYPES.STR) {\r\n noiSource = measured ? \"measured\" : \"estimate\";\r\n } else if (type === PROPERTY_TYPES.ASSISTED_LIVING) {\r\n noiSource = \"bedrooms\";\r\n } else {\r\n noiSource = hasReported ? \"cap\" : \"estimate\";\r\n }\r\n }\r\n\r\n if (!Number.isFinite(noi)) noi = 0;\r\n const activeCapRate = Number.isFinite(price) && price > 0 ? noi / price : null;\r\n\r\n return {\r\n activeCapRate,\r\n noi,\r\n noiSource,\r\n reportedCapRate: hasReported ? reportedCapRate : null,\r\n };\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","equityPercentFromDebt","price","debtBalance","p","Number","d","isFinite","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","amortization","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateSTRNOI","apiResult","grossRate","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","noiPercentage","NOI_PERCENTAGE","value","type","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strApiResult","strGrossIncomeMultiplier","strNoiPercentage","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","resolveListingFinancials","confirmedNOI","estimatedCapRate","reportedCapRate","hasReported","capForNOI","measured","noiSource","activeCapRate","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","fallback","isNaN"],"mappings":"kNAMA,MAAMA,EAAeC,EAAoBC,oBAAoBD,EAAoBE,4BAU1E,SAASC,aAAaC,EAAWC,EAAYC,GAClD,GAAmB,IAAfD,EACF,OAAOD,GAAqB,GAARE,GAGtB,MAAMC,EAAcF,EAAa,GAC3BG,EAAsB,GAARF,EAGpB,OAFYF,GAAaG,EAAcE,KAAKC,IAAI,EAAIH,EAAaC,KACpDC,KAAKC,IAAI,EAAIH,EAAaC,GAAe,EAExD,CAEO,SAASG,gBAAgBC,EAAaC,GAC3C,IACE,MAAMC,EAA6B,GAAdF,EAEfG,EAAwD,GAA1CZ,aADiB,GAAdS,EAC0B,KAAO,IAIxD,OAHuBC,EAAME,GACED,EAAgB,GAGjD,CAAE,MAAOE,GACP,OAAO,CACT,CACF,CAEO,SAASC,uBAAuBC,EAAiBC,GACtD,IAAKA,GAAiBA,GAAiB,EAAG,OAAO,EAEjD,OADyC,GAAlBD,EACEC,EAAiB,GAC5C,CAWO,SAASC,sBAAsBC,EAAOC,GAC3C,MAAMC,EAAIC,OAAOH,GACXI,EAAID,OAAOF,GACjB,OAAKE,OAAOE,SAASH,IAAMA,GAAK,EAAU,EACrCC,OAAOE,SAASD,IACbF,EAAIE,GAAKF,EADe,CAElC,CAUO,SAASI,sBAAsBd,EAAKe,EAAa,IAAMC,EAAU,CAAA,GACtE,MAAMC,YACJA,EAAyD,IAA3C9B,EAAoB+B,qBAA0BC,eAC5DA,EAA+D,IAA9ChC,EAAoBiC,wBAA6BC,SAClEA,EAAWnC,EAAaoC,KAAIC,SAC5BA,EAAWrC,EAAasC,aAAYC,cACpCA,EAAgBC,EAAmBC,eAAcC,UACjDA,EAAYF,EAAmBG,uBAC7Bb,EAEJ,IACE,IAAIc,EAAc9B,EAAM,IACpB+B,EAAa,EAEjB,KAAOA,EAAaN,GAAe,CACjC,MAAMxB,EAAe6B,GAAeb,EAAc,KAE5Cf,EAAiE,GAAnDZ,aADGwC,GAAeX,EAAiB,KACNE,EAAUE,GAErDS,GADiBhC,EAAME,GACQD,EAErC,GAAIL,KAAKqC,IAAID,EAAcjB,GAAca,EACvC,MAGF,MAAMzB,EAAQ6B,EAAcjB,EACtBmB,EAAa/B,EAAQuB,EAAmBS,kBAG5CL,GADE3B,EAAQ,EACmB,EAAIP,KAAKqC,IAAIC,GAEb,EAAItC,KAAKqC,IAAIC,GAIxCJ,EAAc,MAAMA,EAAc,KAClCA,EAAc9B,EAAM0B,EAAmBU,8BACzCN,EAAc9B,EAAM0B,EAAmBW,sCAGzCN,GACF,CAOA,OAJID,EAAcJ,EAAmBY,uBACnCR,EAAcJ,EAAmBY,sBAG5BR,CACT,CAAE,MAAO3B,GACP,OAAO,CACT,CACF,CAUO,SAASoC,uBAAuBxC,EAAaC,EAAKiB,EAAaD,EAAU,CAAA,GAC9E,MAAMK,SACJA,EAAWnC,EAAaoC,KAAIC,SAC5BA,EAAWrC,EAAasC,cACtBR,EAEJ,IACE,MACMf,EAAeF,GADDkB,EAAc,KAM5Bf,EAAiE,GAAnDZ,aAFGS,EAAcE,EAEYoB,EAAUE,GAK3D,OAHuBvB,EAAME,GACED,EAAgB,GAGjD,CAAE,MAAOE,GACP,OAAO,CACT,CACF,CAkBO,SAASqC,gBAAgBzC,EAAa0C,EAAY,KAAMzB,EAAU,CAAA,GACvE,MAAM0B,UACJA,EAAYC,EAAwBC,IAAIC,qBAAoBC,cAC5DA,EAAgBH,EAAwBC,IAAIG,gBAC1C/B,EAEJ,IACE,GAAIyB,GAAa9B,OAAOE,SAAS4B,EAAUO,QAAUP,EAAUO,OAAS,EAAG,CACzE,GAAuB,QAAnBP,EAAUQ,KAAgB,OAAOR,EAAUO,MAC/C,GAAuB,UAAnBP,EAAUQ,KAAkB,OAAOR,EAAUO,MAAQF,CAC3D,CAEA,OAAKnC,OAAOE,SAASd,IAAgBA,GAAe,EAAU,EACvDA,EAAc2C,EAAYI,CACnC,CAAE,MAAO3C,GACP,OAAO,CACT,CACF,CAUO,SAAS+C,mBAAmBnD,EAAaoD,EAASC,EAAeC,EAAeC,YAAatC,EAAU,IAC5G,MAAMuC,aACJA,EAAe,KAAIC,yBACnBA,EAA2Bb,EAAwBC,IAAIC,qBAAoBY,iBAC3EA,EAAmBd,EAAwBC,IAAIG,eAAcW,yBAC7DA,EAA2Bf,EAAwBgB,gBAAgBC,2BAA0BC,aAC7FA,EAAelB,EAAwBgB,gBAAgBG,uBACrD9C,EAEJ,IACE,OAAQoC,EAAaW,eACnB,KAAKV,EAAeT,IAClB,OAAOJ,gBAAgBzC,EAAawD,EAAc,CAChDb,UAAWc,EACXV,cAAeW,IAGnB,KAAKJ,EAAeM,gBAClB,OAAOE,EAAeH,EAA2B,GAEnD,KAAKL,EAAeC,YACpB,QACE,OAAOvD,EAAcoD,EAE3B,CAAE,MAAOhD,GACP,OAAO,CACT,CACF,CA0BO,SAAS6D,0BAAyBH,aACvCA,EAAe,KAAII,aACnBA,EAAe,KAAIC,iBACnBA,EAAgB1D,MAChBA,EAAK4C,aACLA,EAAeC,EAAeC,YAAWa,gBACzCA,EAAkB,KAAIZ,aACtBA,EAAe,MACb,IACF,MAAMa,EAAiC,MAAnBD,GAA2BxD,OAAOE,SAASsD,GACzDE,EAAYD,EAAcD,EAAkBD,EAC5CI,EAAWf,GAAgB5C,OAAOE,SAAS0C,EAAaP,QAAUO,EAAaP,OAAS,IACrE,QAAtBO,EAAaN,MAAwC,UAAtBM,EAAaN,MAE/C,IAAIjD,EACAuE,EACJ,GAAoB,MAAhBN,GAAwBtD,OAAOE,SAASoD,IAAiBA,GAAgB,EAC3EjE,EAAMiE,EACNM,EAAY,gBACP,CACLvE,EAAMkD,mBAAmB1C,EAAO6D,EAAWjB,EAAc,CAAES,eAAcN,iBACzE,MAAMN,GAAQG,GAAgB,IAAIW,cAEhCQ,EADEtB,IAASI,EAAeT,IACd0B,EAAW,WAAa,WAC3BrB,IAASI,EAAeM,gBACrB,WAEAS,EAAc,MAAQ,UAEtC,CAEKzD,OAAOE,SAASb,KAAMA,EAAM,GAGjC,MAAO,CACLwE,cAHoB7D,OAAOE,SAASL,IAAUA,EAAQ,EAAIR,EAAMQ,EAAQ,KAIxER,MACAuE,YACAJ,gBAAiBC,EAAcD,EAAkB,KAErD,CAQO,SAASM,uBAAuB1E,EAAa2E,EAAmE,IAA/ChD,EAAmBiD,2BACzF,IACE,OAAO5E,GAAe2E,EAAoB,IAC5C,CAAE,MAAOvE,GACP,OAAO,CACT,CACF,CAQO,SAASyE,oBAAoB7E,EAAaiB,EAAU,IACzD,MAAM6D,iBACJA,EAAgE,IAA7CnD,EAAmBoD,wBAA6BC,qBACnEA,EAAsE,IAA/CrD,EAAmBiD,0BAA+BK,kBACzEA,EAAkE,IAA9CtD,EAAmBuD,yBAA8BC,oBACrEA,EAAsD,IAAhCxD,EAAmByD,WAAgBC,wBACzDA,EAA+D,IAArC1D,EAAmB2D,gBAAqBlE,eAClEA,EAA+D,IAA9ChC,EAAoBiC,yBACnCJ,EAEJ,IAGE,OAAOjB,GAAe8E,EAAmB,KAClC9E,IAAgBgF,EAAuBC,GAAqB,KAC5DjF,GAAemF,EAAsB,KACpCE,EAA0B,KAAQrF,EALnBA,GAAeoB,EAAiB,KAMzD,CAAE,MAAOhB,GACP,OAAO,CACT,CACF,CAUO,SAASmF,wBAAwBC,EAAYC,EAAcC,EAAmBC,EAAevG,EAAoBwG,8BACtH,IACE,GAAIJ,GAAc,GAAKC,EAAe,GAAKC,GAAqB,GAAKC,GAAgB,EACnF,OAAO,EAIT,GAAIA,GAAgBD,EAClB,OAAO,EAIT,GAAqB,IAAjBD,EAAoB,CACtB,MAAMI,EAAoC,GAApBH,EAEtB,OAAOF,GAAcK,EADe,GAAfF,GACgCE,CACvD,CAEA,MAAMlG,EAAc8F,EAAe,GAC7BI,EAAoC,GAApBH,EAChBI,EAAiC,GAAfH,EAMlBI,EAAUlG,KAAKC,IAAI,EAAIH,EAAakG,GAGpCG,EAAmBR,GAAcO,EAFvBlG,KAAKC,IAAI,EAAIH,EAAamG,KAEmBC,EAAU,GAEvE,OAAOlG,KAAKoG,IAAI,EAAGD,EACrB,CAAE,MAAO5F,GACP,OAAO,CACT,CACF,CASO,SAAS8F,0BAA0BC,EAAcC,EAAmBhH,EAAoBiH,kBAAmB3G,EAAQN,EAAoBwG,8BAC5I,IACE,OAAIO,GAAgB,GAAKC,EAAmB,GAAK1G,EAAQ,EAChDyG,EAGFA,EAAetG,KAAKC,IAAI,EAAIsG,EAAkB1G,EACvD,CAAE,MAAOU,GACP,OAAO+F,CACT,CACF,CAUO,SAASG,0BAA0BC,EAAeC,EAAgBC,EAAgBxF,EAAU,CAAA,GACjG,MAAMmF,iBACJA,EAAmBhH,EAAoBiH,kBAAiBV,aACxDA,EAAevG,EAAoBwG,6BAA4BtE,SAC/DA,EAAWnC,EAAaoC,KAAIC,SAC5BA,EAAWrC,EAAasC,aAAYiF,aACpCA,EAAetH,EAAoBuH,uBAAsBC,eACzDA,EAAiB,IACf3F,EAEJ,IAEE,MAAM4F,EAAmBX,0BAA0BK,EAAeH,EAAkBT,GAG9EmB,EAAuBvB,wBAAwBiB,EAAgBlF,EAAUE,EAAUmE,GAGnFoB,EAA2BxB,wBAAwBkB,EAAgBrH,EAAoB4H,wBAAyBN,EAAcf,GAWpI,OALsBkB,GAAoBD,EAAiB,MAHhCE,EAAuBC,EASpD,CAAE,MAAO3G,GACP,OAAO,CACT,CACF,CAGO,SAAS6G,kBAAkBC,EAAY/G,EAAagH,GACzD,OAAOD,GAAc/G,EAAcgH,EACrC,CAQO,SAASC,2BAA2BpH,EAAaqH,GACtD,OAAKrH,GAAeA,GAAe,EAAU,GAErCA,EAAcqH,GAAgBrH,CACxC,CAQO,SAASsH,2BAA2BtH,EAAauH,GACtD,OAAKvH,GAAeA,GAAe,EAAU,EAEtCA,GAAe,EAAIuH,EAC5B,CAEO,SAASC,eAAevE,EAAOwE,EAAW,KAC/C,OAAiB,MAATxE,GAAkByE,MAAMzE,GAA0BwE,EAAP,IAARxE,CAC7C"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,resolveListingFinancials,safePercentage}from"./financial/calculations.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,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,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage}from"./financial/calculations.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,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};
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 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 resolveListingFinancials,\r\n safePercentage,\r\n} from \"./financial/calculations.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":"k2DA4DY,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 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// 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":"k6DAgEY,MAACA,EAAc"}
@@ -0,0 +1,2 @@
1
+ async function fetchDebt(e,{baseUrl:t="https://api.archerjessop.com"}={}){const a=await fetch(`${t}/debt?address=${encodeURIComponent(e)}`,{method:"GET",headers:{"Content-Type":"application/json"}});if(!a.ok)throw new Error(`HTTP error! status: ${a.status}`);const r=await a.json(),s="number"==typeof r.estimatedMortgageBalance&&Number.isFinite(r.estimatedMortgageBalance)?r.estimatedMortgageBalance:null;return{address:"string"==typeof r.address?r.address:e,currentMortgages:Array.isArray(r.currentMortgages)?r.currentMortgages:[],estimatedMortgageBalance:s,source:null===s?"estimated":"api"}}export{fetchDebt};
2
+ //# sourceMappingURL=debt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debt.js","sources":["../../src/services/debt.js"],"sourcesContent":["// Agnostic debt fetcher: receives an address, returns the property's outstanding debt.\n//\n// PURE IO — no per-repo state, no panel DOM, no caching. Works in both Node (the dashboard\n// add-by-address flow) and the browser (the analyzer engine). The caller owns caching, the\n// loading indicator, the equity computation, and the \"estimated = 100%\" fallback.\n//\n// Returns { address, estimatedMortgageBalance, currentMortgages, source }:\n// - estimatedMortgageBalance: number (debt owing) or null when the service has no figure\n// - currentMortgages: array of lien objects (amount, position, lenderName, loanType, ...)\n// - source: \"api\" when a numeric balance came back, \"estimated\" when it did not\n// Throws on a network / non-OK HTTP error so the caller can treat it as the estimated case.\nexport async function fetchDebt(address, { baseUrl = \"https://api.archerjessop.com\" } = {}) {\n const response = await fetch(\n `${baseUrl}/debt?address=${encodeURIComponent(address)}`,\n { method: \"GET\", headers: { \"Content-Type\": \"application/json\" } }\n );\n\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const data = await response.json();\n\n const balance = typeof data.estimatedMortgageBalance === \"number\" && Number.isFinite(data.estimatedMortgageBalance)\n ? data.estimatedMortgageBalance\n : null;\n\n return {\n address: typeof data.address === \"string\" ? data.address : address,\n currentMortgages: Array.isArray(data.currentMortgages) ? data.currentMortgages : [],\n estimatedMortgageBalance: balance,\n source: balance === null ? \"estimated\" : \"api\",\n };\n}\n"],"names":["async","fetchDebt","address","baseUrl","response","fetch","encodeURIComponent","method","headers","ok","Error","status","data","json","balance","estimatedMortgageBalance","Number","isFinite","currentMortgages","Array","isArray","source"],"mappings":"AAWOA,eAAeC,UAAUC,GAASC,QAAEA,EAAU,gCAAmC,CAAA,GACtF,MAAMC,QAAiBC,MACrB,GAAGF,kBAAwBG,mBAAmBJ,KAC9C,CAAEK,OAAQ,MAAOC,QAAS,CAAE,eAAgB,sBAG9C,IAAKJ,EAASK,GACZ,MAAM,IAAIC,MAAM,uBAAuBN,EAASO,UAGlD,MAAMC,QAAaR,EAASS,OAEtBC,EAAmD,iBAAlCF,EAAKG,0BAAyCC,OAAOC,SAASL,EAAKG,0BACtFH,EAAKG,yBACL,KAEJ,MAAO,CACLb,QAAiC,iBAAjBU,EAAKV,QAAuBU,EAAKV,QAAUA,EAC3DgB,iBAAkBC,MAAMC,QAAQR,EAAKM,kBAAoBN,EAAKM,iBAAmB,GACjFH,yBAA0BD,EAC1BO,OAAoB,OAAZP,EAAmB,YAAc,MAE7C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "7.9.0",
3
+ "version": "7.11.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,2 +0,0 @@
1
- async function fetchEquity(t){const e=await fetch(`https://api.archerjessop.com/equity?address=${encodeURIComponent(t)}`,{method:"GET",headers:{"Content-Type":"application/json"}});if(!e.ok)throw new Error(`HTTP error! status: ${e.status}`);const o=await e.json();if(o&&void 0!==o.equity&&null!==o.equity){let t=o.equity;return"number"==typeof t?t=`${t}%`:"string"==typeof t&&(t.includes("%")||isNaN(parseFloat(t))||(t=`${t}%`)),t}return null}export{fetchEquity};
2
- //# sourceMappingURL=equity.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"equity.js","sources":["../../../src/browser/services/equity.js"],"sourcesContent":["// Agnostic equity fetcher: receives an address, returns equity data. Nothing more.\r\n//\r\n// Returns the equity as a normalized \"<n>%\" string, or null when the endpoint returns no\r\n// usable equity. Throws on a network / non-OK HTTP error so the caller can treat it as the\r\n// fallback case. PURE IO — no per-repo state, no panel DOM, no navigation guard. The caller\r\n// (the analyzer engine) owns caching, the loading indicator, the stale-drop, the provenance\r\n// flag, and the fallback value. The endpoint is identical for every analyzer, so there is\r\n// nothing per-site here.\r\nexport async function fetchEquity(address) {\r\n const response = await fetch(\r\n `https://api.archerjessop.com/equity?address=${encodeURIComponent(address)}`,\r\n { method: \"GET\", headers: { \"Content-Type\": \"application/json\" } }\r\n );\r\n\r\n if (!response.ok) {\r\n throw new Error(`HTTP error! status: ${response.status}`);\r\n }\r\n\r\n const data = await response.json();\r\n\r\n if (data && data.equity !== undefined && data.equity !== null) {\r\n let equityValue = data.equity;\r\n if (typeof equityValue === \"number\") {\r\n equityValue = `${equityValue}%`;\r\n } else if (typeof equityValue === \"string\") {\r\n if (!equityValue.includes(\"%\") && !isNaN(parseFloat(equityValue))) {\r\n equityValue = `${equityValue}%`;\r\n }\r\n }\r\n return equityValue;\r\n }\r\n\r\n return null;\r\n}\r\n"],"names":["async","fetchEquity","address","response","fetch","encodeURIComponent","method","headers","ok","Error","status","data","json","undefined","equity","equityValue","includes","isNaN","parseFloat"],"mappings":"AAQOA,eAAeC,YAAYC,GAChC,MAAMC,QAAiBC,MACrB,+CAA+CC,mBAAmBH,KAClE,CAAEI,OAAQ,MAAOC,QAAS,CAAE,eAAgB,sBAG9C,IAAKJ,EAASK,GACZ,MAAM,IAAIC,MAAM,uBAAuBN,EAASO,UAGlD,MAAMC,QAAaR,EAASS,OAE5B,GAAID,QAAwBE,IAAhBF,EAAKG,QAAwC,OAAhBH,EAAKG,OAAiB,CAC7D,IAAIC,EAAcJ,EAAKG,OAQvB,MAP2B,iBAAhBC,EACTA,EAAc,GAAGA,KACe,iBAAhBA,IACXA,EAAYC,SAAS,MAASC,MAAMC,WAAWH,MAClDA,EAAc,GAAGA,OAGdA,CACT,CAEA,OAAO,IACT"}