@archerjessop/utilities 7.8.0 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{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";const
|
|
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};
|
|
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\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 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","priceWasDefaulted","price"],"mappings":"
|
|
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"}
|