@archerjessop/utilities 7.26.0 → 7.28.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{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";import{formatCurrency as u}from"../../financial/formatters.js";import{PROPERTY_TYPE_CONSTANTS as d}from"../../config/property-types.js";const m=["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:g}){const{state:y,updateState:f}=g;function getCurrentPrice(){if(y.originalPrice&&y.currentPriceDiscount>0){const e=parseFloat(y.originalPrice.replace(/[$,]/g,""))*(1-y.currentPriceDiscount/100);return`$${Math.round(e).toLocaleString()}`}return y.originalPrice}function updatePercentageLabels(){const e=document.querySelector("#prop-down")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=`Down (${y.currentDownPaymentPercent}%)`);const t=document.querySelector("#prop-seller-fi")?.closest(".metric")?.querySelector(".metric-label");t&&(t.textContent=`Seller FI (${y.currentSellerFiPercent}%)`);const n=document.querySelector("#prop-dscr")?.closest(".metric")?.querySelector(".metric-label");n&&(n.textContent=`DSCR (${y.currentDSCRPercent}%)`)}function updateElement(e,t){const n=document.getElementById(e);n&&(n.textContent=t)}return{applyFinancials:function(i){let a=!1,l=!1;if(i){const p={"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;const u=y.cachedDebtBalance;l=Number.isFinite(u)&&u>i.rawDown;for(const[e,t]of Object.entries(p))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,y.currentDownPaymentPercent,y.currentDSCRPercent,y.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 m.forEach(e=>updateElement(e,"N/A"));updatePercentageLabels(),function(){const r="str"===y.currentPropertyType,o=document.getElementById("prop-noi-awning");o&&(o.style.display=r?"inline-block":"none");const c=document.getElementById("prop-noi");if(!c)return;if(c.style.cursor=r?"pointer":"",c.querySelector("input"))return;const s=c.closest(".metric");if(r&&y.cachedStrValue&&Number.isFinite(y.cachedStrValue.value)){c.textContent=`${c.textContent}*`;const r=y.cachedStrValue.value,o=d.STR.NOI_PERCENTAGE,i=`<strong>Manual STR gross (Awning):</strong> ${u(r)}/yr<br>NOI = gross &times; ${Math.round(100*o)}% = ${u(r*o)}<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;s&&(e(s)?n(s,i):(t(s,i),s.querySelector(".metric-label")?.classList.add("has-tooltip")))}else r&&s&&e(s)&&n(s,"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>")}();const p=a||l,g=document.getElementById("ln-footer");g&&g.classList.toggle("negative",p);const f=[];a&&f.push("Cash Flow"),l&&f.push("Equity");const C=document.getElementById("ln-red-reasons");C&&(C.textContent=f.join(" · "),C.style.display=f.length?"":"none")},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)),f({numberOfUnits:t})}},updateActiveCapDisplay:function(){const r=document.getElementById("prop-cap");if(!r)return;if(r.querySelector("input"))return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o);r.textContent=a(y.baseNOI,c);const s=l(y.originalCapRate,y.isUsingEstimatedCapRate),p=`${"<strong>Reported cap rate:</strong> "+(null!=s?`${s}%`:"N/A")}<hr><em>Click the cap rate to enter a value; 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(y.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=y.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"===y.equitySource?"Public records (API)":y.equitySource;let d=`<strong>Debt owing:</strong> ${a?"Unknown":fmtUSD(s)}`;d+=`<br><strong>Source:</strong> ${u}`,y.cachedDebtAddress&&(d+=`<br><strong>Matched:</strong> ${y.cachedDebtAddress}`);const m=Array.isArray(y.cachedMortgages)?y.cachedMortgages:[];m.length&&(d+="<hr>"+m.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 g=r.closest(".metric");if(g){const r=g.querySelector(".metric-label");e(g)?n(g,d):(t(g,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=y.currentPriceDiscount>0?`Price (${y.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";import{formatCurrency as u}from"../../financial/formatters.js";import{PROPERTY_TYPE_CONSTANTS as d}from"../../config/property-types.js";import{formatDate as m}from"../../date/utilities.js";const g=["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:f}){const{state:y,updateState:C}=f;function getCurrentPrice(){if(y.originalPrice&&y.currentPriceDiscount>0){const e=parseFloat(y.originalPrice.replace(/[$,]/g,""))*(1-y.currentPriceDiscount/100);return`$${Math.round(e).toLocaleString()}`}return y.originalPrice}function updatePercentageLabels(){const e=document.querySelector("#prop-down")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=`Down (${y.currentDownPaymentPercent}%)`);const t=document.querySelector("#prop-seller-fi")?.closest(".metric")?.querySelector(".metric-label");t&&(t.textContent=`Seller FI (${y.currentSellerFiPercent}%)`);const n=document.querySelector("#prop-dscr")?.closest(".metric")?.querySelector(".metric-label");n&&(n.textContent=`DSCR (${y.currentDSCRPercent}%)`)}function updateElement(e,t){const n=document.getElementById(e);n&&(n.textContent=t)}return{applyFinancials:function(i){let a=!1,l=!1;if(i){const p={"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;const u=y.cachedDebtBalance;l=Number.isFinite(u)&&u>i.rawDown;for(const[e,t]of Object.entries(p))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,y.currentDownPaymentPercent,y.currentDSCRPercent,y.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 g.forEach(e=>updateElement(e,"N/A"));updatePercentageLabels(),function(){const r="str"===y.currentPropertyType,o=document.getElementById("prop-noi-awning");o&&(o.style.display=r?"inline-block":"none");const c=document.getElementById("prop-noi");if(!c)return;if(c.style.cursor=r?"pointer":"",c.querySelector("input"))return;const s=c.closest(".metric");if(r&&y.cachedStrValue&&Number.isFinite(y.cachedStrValue.value)){c.textContent=`${c.textContent}*`;const r=y.cachedStrValue.value,o=d.STR.NOI_PERCENTAGE,i=`<strong>Manual STR gross (Awning):</strong> ${u(r)}/yr<br>NOI = gross &times; ${Math.round(100*o)}% = ${u(r*o)}<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;s&&(e(s)?n(s,i):(t(s,i),s.querySelector(".metric-label")?.classList.add("has-tooltip")))}else r&&s&&e(s)&&n(s,"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>")}();const p=a||l,m=document.getElementById("ln-footer");m&&m.classList.toggle("negative",p);const f=[];a&&f.push("Cash Flow"),l&&f.push("Equity");const C=document.getElementById("ln-red-reasons");C&&(C.textContent=f.join(" · "),C.style.display=f.length?"":"none")},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)),C({numberOfUnits:t})}},updateActiveCapDisplay:function(){const r=document.getElementById("prop-cap");if(!r)return;if(r.querySelector("input"))return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o);r.textContent=a(y.baseNOI,c);const s=l(y.originalCapRate,y.isUsingEstimatedCapRate),p=`${"<strong>Reported cap rate:</strong> "+(null!=s?`${s}%`:"N/A")}<hr><em>Click the cap rate to enter a value; 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(y.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=y.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"===y.equitySource?"Public records (API)":y.equitySource;let d=`<strong>Debt owing:</strong> ${a?"Unknown":fmtUSD(s)}`;d+=`<br><strong>Source:</strong> ${u}`,y.cachedDebtAddress&&(d+=`<br><strong>Matched:</strong> ${y.cachedDebtAddress}`);const m=Array.isArray(y.cachedMortgages)?y.cachedMortgages:[];m.length&&(d+="<hr>"+m.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 g=r.closest(".metric");if(g){const r=g.querySelector(".metric-label");e(g)?n(g,d):(t(g,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";if(r){const e=r.createdAt?m(r.createdAt):"",t=e?`<br><strong>Filed:</strong> ${e}`:"";c=`\n <strong>Contact:</strong> ${r.contactName} <br>\n ${r.opportunityAddress}${t}\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=y.currentPriceDiscount>0?`Price (${y.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\nimport { equityPercentFromDebt } from \"../../financial/calculations.js\";\r\nimport { formatCurrency } from \"../../financial/formatters.js\";\r\nimport { PROPERTY_TYPE_CONSTANTS } from \"../../config/property-types.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 // Don't clobber an in-progress manual cap edit (the inline input lives in this cell).\r\n if (capElement.querySelector(\"input\")) 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 editHint = \"<hr><em>Click the cap rate to enter a value; click the label to reset</em>\";\r\n const tooltipContent = `${reportedLine}${editHint}`;\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 // STR affordances: the Awning link and the click-to-edit NOI cell exist only in STR mode.\r\n // When a manual STR gross is active (cachedStrValue), mark the painted NOI with a trailing *\r\n // and a source tooltip — mirroring the cap-rate/equity \"*\" convention. Runs after the metric\r\n // paint so the * is appended to the freshly-set NOI text (and re-applied on every recalc).\r\n function updateStrAffordances() {\r\n const isStr = state.currentPropertyType === \"str\";\r\n\r\n const awningLink = document.getElementById(\"prop-noi-awning\");\r\n if (awningLink) awningLink.style.display = isStr ? \"inline-block\" : \"none\";\r\n\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!noiElement) return;\r\n noiElement.style.cursor = isStr ? \"pointer\" : \"\";\r\n if (noiElement.querySelector(\"input\")) return;\r\n\r\n const metric = noiElement.closest(\".metric\");\r\n const manual = isStr && state.cachedStrValue && Number.isFinite(state.cachedStrValue.value);\r\n\r\n if (manual) {\r\n noiElement.textContent = `${noiElement.textContent}*`;\r\n const gross = state.cachedStrValue.value;\r\n const pct = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE;\r\n const tip = `<strong>Manual STR gross (Awning):</strong> ${formatCurrency(gross)}/yr`\r\n + `<br>NOI = gross &times; ${Math.round(pct * 100)}% = ${formatCurrency(gross * pct)}`\r\n + `<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tip);\r\n metric.querySelector(\".metric-label\")?.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tip);\r\n }\r\n }\r\n } else if (isStr && metric && hasTooltip(metric)) {\r\n updateTooltipContent(metric, \"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>\");\r\n }\r\n }\r\n\r\n function applyFinancials(financials) {\r\n let isCashFlowNegative = false;\r\n let insufficientEquity = 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 // Equity-aware: seller financing requires the seller to clear their existing debt at\r\n // close from the buyer's down payment, so a down payment that can't cover known debt is\r\n // an infeasible deal — same red treatment as negative cash flow (mirrors the dashboard).\r\n // Unknown debt (null) is assumed 100% equity and never flags.\r\n const debt = state.cachedDebtBalance;\r\n insufficientEquity = Number.isFinite(debt) && debt > financials.rawDown;\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 updateStrAffordances();\r\n\r\n const isRed = isCashFlowNegative || insufficientEquity;\r\n const footer = document.getElementById(\"ln-footer\");\r\n if (footer) footer.classList.toggle(\"negative\", isRed);\r\n\r\n const reasons = [];\r\n if (isCashFlowNegative) reasons.push(\"Cash Flow\");\r\n if (insufficientEquity) reasons.push(\"Equity\");\r\n const pill = document.getElementById(\"ln-red-reasons\");\r\n if (pill) {\r\n pill.textContent = reasons.join(\" · \");\r\n pill.style.display = reasons.length ? \"\" : \"none\";\r\n }\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","insufficientEquity","elements","noi","down","netToBuyer","sellerFi","cocr30","priceForCOCR15","assignment","dscr","sfPayment","cashFlow","rawCashFlow","debt","cachedDebtBalance","Number","isFinite","rawDown","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","isStr","currentPropertyType","awningLink","style","display","cursor","cachedStrValue","gross","pct","PROPERTY_TYPE_CONSTANTS","STR","NOI_PERCENTAGE","tip","formatCurrency","updateStrAffordances","isRed","footer","toggle","reasons","push","pill","join","length","syncUnitsFieldForType","propertyType","bedroomCount","input","String","numberOfUnits","updateActiveCapDisplay","capElement","priceText","parsePriceNumber","computeActiveCapDisplay","baseNOI","reported","parseReportedCap","originalCapRate","isUsingEstimatedCapRate","updateCapRateLabel","capLabelElement","updateEquityDisplay","equityElement","estimated","equity","equityPercentFromDebt","fmtUSD","n","sourceLabel","equitySource","tooltip","cachedDebtAddress","mortgages","Array","isArray","cachedMortgages","map","m","rate","interestRate","position","amount","loanType","lenderName","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\nimport { formatCurrency } from \"../../financial/formatters.js\";\r\nimport { PROPERTY_TYPE_CONSTANTS } from \"../../config/property-types.js\";\r\nimport { formatDate } from \"../../date/utilities.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 // Don't clobber an in-progress manual cap edit (the inline input lives in this cell).\r\n if (capElement.querySelector(\"input\")) 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 editHint = \"<hr><em>Click the cap rate to enter a value; click the label to reset</em>\";\r\n const tooltipContent = `${reportedLine}${editHint}`;\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 const filedDate = loiData.createdAt ? formatDate(loiData.createdAt) : \"\";\r\n const filedLine = filedDate ? `<br><strong>Filed:</strong> ${filedDate}` : \"\";\r\n tooltipContent = `\r\n <strong>Contact:</strong> ${loiData.contactName} <br>\r\n ${loiData.opportunityAddress}${filedLine}\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 // STR affordances: the Awning link and the click-to-edit NOI cell exist only in STR mode.\r\n // When a manual STR gross is active (cachedStrValue), mark the painted NOI with a trailing *\r\n // and a source tooltip — mirroring the cap-rate/equity \"*\" convention. Runs after the metric\r\n // paint so the * is appended to the freshly-set NOI text (and re-applied on every recalc).\r\n function updateStrAffordances() {\r\n const isStr = state.currentPropertyType === \"str\";\r\n\r\n const awningLink = document.getElementById(\"prop-noi-awning\");\r\n if (awningLink) awningLink.style.display = isStr ? \"inline-block\" : \"none\";\r\n\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!noiElement) return;\r\n noiElement.style.cursor = isStr ? \"pointer\" : \"\";\r\n if (noiElement.querySelector(\"input\")) return;\r\n\r\n const metric = noiElement.closest(\".metric\");\r\n const manual = isStr && state.cachedStrValue && Number.isFinite(state.cachedStrValue.value);\r\n\r\n if (manual) {\r\n noiElement.textContent = `${noiElement.textContent}*`;\r\n const gross = state.cachedStrValue.value;\r\n const pct = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE;\r\n const tip = `<strong>Manual STR gross (Awning):</strong> ${formatCurrency(gross)}/yr`\r\n + `<br>NOI = gross &times; ${Math.round(pct * 100)}% = ${formatCurrency(gross * pct)}`\r\n + `<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tip);\r\n metric.querySelector(\".metric-label\")?.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tip);\r\n }\r\n }\r\n } else if (isStr && metric && hasTooltip(metric)) {\r\n updateTooltipContent(metric, \"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>\");\r\n }\r\n }\r\n\r\n function applyFinancials(financials) {\r\n let isCashFlowNegative = false;\r\n let insufficientEquity = 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 // Equity-aware: seller financing requires the seller to clear their existing debt at\r\n // close from the buyer's down payment, so a down payment that can't cover known debt is\r\n // an infeasible deal — same red treatment as negative cash flow (mirrors the dashboard).\r\n // Unknown debt (null) is assumed 100% equity and never flags.\r\n const debt = state.cachedDebtBalance;\r\n insufficientEquity = Number.isFinite(debt) && debt > financials.rawDown;\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 updateStrAffordances();\r\n\r\n const isRed = isCashFlowNegative || insufficientEquity;\r\n const footer = document.getElementById(\"ln-footer\");\r\n if (footer) footer.classList.toggle(\"negative\", isRed);\r\n\r\n const reasons = [];\r\n if (isCashFlowNegative) reasons.push(\"Cash Flow\");\r\n if (insufficientEquity) reasons.push(\"Equity\");\r\n const pill = document.getElementById(\"ln-red-reasons\");\r\n if (pill) {\r\n pill.textContent = reasons.join(\" · \");\r\n pill.style.display = reasons.length ? \"\" : \"none\";\r\n }\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","insufficientEquity","elements","noi","down","netToBuyer","sellerFi","cocr30","priceForCOCR15","assignment","dscr","sfPayment","cashFlow","rawCashFlow","debt","cachedDebtBalance","Number","isFinite","rawDown","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","isStr","currentPropertyType","awningLink","style","display","cursor","cachedStrValue","gross","pct","PROPERTY_TYPE_CONSTANTS","STR","NOI_PERCENTAGE","tip","formatCurrency","updateStrAffordances","isRed","footer","toggle","reasons","push","pill","join","length","syncUnitsFieldForType","propertyType","bedroomCount","input","String","numberOfUnits","updateActiveCapDisplay","capElement","priceText","parsePriceNumber","computeActiveCapDisplay","baseNOI","reported","parseReportedCap","originalCapRate","isUsingEstimatedCapRate","updateCapRateLabel","capLabelElement","updateEquityDisplay","equityElement","estimated","equity","equityPercentFromDebt","fmtUSD","n","sourceLabel","equitySource","tooltip","cachedDebtAddress","mortgages","Array","isArray","cachedMortgages","map","m","rate","interestRate","position","amount","loanType","lenderName","updateLeadStatusTooltip","loiData","leadElement","filedDate","createdAt","formatDate","filedLine","contactName","opportunityAddress","updatePriceLabel","priceLabelElement"],"mappings":"orBAiBA,MAAMA,EAAwB,CAC5B,WAAY,YAAa,WAAY,iBAAkB,eACvD,eAAgB,kBAAmB,YAAa,UAAW,iBAGtD,SAASC,cAAaC,IAAEA,IAC7B,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBF,EAE/B,SAASG,kBACP,GAAIF,EAAMG,eAAiBH,EAAMI,qBAAuB,EAAG,CACzD,MACMC,EADeC,WAAWN,EAAMG,cAAcI,QAAQ,QAAS,MAC7B,EAAIP,EAAMI,qBAAuB,KACzE,MAAO,IAAII,KAAKC,MAAMJ,GAAiBK,kBACzC,CACA,OAAOV,EAAMG,aACf,CASA,SAASQ,yBACP,MAAMC,EAAmBC,SAASC,cAAc,eAAeC,QAAQ,YAAYD,cAAc,iBAC7FF,IAAkBA,EAAiBI,YAAc,SAAShB,EAAMiB,+BAEpE,MAAMC,EAAuBL,SAASC,cAAc,oBAAoBC,QAAQ,YAAYD,cAAc,iBACtGI,IAAsBA,EAAqBF,YAAc,cAAchB,EAAMmB,4BAEjF,MAAMC,EAAmBP,SAASC,cAAc,eAAeC,QAAQ,YAAYD,cAAc,iBAC7FM,IAAkBA,EAAiBJ,YAAc,SAAShB,EAAMqB,uBACtE,CAEA,SAASC,cAAcC,EAAIC,GACzB,MAAMC,EAAUZ,SAASa,eAAeH,GACpCE,IAASA,EAAQT,YAAcQ,EACrC,CA0PA,MAAO,CACLG,gBAlDF,SAAyBC,GACvB,IAAIC,GAAqB,EACrBC,GAAqB,EACzB,GAAIF,EAAY,CACd,MAAMG,EAAW,CACf,WAAYH,EAAWI,IACvB,YAAaJ,EAAWK,KACxB,WAAYL,EAAWM,WACvB,iBAAkBN,EAAWO,SAC7B,eAAgBP,EAAWQ,OAC3B,eAAgBR,EAAWS,eAC3B,kBAAmBT,EAAWU,WAC9B,YAAaV,EAAWW,KACxB,UAAWX,EAAWY,UACtB,gBAAiBZ,EAAWa,UAE9BZ,EAAqBD,EAAWc,YAAc,EAK9C,MAAMC,EAAO3C,EAAM4C,kBACnBd,EAAqBe,OAAOC,SAASH,IAASA,EAAOf,EAAWmB,QAChE,IAAK,MAAOxB,EAAIC,KAAUwB,OAAOC,QAAQlB,GAAWT,cAAcC,EAAIC,GACtE0B,WAAW,MAvGf,WACE,MAAMC,EAActC,SAASa,eAAe,aACtC0B,EAAevC,SAASa,eAAe,cACvC2B,EAAaxC,SAASa,eAAe,YAC3C,IAAKyB,IAAgBC,IAAiBC,EAAY,OAElD,MAAMC,EAAgBC,EAAmBH,EAAapC,YAAaqC,EAAWrC,aAC9E,GAAIsC,EAAe,CACjB,MAAME,EAAiBC,EACrBH,EAAcI,MACdJ,EAActB,IACdhC,EAAMiB,0BACNjB,EAAMqB,mBACNrB,EAAMmB,wBAEFwC,EAASR,EAAYpC,QAAQ,WAC/B4C,IACGC,EAAWD,GACXE,EAAqBF,EAAQH,GADTM,EAAcH,EAAQH,GAGnD,CACF,CAmFMO,GAjFN,WACE,MAAMC,EAAkBnD,SAASa,eAAe,iBAC1C0B,EAAevC,SAASa,eAAe,cAC7C,IAAKsC,IAAoBZ,EAAc,OAEvC,MAAMa,EAAeC,EAAkBd,EAAapC,YAAagD,EAAgBhD,aACjF,GAAIiD,EAAc,CAChB,MAAMT,EAAiBW,EAA4BF,EAAaP,MAAOO,EAAaG,iBAC9ET,EAASK,EAAgBjD,QAAQ,WACvC,GAAI4C,EAAQ,CACV,MAAMU,EAAQV,EAAO7C,cAAc,iBAC/BuD,GAAOA,EAAMC,UAAUC,IAAI,eAC1BX,EAAWD,GACXE,EAAqBF,EAAQH,GADTM,EAAcH,EAAQH,EAEjD,CACF,CACF,CAkEMgB,IACC,IACL,MACE3E,EAAsB4E,QAASlD,GAAOD,cAAcC,EAAI,QAG1DZ,yBAlEF,WACE,MAAM+D,EAAsC,QAA9B1E,EAAM2E,oBAEdC,EAAa/D,SAASa,eAAe,mBACvCkD,IAAYA,EAAWC,MAAMC,QAAUJ,EAAQ,eAAiB,QAEpE,MAAMrB,EAAaxC,SAASa,eAAe,YAC3C,IAAK2B,EAAY,OAEjB,GADAA,EAAWwB,MAAME,OAASL,EAAQ,UAAY,GAC1CrB,EAAWvC,cAAc,SAAU,OAEvC,MAAM6C,EAASN,EAAWtC,QAAQ,WAGlC,GAFe2D,GAAS1E,EAAMgF,gBAAkBnC,OAAOC,SAAS9C,EAAMgF,eAAexD,OAEzE,CACV6B,EAAWrC,YAAc,GAAGqC,EAAWrC,eACvC,MAAMiE,EAAQjF,EAAMgF,eAAexD,MAC7B0D,EAAMC,EAAwBC,IAAIC,eAClCC,EAAM,+CAA+CC,EAAeN,gCAC3CzE,KAAKC,MAAY,IAANyE,SAAiBK,EAAeN,EAAQC,8EAE9EvB,IACGC,EAAWD,GAIdE,EAAqBF,EAAQ2B,IAH7BxB,EAAcH,EAAQ2B,GACtB3B,EAAO7C,cAAc,kBAAkBwD,UAAUC,IAAI,gBAK3D,MAAWG,GAASf,GAAUC,EAAWD,IACvCE,EAAqBF,EAAQ,2GAEjC,CAmCE6B,GAEA,MAAMC,EAAQ5D,GAAsBC,EAC9B4D,EAAS7E,SAASa,eAAe,aACnCgE,GAAQA,EAAOpB,UAAUqB,OAAO,WAAYF,GAEhD,MAAMG,EAAU,GACZ/D,GAAoB+D,EAAQC,KAAK,aACjC/D,GAAoB8D,EAAQC,KAAK,UACrC,MAAMC,EAAOjF,SAASa,eAAe,kBACjCoE,IACFA,EAAK9E,YAAc4E,EAAQG,KAAK,OAChCD,EAAKjB,MAAMC,QAAUc,EAAQI,OAAS,GAAK,OAE/C,EAIE9F,gCACA+F,sBArKF,SAA+BC,EAAcC,GAC3C,MAAM9B,EAAQxD,SAASC,cAAc,uBAErC,GADIuD,IAAOA,EAAMrD,YAA+B,aAAjBkF,EAA8B,OAAS,SACjD,aAAjBA,GAA+C,MAAhBC,EAAsB,CACvD,MAAMC,EAAQvF,SAASa,eAAe,kBAClC0E,IAAOA,EAAM5E,MAAQ6E,OAAOF,IAChClG,EAAY,CAAEqG,cAAeH,GAC/B,CACF,EA8JEI,uBA5OF,WACE,MAAMC,EAAa3F,SAASa,eAAe,YAC3C,IAAK8E,EAAY,OAEjB,GAAIA,EAAW1F,cAAc,SAAU,OAEvC,MAAM2F,EAAYvG,mBAAqBW,SAASa,eAAe,eAAeV,aAAe,GACvF0C,EAAQgD,EAAiBD,GAC/BD,EAAWxF,YAAc2F,EAAwB3G,EAAM4G,QAASlD,GAEhE,MAAMmD,EAAWC,EAAiB9G,EAAM+G,gBAAiB/G,EAAMgH,yBAGzDxD,EAAiB,GAFF,wCAAmD,MAAZqD,EAAmB,GAAGA,KAAc,mFAI1FlD,EAAS6C,EAAWzF,QAAQ,WAClC,GAAI4C,EAAQ,CACV,MAAMU,EAAQV,EAAO7C,cAAc,iBAC9B8C,EAAWD,GAIdE,EAAqBF,EAAQH,IAH7BM,EAAcH,EAAQH,GAClBa,GAAOA,EAAMC,UAAUC,IAAI,eAInC,CACF,EAoNE0C,mBA7PF,WACE,MAAMC,EAAkBrG,SAASC,cAAc,cAAcC,QAAQ,YAAYD,cAAc,iBAC/F,GAAKoG,EACL,OAAQlH,EAAM2E,qBACZ,IAAK,MAAOuC,EAAgBlG,YAAc,iBAAkB,MAC5D,IAAK,WAAYkG,EAAgBlG,YAAc,sBAAuB,MACtE,QAASkG,EAAgBlG,YAAc,WAE3C,EAsPEM,4BACA6F,oBAjNF,WACE,MAAMC,EAAgBvG,SAASa,eAAe,eAC9C,IAAK0F,EAAe,OAEpB,MAAMX,EAAYvG,mBAAqBW,SAASa,eAAe,eAAeV,aAAe,GACvF0C,EAAQgD,EAAiBD,GACzB9D,EAAO3C,EAAM4C,kBACbyE,EAAY1E,QACZ2E,EAASC,EAAsB7D,EAAOf,GAC5CyE,EAAcpG,YAAc,GAAGR,KAAKC,MAAe,IAAT6G,MAAiBD,EAAY,IAAM,KAE7E,MAAMG,OAAUC,GAAO5E,OAAOC,SAASD,OAAO4E,IAAM,IAAIjH,KAAKC,MAAMoC,OAAO4E,IAAI/G,mBAAqB,MAC7FgH,EAAcL,EAChB,iDACwB,YAAvBrH,EAAM2H,aAA6B,uBAAyB3H,EAAM2H,aAEvE,IAAIC,EAAU,gCAAgCP,EAAY,UAAYG,OAAO7E,KAC7EiF,GAAW,gCAAgCF,IACvC1H,EAAM6H,oBAAmBD,GAAW,iCAAiC5H,EAAM6H,qBAE/E,MAAMC,EAAYC,MAAMC,QAAQhI,EAAMiI,iBAAmBjI,EAAMiI,gBAAkB,GAC7EH,EAAU9B,SACZ4B,GAAW,OAASE,EAAUI,IAAKC,IACjC,MAAMC,EAA0B,MAAlBD,EAAEE,cAA2C,KAAnBF,EAAEE,aAAuB,MAAMF,EAAEE,gBAAkB,GAC3F,MAAO,WAAWF,EAAEG,UAAY,oBAAoBd,OAAOW,EAAEI,WAAWJ,EAAEK,UAAY,KAAKJ,MAASD,EAAEM,YAAc,SACnH1C,KAAK,SAGV,MAAMpC,EAASyD,EAAcrG,QAAQ,WACrC,GAAI4C,EAAQ,CACV,MAAMU,EAAQV,EAAO7C,cAAc,iBAC9B8C,EAAWD,GAIdE,EAAqBF,EAAQiE,IAH7B9D,EAAcH,EAAQiE,GAClBvD,GAAOA,EAAMC,UAAUC,IAAI,eAInC,CACF,EA4KEmE,wBAhKF,SAAiCC,GAC/B,MAAMC,EAAc/H,SAASa,eAAe,oBAC5C,IAAKkH,EAAa,OAClB,IAAIpF,EAAiB,uBACrB,GAAImF,EAAS,CACX,MAAME,EAAYF,EAAQG,UAAYC,EAAWJ,EAAQG,WAAa,GAChEE,EAAYH,EAAY,+BAA+BA,IAAc,GAC3ErF,EAAiB,qCACWmF,EAAQM,4BAClCN,EAAQO,qBAAqBF,SAEjC,CACA,MAAMrF,EAASiF,EAAY7H,QAAQ,WACnC,GAAI4C,EACF,GAAKC,EAAWD,GAKdE,EAAqBF,EAAQH,OALN,CACvBM,EAAcH,EAAQH,GACtB,MAAMa,EAAQV,EAAO7C,cAAc,iBAC/BuD,GAAOA,EAAMC,UAAUC,IAAI,cACjC,CAIJ,EA2IE5D,8CACAwI,iBAzRF,WACE,MAAMC,EAAoBvI,SAASC,cAAc,gBAAgBC,QAAQ,YAAYD,cAAc,iBAC/FsI,IACFA,EAAkBpI,YAAchB,EAAMI,qBAAuB,EAAI,UAAUJ,EAAMI,yBAA2B,QAEhH,EAsRF"}
@@ -1,2 +1,2 @@
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};
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{contactName:s.cachedLoiData.contactName,createdAt:s.cachedLoiData.createdAt,leadStatus:s.cachedLoiData.leadStatus,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={contactName:s.contactName||"No contact available",createdAt:s.createdAt||null,leadStatus:c,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 { 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
+ {"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 contactName: state.cachedLoiData.contactName,\r\n createdAt: state.cachedLoiData.createdAt,\r\n leadStatus: state.cachedLoiData.leadStatus,\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 contactName: data.contactName || \"No contact available\",\r\n createdAt: data.createdAt || null,\r\n leadStatus,\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","createdAt","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
- import{getEnvVar as E}from"../environment/utilities.js";const _={ASSIGNMENT_FEE_PERCENTAGE:.05,NET_TO_BUYER_PERCENTAGE:.1,CLOSING_COSTS_PERCENTAGE:.0125,HARD_MONEY_RATE:.03,REHAB_RATE:0,SELLER_AGENT_COMMISSION:.025,BUYER_AGENT_COMMISSION:.025,MINIMUM_COCR15_PRICE:1e4,MAX_COCR15_PRICE_MULTIPLIER:50,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:20,MAX_ITERATIONS:50,CALCULATION_TOLERANCE:.001,ADJUSTMENT_FACTOR:.5,EXPORT_URL_BASE:E("EXPORT_URL_BASE","https://app.archerjessop.com/property-dashboard/import"),EXCLUDED_EXPORT_VALUES:["Loading...","Not found","",null,void 0],DASHBOARD_URL_BASE:E("DASHBOARD_URL_BASE","https://app.archerjessop.com/property-dashboard/")},{ASSIGNMENT_FEE_PERCENTAGE:R,NET_TO_BUYER_PERCENTAGE:A,CLOSING_COSTS_PERCENTAGE:C,HARD_MONEY_RATE:T,SELLER_AGENT_COMMISSION:I,BUYER_AGENT_COMMISSION:N,MINIMUM_COCR15_PRICE:O,MAX_COCR15_PRICE_MULTIPLIER:S,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:M,REHAB_RATE:L}=_;export{R as ASSIGNMENT_FEE_PERCENTAGE,_ as BUSINESS_CONSTANTS,N as BUYER_AGENT_COMMISSION,C as CLOSING_COSTS_PERCENTAGE,M as CONSERVATIVE_COCR15_PRICE_MULTIPLIER,T as HARD_MONEY_RATE,S as MAX_COCR15_PRICE_MULTIPLIER,O as MINIMUM_COCR15_PRICE,A as NET_TO_BUYER_PERCENTAGE,L as REHAB_RATE,I as SELLER_AGENT_COMMISSION};
1
+ import{getEnvVar as E}from"../environment/utilities.js";const _={ASSIGNMENT_FEE_PERCENTAGE:.05,CASH_OFFER_ASSIGNMENT_PERCENTAGE:.07,NET_TO_BUYER_PERCENTAGE:.1,CLOSING_COSTS_PERCENTAGE:.0125,HARD_MONEY_RATE:.03,REHAB_RATE:0,SELLER_AGENT_COMMISSION:.025,BUYER_AGENT_COMMISSION:.025,MINIMUM_COCR15_PRICE:1e4,MAX_COCR15_PRICE_MULTIPLIER:50,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:20,MAX_ITERATIONS:50,CALCULATION_TOLERANCE:.001,ADJUSTMENT_FACTOR:.5,EXPORT_URL_BASE:E("EXPORT_URL_BASE","https://app.archerjessop.com/property-dashboard/import"),EXCLUDED_EXPORT_VALUES:["Loading...","Not found","",null,void 0],DASHBOARD_URL_BASE:E("DASHBOARD_URL_BASE","https://app.archerjessop.com/property-dashboard/")},{ASSIGNMENT_FEE_PERCENTAGE:R,CASH_OFFER_ASSIGNMENT_PERCENTAGE:A,NET_TO_BUYER_PERCENTAGE:C,CLOSING_COSTS_PERCENTAGE:T,HARD_MONEY_RATE:N,SELLER_AGENT_COMMISSION:I,BUYER_AGENT_COMMISSION:S,MINIMUM_COCR15_PRICE:O,MAX_COCR15_PRICE_MULTIPLIER:M,CONSERVATIVE_COCR15_PRICE_MULTIPLIER:L,REHAB_RATE:P}=_;export{R as ASSIGNMENT_FEE_PERCENTAGE,_ as BUSINESS_CONSTANTS,S as BUYER_AGENT_COMMISSION,A as CASH_OFFER_ASSIGNMENT_PERCENTAGE,T as CLOSING_COSTS_PERCENTAGE,L as CONSERVATIVE_COCR15_PRICE_MULTIPLIER,N as HARD_MONEY_RATE,M as MAX_COCR15_PRICE_MULTIPLIER,O as MINIMUM_COCR15_PRICE,C as NET_TO_BUYER_PERCENTAGE,P as REHAB_RATE,I as SELLER_AGENT_COMMISSION};
2
2
  //# sourceMappingURL=business.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"business.js","sources":["../../src/config/business.js"],"sourcesContent":["/**\r\n * Business logic and fee constants\r\n */\r\n\r\nimport { getEnvVar } from \"../environment/utilities.js\";\r\n\r\nexport const BUSINESS_CONSTANTS = {\r\n // Assignment and transaction fees\r\n ASSIGNMENT_FEE_PERCENTAGE: 0.05, // 5% assignment fee\r\n NET_TO_BUYER_PERCENTAGE: 0.10, // 10% net to buyer\r\n CLOSING_COSTS_PERCENTAGE: 0.0125, // 1.25% closing costs\r\n HARD_MONEY_RATE: 0.03, // 3% financing fee\r\n REHAB_RATE: 0.0, // 0% rehab fee\r\n \r\n // Agent commissions\r\n SELLER_AGENT_COMMISSION: 0.025, // 3% seller agent commission\r\n BUYER_AGENT_COMMISSION: 0.025, // 3% buyer agent commission\r\n \r\n // COCR15 price calculation limits\r\n MINIMUM_COCR15_PRICE: 10000, // $10,000 minimum\r\n MAX_COCR15_PRICE_MULTIPLIER: 50, // Maximum price as multiple of NOI\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER: 20, // Conservative price limit\r\n \r\n // Algorithm parameters (duplicated for business logic context)\r\n\r\n \r\n // Calculation parameters\r\n MAX_ITERATIONS: 50, // Maximum iterations for iterative calculations\r\n CALCULATION_TOLERANCE: 0.001, // Tolerance for convergence\r\n ADJUSTMENT_FACTOR: 0.5, // Adjustment factor for iterations\r\n\r\n\r\n \r\n // URL constants - safe for both Node.js and browser\r\n EXPORT_URL_BASE: getEnvVar(\r\n \"EXPORT_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/import\"\r\n ),\r\n EXCLUDED_EXPORT_VALUES: [\"Loading...\", \"Not found\", \"\", null, undefined],\r\n DASHBOARD_URL_BASE: getEnvVar(\r\n \"DASHBOARD_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/\"\r\n ),\r\n \r\n};\r\n\r\n// Convenience exports for commonly used values\r\nexport const {\r\n ASSIGNMENT_FEE_PERCENTAGE,\r\n NET_TO_BUYER_PERCENTAGE,\r\n CLOSING_COSTS_PERCENTAGE,\r\n HARD_MONEY_RATE,\r\n SELLER_AGENT_COMMISSION,\r\n BUYER_AGENT_COMMISSION,\r\n MINIMUM_COCR15_PRICE,\r\n MAX_COCR15_PRICE_MULTIPLIER,\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER,\r\n REHAB_RATE,\r\n} = BUSINESS_CONSTANTS;\r\n"],"names":["BUSINESS_CONSTANTS","ASSIGNMENT_FEE_PERCENTAGE","NET_TO_BUYER_PERCENTAGE","CLOSING_COSTS_PERCENTAGE","HARD_MONEY_RATE","REHAB_RATE","SELLER_AGENT_COMMISSION","BUYER_AGENT_COMMISSION","MINIMUM_COCR15_PRICE","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MAX_ITERATIONS","CALCULATION_TOLERANCE","ADJUSTMENT_FACTOR","EXPORT_URL_BASE","getEnvVar","EXCLUDED_EXPORT_VALUES","undefined","DASHBOARD_URL_BASE"],"mappings":"wDAMY,MAACA,EAAqB,CAEhCC,0BAA2B,IAC3BC,wBAAyB,GACzBC,yBAA0B,MAC1BC,gBAAiB,IACjBC,WAAY,EAGZC,wBAAyB,KACzBC,uBAAwB,KAGxBC,qBAAsB,IACtBC,4BAA6B,GAC7BC,qCAAsC,GAMtCC,eAAgB,GAChBC,sBAAuB,KACvBC,kBAAmB,GAKnBC,gBAAiBC,EACf,kBACA,0DAEFC,uBAAwB,CAAC,aAAc,YAAa,GAAI,UAAMC,GAC9DC,mBAAoBH,EAClB,qBACA,sDAMSd,0BACXA,EAAyBC,wBACzBA,EAAuBC,yBACvBA,EAAwBC,gBACxBA,EAAeE,wBACfA,EAAuBC,uBACvBA,EAAsBC,qBACtBA,EAAoBC,4BACpBA,EAA2BC,qCAC3BA,EAAoCL,WACpCA,GACEL"}
1
+ {"version":3,"file":"business.js","sources":["../../src/config/business.js"],"sourcesContent":["/**\r\n * Business logic and fee constants\r\n */\r\n\r\nimport { getEnvVar } from \"../environment/utilities.js\";\r\n\r\nexport const BUSINESS_CONSTANTS = {\r\n // Assignment and transaction fees\r\n ASSIGNMENT_FEE_PERCENTAGE: 0.05, // 5% assignment fee\r\n CASH_OFFER_ASSIGNMENT_PERCENTAGE: 0.07, // 7% haircut off the 15%-COCR price for a cash offer (assignment + costs)\r\n NET_TO_BUYER_PERCENTAGE: 0.10, // 10% net to buyer\r\n CLOSING_COSTS_PERCENTAGE: 0.0125, // 1.25% closing costs\r\n HARD_MONEY_RATE: 0.03, // 3% financing fee\r\n REHAB_RATE: 0.0, // 0% rehab fee\r\n \r\n // Agent commissions\r\n SELLER_AGENT_COMMISSION: 0.025, // 3% seller agent commission\r\n BUYER_AGENT_COMMISSION: 0.025, // 3% buyer agent commission\r\n \r\n // COCR15 price calculation limits\r\n MINIMUM_COCR15_PRICE: 10000, // $10,000 minimum\r\n MAX_COCR15_PRICE_MULTIPLIER: 50, // Maximum price as multiple of NOI\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER: 20, // Conservative price limit\r\n \r\n // Algorithm parameters (duplicated for business logic context)\r\n\r\n \r\n // Calculation parameters\r\n MAX_ITERATIONS: 50, // Maximum iterations for iterative calculations\r\n CALCULATION_TOLERANCE: 0.001, // Tolerance for convergence\r\n ADJUSTMENT_FACTOR: 0.5, // Adjustment factor for iterations\r\n\r\n\r\n \r\n // URL constants - safe for both Node.js and browser\r\n EXPORT_URL_BASE: getEnvVar(\r\n \"EXPORT_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/import\"\r\n ),\r\n EXCLUDED_EXPORT_VALUES: [\"Loading...\", \"Not found\", \"\", null, undefined],\r\n DASHBOARD_URL_BASE: getEnvVar(\r\n \"DASHBOARD_URL_BASE\",\r\n \"https://app.archerjessop.com/property-dashboard/\"\r\n ),\r\n \r\n};\r\n\r\n// Convenience exports for commonly used values\r\nexport const {\r\n ASSIGNMENT_FEE_PERCENTAGE,\r\n CASH_OFFER_ASSIGNMENT_PERCENTAGE,\r\n NET_TO_BUYER_PERCENTAGE,\r\n CLOSING_COSTS_PERCENTAGE,\r\n HARD_MONEY_RATE,\r\n SELLER_AGENT_COMMISSION,\r\n BUYER_AGENT_COMMISSION,\r\n MINIMUM_COCR15_PRICE,\r\n MAX_COCR15_PRICE_MULTIPLIER,\r\n CONSERVATIVE_COCR15_PRICE_MULTIPLIER,\r\n REHAB_RATE,\r\n} = BUSINESS_CONSTANTS;\r\n"],"names":["BUSINESS_CONSTANTS","ASSIGNMENT_FEE_PERCENTAGE","CASH_OFFER_ASSIGNMENT_PERCENTAGE","NET_TO_BUYER_PERCENTAGE","CLOSING_COSTS_PERCENTAGE","HARD_MONEY_RATE","REHAB_RATE","SELLER_AGENT_COMMISSION","BUYER_AGENT_COMMISSION","MINIMUM_COCR15_PRICE","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MAX_ITERATIONS","CALCULATION_TOLERANCE","ADJUSTMENT_FACTOR","EXPORT_URL_BASE","getEnvVar","EXCLUDED_EXPORT_VALUES","undefined","DASHBOARD_URL_BASE"],"mappings":"wDAMY,MAACA,EAAqB,CAEhCC,0BAA2B,IAC3BC,iCAAkC,IAClCC,wBAAyB,GACzBC,yBAA0B,MAC1BC,gBAAiB,IACjBC,WAAY,EAGZC,wBAAyB,KACzBC,uBAAwB,KAGxBC,qBAAsB,IACtBC,4BAA6B,GAC7BC,qCAAsC,GAMtCC,eAAgB,GAChBC,sBAAuB,KACvBC,kBAAmB,GAKnBC,gBAAiBC,EACf,kBACA,0DAEFC,uBAAwB,CAAC,aAAc,YAAa,GAAI,UAAMC,GAC9DC,mBAAoBH,EAClB,qBACA,sDAMSf,0BACXA,EAAyBC,iCACzBA,EAAgCC,wBAChCA,EAAuBC,yBACvBA,EAAwBC,gBACxBA,EAAeE,wBACfA,EAAuBC,uBACvBA,EAAsBC,qBACtBA,EAAoBC,4BACpBA,EAA2BC,qCAC3BA,EAAoCL,WACpCA,GACEN"}
@@ -1,2 +1,2 @@
1
- import{FINANCIAL_CONSTANTS as e}from"../config/financial.js";import{BUSINESS_CONSTANTS as t}from"../config/business.js";import{PROPERTY_TYPES as r,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";const c=e.INTEREST_RATE_TIERS[e.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(e,t,r){if(0===t)return e/(12*r);const a=t/12,c=12*r;return e*(a*Math.pow(1+a,c))/(Math.pow(1+a,c)-1)}function calculateCOCR30(e,t){try{const r=.3*e,a=12*calculatePMT(.7*e,.075,30);return(t-a)/r*100}catch(e){return 0}}function calculateCashFlowYield(e,t){if(!t||t<=0)return 0;return 12*e/t*100}function equityPercentFromDebt(e,t){const r=Number(e),a=Number(t);return!Number.isFinite(r)||r<=0?1:Number.isFinite(a)?(r-a)/r:1}function calculatePriceForCOCR(r,a=.15,n={}){const{downPercent:l=100*e.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:u=100*e.DEFAULT_DSCR_PERCENTAGE,dscrRate:o=c.rate,dscrTerm:i=c.amortization,maxIterations:s=t.MAX_ITERATIONS,tolerance:E=t.CALCULATION_TOLERANCE}=n;try{let e=r/.08,c=0;for(;c<s;){const n=e*(l/100),s=12*calculatePMT(e*(u/100),o,i),T=(r-s)/n;if(Math.abs(T-a)<E)break;const R=T-a,A=R*t.ADJUSTMENT_FACTOR;e*=R>0?1+Math.abs(A):1-Math.abs(A),e<1e3&&(e=1e3),e>r*t.MAX_COCR15_PRICE_MULTIPLIER&&(e=r*t.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),c++}return e<t.MINIMUM_COCR15_PRICE&&(e=t.MINIMUM_COCR15_PRICE),e}catch(e){return 0}}function calculateCOCRAtPercent(e,t,r,a={}){const{dscrRate:n=c.rate,dscrTerm:l=c.amortization}=a;try{const a=e*(r/100),c=12*calculatePMT(e-a,n,l);return(t-c)/a*100}catch(e){return 0}}function calculateSTRNOI(e,t=null,r={}){const{grossRate:c=a.STR.ESTIMATED_GROSS_RATE,noiPercentage:n=a.STR.NOI_PERCENTAGE}=r;try{if(t&&Number.isFinite(t.value)&&t.value>=0){if("noi"===t.type)return t.value;if("gross"===t.type)return t.value*n}return!Number.isFinite(e)||e<=0?0:e*c*n}catch(e){return 0}}function calculateNOIByType(e,t,c=r.MULTIFAMILY,n={}){const{strApiResult:l=null,strGrossIncomeMultiplier:u=a.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:o=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:i=a.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:s=a.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch((c||r.MULTIFAMILY).toLowerCase()){case r.STR:return calculateSTRNOI(e,l,{grossRate:u,noiPercentage:o});case r.ASSISTED_LIVING:return s*i*12;case r.MULTIFAMILY:default:return e*t}}catch(e){return 0}}function resolveListingFinancials({bedroomCount:e=null,confirmedNOI:t=null,estimatedCapRate:a,price:c,propertyType:n=r.MULTIFAMILY,reportedCapRate:l=null,strApiResult:u=null}={}){const o=null!=l&&Number.isFinite(l),i=o?l:a,s=u&&Number.isFinite(u.value)&&u.value>=0&&("noi"===u.type||"gross"===u.type);let E,T;if(null!=t&&Number.isFinite(t)&&t>=0)E=t,T="confirmed";else{E=calculateNOIByType(c,i,n,{bedroomCount:e,strApiResult:u});const t=(n||"").toLowerCase();T=t===r.STR?s?"measured":"estimate":t===r.ASSISTED_LIVING?"bedrooms":o?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(c)&&c>0?E/c:null,noi:E,noiSource:T,reportedCapRate:o?l:null}}function calculateAssignmentFee(e,r=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(r/100)}catch(e){return 0}}function calculateNetToBuyer(r,a={}){const{buyerCostPercent:c=100*t.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*t.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*t.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*t.REHAB_RATE,additionalCostFinancing:o=100*t.HARD_MONEY_RATE,dscrLtvPercent:i=100*e.DEFAULT_DSCR_PERCENTAGE}=a;try{return r*(c/100)-r*((n+l)/100)-r*(u/100)-o/100*(r-r*(i/100))}catch(e){return 0}}function calculateBalloonBalance(t,r,a,c=e.DEFAULT_BALLOON_PERIOD_YEARS){try{if(t<=0||r<0||a<=0||c<=0)return 0;if(c>=a)return 0;if(0===r){const e=12*a;return t*(e-12*c)/e}const e=r/12,n=12*a,l=12*c,u=Math.pow(1+e,n),o=t*(u-Math.pow(1+e,l))/(u-1);return Math.max(0,o)}catch(e){return 0}}function calculateAppreciatedValue(t,r=e.APPRECIATION_RATE,a=e.DEFAULT_BALLOON_PERIOD_YEARS){try{return t<=0||r<0||a<0?t:t*Math.pow(1+r,a)}catch(e){return t}}function calculateCashOutAfterRefi(t,r,a,n={}){const{appreciationRate:l=e.APPRECIATION_RATE,balloonYears:u=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:o=c.rate,dscrTerm:i=c.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=n;try{const c=calculateAppreciatedValue(t,l,u),n=calculateBalloonBalance(r,o,i,u),T=calculateBalloonBalance(a,e.SELLER_FI_INTEREST_RATE,s,u);return c*(E/100)-(n+T)}catch(e){return 0}}function calculateCashFlow(e,t,r){return e-(t+r)}function calculateDiscountFromPrice(e,t){return!e||e<=0?0:(e-t)/e}function calculatePriceFromDiscount(e,t){return!e||e<=0?0:e*(1-t)}function safePercentage(e,t=100){return null==e||isNaN(e)?t:100*e}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage};
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 c}from"../config/property-types.js";const a=e.INTEREST_RATE_TIERS[e.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(e,t,r){if(0===t)return e/(12*r);const c=t/12,a=12*r;return e*(c*Math.pow(1+c,a))/(Math.pow(1+c,a)-1)}function calculateCOCR30(e,t){try{const r=.3*e,c=12*calculatePMT(.7*e,.075,30);return(t-c)/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),c=Number(t);return!Number.isFinite(r)||r<=0?1:Number.isFinite(c)?(r-c)/r:1}function calculatePriceForCOCR(r,c=.15,n={}){const{downPercent:l=100*e.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:u=100*e.DEFAULT_DSCR_PERCENTAGE,dscrRate:i=a.rate,dscrTerm:o=a.amortization,maxIterations:s=t.MAX_ITERATIONS,tolerance:E=t.CALCULATION_TOLERANCE}=n;try{let e=r/.08,a=0;for(;a<s;){const n=e*(l/100),s=12*calculatePMT(e*(u/100),i,o),T=(r-s)/n;if(Math.abs(T-c)<E)break;const R=T-c,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),a++}return e<t.MINIMUM_COCR15_PRICE&&(e=t.MINIMUM_COCR15_PRICE),e}catch(e){return 0}}function calculateCOCRAtPercent(e,t,r,c={}){const{dscrRate:n=a.rate,dscrTerm:l=a.amortization}=c;try{const c=e*(r/100),a=12*calculatePMT(e-c,n,l);return(t-a)/c*100}catch(e){return 0}}function calculateSTRNOI(e,t=null,r={}){const{grossRate:a=c.STR.ESTIMATED_GROSS_RATE,noiPercentage:n=c.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*a*n}catch(e){return 0}}function calculateNOIByType(e,t,a=r.MULTIFAMILY,n={}){const{strApiResult:l=null,strGrossIncomeMultiplier:u=c.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:i=c.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:o=c.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:s=c.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch((a||r.MULTIFAMILY).toLowerCase()){case r.STR:return calculateSTRNOI(e,l,{grossRate:u,noiPercentage:i});case r.ASSISTED_LIVING:return s*o*12;case r.MULTIFAMILY:default:return e*t}}catch(e){return 0}}function resolveListingFinancials({bedroomCount:e=null,confirmedNOI:t=null,estimatedCapRate:c,price:a,propertyType:n=r.MULTIFAMILY,reportedCapRate:l=null,strApiResult:u=null}={}){const i=null!=l&&Number.isFinite(l),o=i?l:c,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(a,o,n,{bedroomCount:e,strApiResult:u});const t=(n||"").toLowerCase();T=t===r.STR?s?"measured":"estimate":t===r.ASSISTED_LIVING?"bedrooms":i?"cap":"estimate"}Number.isFinite(E)||(E=0);return{activeCapRate:Number.isFinite(a)&&a>0?E/a:null,noi:E,noiSource:T,reportedCapRate:i?l:null}}function calculateAssignmentFee(e,r=100*t.ASSIGNMENT_FEE_PERCENTAGE){try{return e*(r/100)}catch(e){return 0}}function calculateCashOfferPrice(e,r=t.CASH_OFFER_ASSIGNMENT_PERCENTAGE){const c=Number(e);return!Number.isFinite(c)||c<=0?null:c*(1-r)}function calculateNetToBuyer(r,c={}){const{buyerCostPercent:a=100*t.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*t.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*t.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*t.REHAB_RATE,additionalCostFinancing:i=100*t.HARD_MONEY_RATE,dscrLtvPercent:o=100*e.DEFAULT_DSCR_PERCENTAGE}=c;try{return r*(a/100)-r*((n+l)/100)-r*(u/100)-i/100*(r-r*(o/100))}catch(e){return 0}}function calculateBalloonBalance(t,r,c,a=e.DEFAULT_BALLOON_PERIOD_YEARS){try{if(t<=0||r<0||c<=0||a<=0)return 0;if(a>=c)return 0;if(0===r){const e=12*c;return t*(e-12*a)/e}const e=r/12,n=12*c,l=12*a,u=Math.pow(1+e,n),i=t*(u-Math.pow(1+e,l))/(u-1);return Math.max(0,i)}catch(e){return 0}}function calculateAppreciatedValue(t,r=e.APPRECIATION_RATE,c=e.DEFAULT_BALLOON_PERIOD_YEARS){try{return t<=0||r<0||c<0?t:t*Math.pow(1+r,c)}catch(e){return t}}function calculateCashOutAfterRefi(t,r,c,n={}){const{appreciationRate:l=e.APPRECIATION_RATE,balloonYears:u=e.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:i=a.rate,dscrTerm:o=a.amortization,sellerFiTerm:s=e.SELLER_FI_AMORTIZATION,refiLtvPercent:E=70}=n;try{const a=calculateAppreciatedValue(t,l,u),n=calculateBalloonBalance(r,i,o,u),T=calculateBalloonBalance(c,e.SELLER_FI_INTEREST_RATE,s,u);return a*(E/100)-(n+T)}catch(e){return 0}}function calculateCashFlow(e,t,r){return e-(t+r)}function calculateDiscountFromPrice(e,t){return!e||e<=0?0:(e-t)/e}function calculatePriceFromDiscount(e,t){return!e||e<=0?0:e*(1-t)}function safePercentage(e,t=100){return null==e||isNaN(e)?t:100*e}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOfferPrice,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage};
2
2
  //# sourceMappingURL=calculations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n/**\r\n * Equity as a decimal fraction of price, derived from outstanding debt.\r\n * equity = (price - debtBalance) / price. Recompute whenever the user edits price.\r\n * Falls back to 1 (100% equity) when debt is unavailable or price is non-positive,\r\n * matching the \"estimated = 100%\" rule when the debt service returns no number.\r\n * @param {number} price - Listing/asking/offered price.\r\n * @param {number} debtBalance - Outstanding mortgage balance owing.\r\n * @returns {number} Equity as a decimal (e.g. 0.59 for 59%); 1 when debt is unknown.\r\n */\r\nexport function equityPercentFromDebt(price, debtBalance) {\r\n const p = Number(price);\r\n const d = Number(debtBalance);\r\n if (!Number.isFinite(p) || p <= 0) return 1;\r\n if (!Number.isFinite(d)) return 1;\r\n return (p - d) / p;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate STR (short-term rental) NOI - the single source of STR NOI math.\r\n *\r\n * Resolution order:\r\n * 1. apiResult type 'noi' -> value is already net, return as-is\r\n * 2. apiResult type 'gross' -> apply NOI margin (value * noiPercentage)\r\n * 3. no/invalid apiResult -> estimate from price (price * grossRate * noiPercentage)\r\n *\r\n * apiResult comes from api.archerjessop.com/str-revenue: { value, type }.\r\n * Pass null while that backend is not live (the price estimate is used).\r\n *\r\n * @param {number} askingPrice - Property asking price\r\n * @param {{value:number, type:'noi'|'gross'}|null} apiResult - STR revenue API result\r\n * @param {Object} options - Rate overrides (default to STR config constants)\r\n * @returns {number} Annual NOI (0 on invalid input)\r\n */\r\nexport function calculateSTRNOI(askingPrice, apiResult = null, options = {}) {\r\n const {\r\n grossRate = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n noiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE\r\n } = options;\r\n\r\n try {\r\n if (apiResult && Number.isFinite(apiResult.value) && apiResult.value >= 0) {\r\n if (apiResult.type === \"noi\") return apiResult.value;\r\n if (apiResult.type === \"gross\") return apiResult.value * noiPercentage;\r\n }\r\n\r\n if (!Number.isFinite(askingPrice) || askingPrice <= 0) return 0;\r\n return askingPrice * grossRate * noiPercentage;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strApiResult = null,\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch ((propertyType || PROPERTY_TYPES.MULTIFAMILY).toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n return calculateSTRNOI(askingPrice, strApiResult, {\r\n grossRate: strGrossIncomeMultiplier,\r\n noiPercentage: strNoiPercentage\r\n });\r\n\r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Resolve a listing's canonical NOI and the cap rates derived from it.\r\n *\r\n * NOI is the source of truth; each property type computes it by its own model\r\n * (delegated to calculateNOIByType, unchanged). The active (displayed) cap rate is\r\n * always derived as NOI / price. The reported cap rate is carried through as\r\n * provenance (shown on hover; null => the UI shows \"N/A\"); it only DRIVES the NOI for\r\n * multifamily, where the cap rate is the model input.\r\n *\r\n * NOI precedence: an analyst-confirmed NOI overrides the per-type model; otherwise STR\r\n * uses measured 3rd-party revenue when present and the price-based estimate otherwise;\r\n * assisted uses bedroom count; multifamily uses the reported cap (or estimatedCapRate\r\n * when none was reported).\r\n *\r\n * @param {Object} input\r\n * @param {number|null} [input.bedroomCount] - Bedroom count for assisted living\r\n * @param {number|null} [input.confirmedNOI] - Analyst-confirmed/edited NOI; overrides the per-type model\r\n * @param {number} [input.estimatedCapRate] - Fallback cap (decimal) used for multifamily NOI when none was reported\r\n * @param {number} input.price - Asking price\r\n * @param {string} [input.propertyType] - One of PROPERTY_TYPES\r\n * @param {number|null} [input.reportedCapRate] - Real scraped/listed cap rate as a decimal (e.g. 0.0486); null when none was reported\r\n * @param {{value:number,type:'noi'|'gross'}|null} [input.strApiResult] - Measured STR revenue (3rd-party); null until that backend ships\r\n * @returns {{activeCapRate:(number|null), noi:number, noiSource:string, reportedCapRate:(number|null)}}\r\n */\r\nexport function resolveListingFinancials({\r\n bedroomCount = null,\r\n confirmedNOI = null,\r\n estimatedCapRate,\r\n price,\r\n propertyType = PROPERTY_TYPES.MULTIFAMILY,\r\n reportedCapRate = null,\r\n strApiResult = null,\r\n} = {}) {\r\n const hasReported = reportedCapRate != null && Number.isFinite(reportedCapRate);\r\n const capForNOI = hasReported ? reportedCapRate : estimatedCapRate;\r\n const measured = strApiResult && Number.isFinite(strApiResult.value) && strApiResult.value >= 0 &&\r\n (strApiResult.type === \"noi\" || strApiResult.type === \"gross\");\r\n\r\n let noi;\r\n let noiSource;\r\n if (confirmedNOI != null && Number.isFinite(confirmedNOI) && confirmedNOI >= 0) {\r\n noi = confirmedNOI;\r\n noiSource = \"confirmed\";\r\n } else {\r\n noi = calculateNOIByType(price, capForNOI, propertyType, { bedroomCount, strApiResult });\r\n const type = (propertyType || \"\").toLowerCase();\r\n if (type === PROPERTY_TYPES.STR) {\r\n noiSource = measured ? \"measured\" : \"estimate\";\r\n } else if (type === PROPERTY_TYPES.ASSISTED_LIVING) {\r\n noiSource = \"bedrooms\";\r\n } else {\r\n noiSource = hasReported ? \"cap\" : \"estimate\";\r\n }\r\n }\r\n\r\n if (!Number.isFinite(noi)) noi = 0;\r\n const activeCapRate = Number.isFinite(price) && price > 0 ? noi / price : null;\r\n\r\n return {\r\n activeCapRate,\r\n noi,\r\n noiSource,\r\n reportedCapRate: hasReported ? reportedCapRate : null,\r\n };\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","equityPercentFromDebt","price","debtBalance","p","Number","d","isFinite","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","amortization","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateSTRNOI","apiResult","grossRate","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","noiPercentage","NOI_PERCENTAGE","value","type","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strApiResult","strGrossIncomeMultiplier","strNoiPercentage","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","resolveListingFinancials","confirmedNOI","estimatedCapRate","reportedCapRate","hasReported","capForNOI","measured","noiSource","activeCapRate","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","fallback","isNaN"],"mappings":"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"}
1
+ {"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n/**\r\n * Equity as a decimal fraction of price, derived from outstanding debt.\r\n * equity = (price - debtBalance) / price. Recompute whenever the user edits price.\r\n * Falls back to 1 (100% equity) when debt is unavailable or price is non-positive,\r\n * matching the \"estimated = 100%\" rule when the debt service returns no number.\r\n * @param {number} price - Listing/asking/offered price.\r\n * @param {number} debtBalance - Outstanding mortgage balance owing.\r\n * @returns {number} Equity as a decimal (e.g. 0.59 for 59%); 1 when debt is unknown.\r\n */\r\nexport function equityPercentFromDebt(price, debtBalance) {\r\n const p = Number(price);\r\n const d = Number(debtBalance);\r\n if (!Number.isFinite(p) || p <= 0) return 1;\r\n if (!Number.isFinite(d)) return 1;\r\n return (p - d) / p;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate STR (short-term rental) NOI - the single source of STR NOI math.\r\n *\r\n * Resolution order:\r\n * 1. apiResult type 'noi' -> value is already net, return as-is\r\n * 2. apiResult type 'gross' -> apply NOI margin (value * noiPercentage)\r\n * 3. no/invalid apiResult -> estimate from price (price * grossRate * noiPercentage)\r\n *\r\n * apiResult comes from api.archerjessop.com/str-revenue: { value, type }.\r\n * Pass null while that backend is not live (the price estimate is used).\r\n *\r\n * @param {number} askingPrice - Property asking price\r\n * @param {{value:number, type:'noi'|'gross'}|null} apiResult - STR revenue API result\r\n * @param {Object} options - Rate overrides (default to STR config constants)\r\n * @returns {number} Annual NOI (0 on invalid input)\r\n */\r\nexport function calculateSTRNOI(askingPrice, apiResult = null, options = {}) {\r\n const {\r\n grossRate = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n noiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE\r\n } = options;\r\n\r\n try {\r\n if (apiResult && Number.isFinite(apiResult.value) && apiResult.value >= 0) {\r\n if (apiResult.type === \"noi\") return apiResult.value;\r\n if (apiResult.type === \"gross\") return apiResult.value * noiPercentage;\r\n }\r\n\r\n if (!Number.isFinite(askingPrice) || askingPrice <= 0) return 0;\r\n return askingPrice * grossRate * noiPercentage;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strApiResult = null,\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch ((propertyType || PROPERTY_TYPES.MULTIFAMILY).toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n return calculateSTRNOI(askingPrice, strApiResult, {\r\n grossRate: strGrossIncomeMultiplier,\r\n noiPercentage: strNoiPercentage\r\n });\r\n\r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Resolve a listing's canonical NOI and the cap rates derived from it.\r\n *\r\n * NOI is the source of truth; each property type computes it by its own model\r\n * (delegated to calculateNOIByType, unchanged). The active (displayed) cap rate is\r\n * always derived as NOI / price. The reported cap rate is carried through as\r\n * provenance (shown on hover; null => the UI shows \"N/A\"); it only DRIVES the NOI for\r\n * multifamily, where the cap rate is the model input.\r\n *\r\n * NOI precedence: an analyst-confirmed NOI overrides the per-type model; otherwise STR\r\n * uses measured 3rd-party revenue when present and the price-based estimate otherwise;\r\n * assisted uses bedroom count; multifamily uses the reported cap (or estimatedCapRate\r\n * when none was reported).\r\n *\r\n * @param {Object} input\r\n * @param {number|null} [input.bedroomCount] - Bedroom count for assisted living\r\n * @param {number|null} [input.confirmedNOI] - Analyst-confirmed/edited NOI; overrides the per-type model\r\n * @param {number} [input.estimatedCapRate] - Fallback cap (decimal) used for multifamily NOI when none was reported\r\n * @param {number} input.price - Asking price\r\n * @param {string} [input.propertyType] - One of PROPERTY_TYPES\r\n * @param {number|null} [input.reportedCapRate] - Real scraped/listed cap rate as a decimal (e.g. 0.0486); null when none was reported\r\n * @param {{value:number,type:'noi'|'gross'}|null} [input.strApiResult] - Measured STR revenue (3rd-party); null until that backend ships\r\n * @returns {{activeCapRate:(number|null), noi:number, noiSource:string, reportedCapRate:(number|null)}}\r\n */\r\nexport function resolveListingFinancials({\r\n bedroomCount = null,\r\n confirmedNOI = null,\r\n estimatedCapRate,\r\n price,\r\n propertyType = PROPERTY_TYPES.MULTIFAMILY,\r\n reportedCapRate = null,\r\n strApiResult = null,\r\n} = {}) {\r\n const hasReported = reportedCapRate != null && Number.isFinite(reportedCapRate);\r\n const capForNOI = hasReported ? reportedCapRate : estimatedCapRate;\r\n const measured = strApiResult && Number.isFinite(strApiResult.value) && strApiResult.value >= 0 &&\r\n (strApiResult.type === \"noi\" || strApiResult.type === \"gross\");\r\n\r\n let noi;\r\n let noiSource;\r\n if (confirmedNOI != null && Number.isFinite(confirmedNOI) && confirmedNOI >= 0) {\r\n noi = confirmedNOI;\r\n noiSource = \"confirmed\";\r\n } else {\r\n noi = calculateNOIByType(price, capForNOI, propertyType, { bedroomCount, strApiResult });\r\n const type = (propertyType || \"\").toLowerCase();\r\n if (type === PROPERTY_TYPES.STR) {\r\n noiSource = measured ? \"measured\" : \"estimate\";\r\n } else if (type === PROPERTY_TYPES.ASSISTED_LIVING) {\r\n noiSource = \"bedrooms\";\r\n } else {\r\n noiSource = hasReported ? \"cap\" : \"estimate\";\r\n }\r\n }\r\n\r\n if (!Number.isFinite(noi)) noi = 0;\r\n const activeCapRate = Number.isFinite(price) && price > 0 ? noi / price : null;\r\n\r\n return {\r\n activeCapRate,\r\n noi,\r\n noiSource,\r\n reportedCapRate: hasReported ? reportedCapRate : null,\r\n };\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate the all-cash offer price from the 15%-COCR price.\r\n * The cash offer is the standard-investor 15%-COCR price less a haircut that\r\n * covers the assignment fee and associated costs.\r\n * @param {number} cocr15Price - Price at which COCR hits 15% on the NOI (dollars)\r\n * @param {number} assignmentPercent - Haircut as a decimal (uses config default)\r\n * @returns {number|null} Cash offer price, or null when cocr15Price is not a positive number\r\n */\r\nexport function calculateCashOfferPrice(cocr15Price, assignmentPercent = BUSINESS_CONSTANTS.CASH_OFFER_ASSIGNMENT_PERCENTAGE) {\r\n const price = Number(cocr15Price);\r\n if (!Number.isFinite(price) || price <= 0) return null;\r\n return price * (1 - assignmentPercent);\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","equityPercentFromDebt","price","debtBalance","p","Number","d","isFinite","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","amortization","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateSTRNOI","apiResult","grossRate","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","noiPercentage","NOI_PERCENTAGE","value","type","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strApiResult","strGrossIncomeMultiplier","strNoiPercentage","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","resolveListingFinancials","confirmedNOI","estimatedCapRate","reportedCapRate","hasReported","capForNOI","measured","noiSource","activeCapRate","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateCashOfferPrice","cocr15Price","CASH_OFFER_ASSIGNMENT_PERCENTAGE","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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"}
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,equityPercentFromDebt,resolveListingFinancials,safePercentage}from"./financial/calculations.js";export{EQUITY_CARRY_TIERS,calculateEquityCarryScore}from"./financial/equity-carry.js";export{fetchDebt}from"./services/debt.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType}from"./export/export-logic.js";export{calculateDOM,formatDate}from"./date/utilities.js";export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput}from"./formatting/financial-formatting.js";export{normalizeWhitespace}from"./formatting/text.js";export{CALCULATION_TOLERANCE,DEFAULT_CAP_RATE,DEFAULT_DOWN_PAYMENT,DEFAULT_DSCR_PERCENTAGE,DEFAULT_EQUITY_ESTIMATE,DEFAULT_INTEREST_RATE_TYPE,FINANCIAL_CONSTANTS,INTEREST_RATE_TIERS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE,determineInterestRateType}from"./config/financial.js";export{ASSISTED_LIVING,MULTIFAMILY,PROPERTY_TYPES,PROPERTY_TYPE_CONSTANTS,STR}from"./config/property-types.js";export{ASSIGNMENT_FEE_PERCENTAGE,BUSINESS_CONSTANTS,BUYER_AGENT_COMMISSION,CLOSING_COSTS_PERCENTAGE,CONSERVATIVE_COCR15_PRICE_MULTIPLIER,HARD_MONEY_RATE,MAX_COCR15_PRICE_MULTIPLIER,MINIMUM_COCR15_PRICE,NET_TO_BUYER_PERCENTAGE,REHAB_RATE,SELLER_AGENT_COMMISSION}from"./config/business.js";export{lookupLOI}from"./services/loi-lookup.js";export{LOI_LOOKUP_CONFIG,LOI_SENT_STATUS,MATCH_TYPES}from"./config/loi-lookup.js";export{getEnvVar,isBrowserEnvironment,isNodeEnvironment}from"./environment/utilities.js";const e="./dist/styles/base.css";export{e as STYLES_PATH};
1
+ export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOfferPrice,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,equityPercentFromDebt,resolveListingFinancials,safePercentage}from"./financial/calculations.js";export{EQUITY_CARRY_TIERS,calculateEquityCarryScore}from"./financial/equity-carry.js";export{fetchDebt}from"./services/debt.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent,mapPropertyType}from"./export/export-logic.js";export{calculateDOM,formatDate}from"./date/utilities.js";export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput}from"./formatting/financial-formatting.js";export{normalizeWhitespace}from"./formatting/text.js";export{CALCULATION_TOLERANCE,DEFAULT_CAP_RATE,DEFAULT_DOWN_PAYMENT,DEFAULT_DSCR_PERCENTAGE,DEFAULT_EQUITY_ESTIMATE,DEFAULT_INTEREST_RATE_TYPE,FINANCIAL_CONSTANTS,INTEREST_RATE_TIERS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE,determineInterestRateType}from"./config/financial.js";export{ASSISTED_LIVING,MULTIFAMILY,PROPERTY_TYPES,PROPERTY_TYPE_CONSTANTS,STR}from"./config/property-types.js";export{ASSIGNMENT_FEE_PERCENTAGE,BUSINESS_CONSTANTS,BUYER_AGENT_COMMISSION,CASH_OFFER_ASSIGNMENT_PERCENTAGE,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 equityPercentFromDebt,\r\n resolveListingFinancials,\r\n safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Equity Carry scoring engine (pure; scores scraped listings into deal pools)\r\nexport { EQUITY_CARRY_TIERS, calculateEquityCarryScore } from \"./financial/equity-carry.js\";\r\n\r\n// Agnostic debt service (pure IO; Node + browser)\r\nexport { fetchDebt } from \"./services/debt.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Export logic (pure export-object creation)\r\nexport {\r\n calculateOriginalPrice,\r\n convertCapRateToDecimal,\r\n createExportObjectCore,\r\n formatDownPaymentPercent,\r\n mapPropertyType,\r\n} from \"./export/export-logic.js\";\r\n\r\n// Date utilities\r\nexport { calculateDOM, formatDate } from \"./date/utilities.js\";\r\n\r\n// Formatting utilities\r\nexport { \r\n calculateCursorPosition,\r\n extractNumericValue,\r\n filterNumericInput,\r\n formatInputDisplay,\r\n formatLiveInput,\r\n formatLiveNumber,\r\n parseNumericInput\r\n} from \"./formatting/financial-formatting.js\";\r\n\r\n// Text formatting utilities\r\nexport { normalizeWhitespace } from \"./formatting/text.js\";\r\n\r\n// Configuration constants\r\nexport * from \"./config/financial.js\";\r\nexport * from \"./config/property-types.js\";\r\nexport * from \"./config/business.js\";\r\n\r\nexport const STYLES_PATH = \"./dist/styles/base.css\";\r\n\r\n// LOI Lookup service and config\r\nexport { lookupLOI } from \"./services/loi-lookup.js\";\r\nexport { LOI_LOOKUP_CONFIG, MATCH_TYPES, LOI_SENT_STATUS } from \"./config/loi-lookup.js\";\r\n\r\n// Environment utilities\r\nexport { \r\n getEnvVar, \r\n isNodeEnvironment, \r\n isBrowserEnvironment \r\n} from \"./environment/utilities.js\";"],"names":["STYLES_PATH"],"mappings":"w/DAmEY,MAACA,EAAc"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\r\n * @archerjessop/utilities\r\n * Shared utilities for ArcherJessop property analysis tools\r\n */\r\n\r\n// Financial calculations\r\nexport { \r\n calculateAppreciatedValue,\r\n calculateAssignmentFee,\r\n calculateBalloonBalance,\r\n calculateCashFlow,\r\n calculateCashFlowYield,\r\n calculateCashOfferPrice,\r\n calculateCashOutAfterRefi,\r\n calculateCOCR30, \r\n calculateCOCRAtPercent,\r\n calculateDiscountFromPrice,\r\n calculateNetToBuyer,\r\n calculateNOIByType,\r\n calculatePMT,\r\n calculatePriceForCOCR,\r\n calculatePriceFromDiscount,\r\n calculateSTRNOI,\r\n equityPercentFromDebt,\r\n resolveListingFinancials,\r\n safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Equity Carry scoring engine (pure; scores scraped listings into deal pools)\r\nexport { EQUITY_CARRY_TIERS, calculateEquityCarryScore } from \"./financial/equity-carry.js\";\r\n\r\n// Agnostic debt service (pure IO; Node + browser)\r\nexport { fetchDebt } from \"./services/debt.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Export logic (pure export-object creation)\r\nexport {\r\n calculateOriginalPrice,\r\n convertCapRateToDecimal,\r\n createExportObjectCore,\r\n formatDownPaymentPercent,\r\n mapPropertyType,\r\n} from \"./export/export-logic.js\";\r\n\r\n// Date utilities\r\nexport { calculateDOM, formatDate } from \"./date/utilities.js\";\r\n\r\n// Formatting utilities\r\nexport { \r\n calculateCursorPosition,\r\n extractNumericValue,\r\n filterNumericInput,\r\n formatInputDisplay,\r\n formatLiveInput,\r\n formatLiveNumber,\r\n parseNumericInput\r\n} from \"./formatting/financial-formatting.js\";\r\n\r\n// Text formatting utilities\r\nexport { normalizeWhitespace } from \"./formatting/text.js\";\r\n\r\n// Configuration constants\r\nexport * from \"./config/financial.js\";\r\nexport * from \"./config/property-types.js\";\r\nexport * from \"./config/business.js\";\r\n\r\nexport const STYLES_PATH = \"./dist/styles/base.css\";\r\n\r\n// LOI Lookup service and config\r\nexport { lookupLOI } from \"./services/loi-lookup.js\";\r\nexport { LOI_LOOKUP_CONFIG, MATCH_TYPES, LOI_SENT_STATUS } from \"./config/loi-lookup.js\";\r\n\r\n// Environment utilities\r\nexport { \r\n getEnvVar, \r\n isNodeEnvironment, \r\n isBrowserEnvironment \r\n} from \"./environment/utilities.js\";"],"names":["STYLES_PATH"],"mappings":"ijEAoEY,MAACA,EAAc"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "7.26.0",
3
+ "version": "7.28.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",