[object Object]

← back to Commercialrealestate

DSCR Qualifier: add vacancy % + interest-only underwriting levers (R1 completed)

8e33148699aa3a8587321a3213410dfcb6f61d14 · 2026-08-06 17:02:33 -0700 · Steve

Two real DSCR-loan levers, neutral at defaults (six-unit stays 1.35):
- Vacancy %: DSCR on EFFECTIVE rents = gross x (1 - vac%); breakdown + note show effective vs gross when vac>0
- Interest-only toggle (Build mode): payment = loan x rate/12 vs amortizing P&I (CREFin); breakdown + note label it
R1 agent added the UI then died on a transient rate-limit; I wired the logic.
Verified real Chrome: default 1.35, 5% vac 1.28, interest-only 1.48, toggles back to 1.35.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 8e33148699aa3a8587321a3213410dfcb6f61d14
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 17:02:33 2026 -0700

    DSCR Qualifier: add vacancy % + interest-only underwriting levers (R1 completed)
    
    Two real DSCR-loan levers, neutral at defaults (six-unit stays 1.35):
    - Vacancy %: DSCR on EFFECTIVE rents = gross x (1 - vac%); breakdown + note show effective vs gross when vac>0
    - Interest-only toggle (Build mode): payment = loan x rate/12 vs amortizing P&I (CREFin); breakdown + note label it
    R1 agent added the UI then died on a transient rate-limit; I wired the logic.
    Verified real Chrome: default 1.35, 5% vac 1.28, interest-only 1.48, toggles back to 1.35.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/crcp.html | 49 ++++++++++++++++++++++++++++++++-----------------
 1 file changed, 32 insertions(+), 17 deletions(-)

diff --git a/public/crcp.html b/public/crcp.html
index 2bc512c..e846aa1 100644
--- a/public/crcp.html
+++ b/public/crcp.html
@@ -201,10 +201,14 @@
           <button type="button" class="addunit" id="dAddUnit">+ add unit</button>
           <div class="rentsum">Gross monthly rents: <b id="dRentSum">—</b> &nbsp;·&nbsp; annual <b id="dRentAnn">—</b></div>
         </div>
+        <div class="dfield"><label>Vacancy % (0 = fully occupied)</label><input type="number" id="dVac" value="0" min="0" max="30" step="1"></div>
         <div class="dfield"><label>PITI — the monthly payment</label>
           <div class="dtog" id="dPitiMode"><button type="button" class="on" data-v="build">Build it</button><button type="button" data-v="direct">Enter directly</button></div>
         </div>
         <div id="dBuild">
+          <div class="dfield"><label>Payment type</label>
+            <div class="dtog" id="dPayType"><button type="button" class="on" data-v="pi">P&amp;I (amortizing)</button><button type="button" data-v="io">Interest-only</button></div>
+          </div>
           <div class="row2">
             <div class="dfield"><label>Loan amount</label><input type="number" id="dLoan" value="960000" min="0" step="1000"></div>
             <div class="dfield"><label>Rate %</label><input type="number" id="dRate" value="7.25" min="0" step="0.05"></div>
@@ -675,7 +679,7 @@ loadSegments(); setInterval(loadSegments, 20000);
 // ── DSCR Qualifier — rents ÷ PITI, no-tax-returns qualification + Frank's referral note ──
 (function(){
   const panel=document.getElementById('dscrPanel'); if(!panel) return;
-  let basis='current', pitiMode='build';
+  let basis='current', pitiMode='build', payType='pi';
   // Monthly P&I from loan/rate/amort — reuse the ranker's amortization (finance.js) so the desk
   // and the property ranker never disagree; fall back to an inline standard mortgage formula if
   // finance.js didn't load for any reason.
@@ -710,32 +714,41 @@ loadSegments(); setInterval(loadSegments, 20000);
   function buildNote(g){
     const addr=(document.getElementById('dAddr').value||'').trim()||'that property';
     const units=unitRows().length, basisWord=basis==='projected'?'projected fair-market':'current';
+    const cap=basisWord[0].toUpperCase()+basisWord.slice(1);
+    const rentsLbl = g.vac>0 ? `${cap} rents at ${g.vac}% vacancy` : `${cap} rents`;
+    const rentsVal = g.vac>0 ? `${usd(g.eff)}/mo effective (${usd(g.gross)} gross)` : `${usd(g.gross)}/mo  (${usd(g.gross*12)}/yr)`;
+    const payLbl = (pitiMode==='build'&&g.payType==='io') ? 'Interest-only payment' : 'Full payment (PITI)';
     let line, close;
     if(g.dscr==null){ line='the payment nets to zero'; close='Send me the rent roll and I’ll pencil it.'; }
     else if(g.dscr>=g.thresh){ line=`a ${g.dscr.toFixed(2)} DSCR — clears the ${g.thresh.toFixed(2)} line with ${usd(g.surplus)}/mo to spare`; close='It qualifies as-is — happy to send terms and get an app started whenever your seller’s ready.'; }
-    else if(g.dscr>=1.0){ line=`a ${g.dscr.toFixed(2)} DSCR — the rents cover the payment but it’s under a typical ${g.thresh.toFixed(2)} approval line`; close=`We can still get there: about ${usd(g.rentsNeeded-g.gross)}/mo more in rent, a bit more down, or a lender at ${g.dscr.toFixed(2)} min gets it done. Want me to run those?`; }
+    else if(g.dscr>=1.0){ line=`a ${g.dscr.toFixed(2)} DSCR — the rents cover the payment but it’s under a typical ${g.thresh.toFixed(2)} approval line`; close=`We can still get there: about ${usd(g.rentsNeeded-g.eff)}/mo more in rent, a bit more down, or a lender at ${g.dscr.toFixed(2)} min gets it done. Want me to run those?`; }
     else { line=`a ${g.dscr.toFixed(2)} DSCR — the rents fall about ${usd(g.shortfall)}/mo short of the payment`; close=`It doesn’t pencil at asking on rents alone — projected fair-market rents or more down would move it. Want me to model it?`; }
     return `Hey — thanks for the list. I called the agent on ${addr} and ran DSCR numbers on it as an alternative to commercial financing on that ${units}-unit.\n\n`+
       `The nice part: it’s a DSCR loan, so no tax returns — it qualifies on the building’s own rents.\n\n`+
-      `  • ${basisWord[0].toUpperCase()+basisWord.slice(1)} rents:  ${usd(g.gross)}/mo  (${usd(g.gross*12)}/yr)\n`+
-      `  • Full payment (PITI):  ${usd(g.piti)}/mo\n`+
+      `  • ${rentsLbl}:  ${rentsVal}\n`+
+      `  • ${payLbl}:  ${usd(g.piti)}/mo\n`+
       `  • DSCR:  ${g.dscr==null?'—':g.dscr.toFixed(2)}  →  ${line}\n\n`+
       `${close}\n\n`+
       `Happy to run a same-day scenario on any of the others on your list.\n— Frank · your commercial-lending desk`;
   }
   function recompute(){
     const gross=grossMonthly();
-    document.getElementById('dRentSum').textContent=usd(gross);
-    document.getElementById('dRentAnn').textContent=usd(gross*12);
-    let piti;
-    if(pitiMode==='build'){ piti = monthlyPI(num('dLoan'),num('dRate'),num('dAmort')||30) + num('dTax') + num('dIns') + num('dHoa'); }
-    else { piti = num('dPiti'); }
+    const vac=Math.max(0,Math.min(100,num('dVac')));   // vacancy %; 0 = fully occupied
+    const eff=gross*(1-vac/100);                        // effective rents used for DSCR
+    document.getElementById('dRentSum').textContent=usd(gross)+(vac>0?` → ${usd(eff)} @ ${vac}% vac`:'');
+    document.getElementById('dRentAnn').textContent=usd((vac>0?eff:gross)*12);
+    let piti, pi=null;
+    if(pitiMode==='build'){
+      // interest-only = loan × monthly rate; else amortizing P&I (CREFin, single source of truth)
+      pi = payType==='io' ? num('dLoan')*(num('dRate')/100/12) : monthlyPI(num('dLoan'),num('dRate'),num('dAmort')||30);
+      piti = pi + num('dTax') + num('dIns') + num('dHoa');
+    } else { piti = num('dPiti'); }
     const thresh=num('dThresh')||1.20;
-    const dscr = piti>0 ? gross/piti : null;
-    const g={ gross, piti, thresh, dscr,
-      surplus: gross-piti, shortfall: piti-gross,
-      maxPiti: thresh>0 ? gross/thresh : 0,        // most PITI these rents support at the approval line
-      rentsNeeded: piti*thresh };                   // rents needed to hit the approval line
+    const dscr = piti>0 ? eff/piti : null;             // DSCR on EFFECTIVE rents
+    const g={ gross, eff, vac, payType, piti, thresh, dscr,
+      surplus: eff-piti, shortfall: piti-eff,
+      maxPiti: thresh>0 ? eff/thresh : 0,               // most PITI these rents support at the approval line
+      rentsNeeded: piti*thresh };                       // effective rents needed to hit the approval line
     // headline
     document.getElementById('dRatio').textContent = dscr==null?'—':dscr.toFixed(2);
     const v=verdict(dscr,thresh), badge=document.getElementById('dBadge');
@@ -745,10 +758,11 @@ loadSegments(); setInterval(loadSegments, 20000);
     const gap = dscr==null?'':(dscr>=thresh
       ? `<div class="r"><span>Monthly surplus over payment</span><b style="color:var(--active)">${usd(g.surplus)}</b></div>`
       : `<div class="r"><span>Rents needed to hit ${thresh.toFixed(2)}</span><b>${usd(g.rentsNeeded)}/mo</b></div>`+
-        `<div class="r"><span>${gross>=piti?'Cushion above payment':'Monthly shortfall'}</span><b style="color:${gross>=piti?'var(--active)':'var(--del)'}">${usd(Math.abs(g.surplus))}</b></div>`);
+        `<div class="r"><span>${eff>=piti?'Cushion above payment':'Monthly shortfall'}</span><b style="color:${eff>=piti?'var(--active)':'var(--del)'}">${usd(Math.abs(g.surplus))}</b></div>`);
     document.getElementById('dBrk').innerHTML =
       `<div class="r"><span>Gross monthly rents</span><b>${usd(gross)}</b></div>`+
-      `<div class="r"><span>PITI (payment)</span><b>${usd(piti)}</b></div>`+
+      (vac>0?`<div class="r"><span>Effective rents (${vac}% vacancy)</span><b>${usd(eff)}</b></div>`:'')+
+      `<div class="r"><span>PITI (${pitiMode==='build'&&payType==='io'?'interest-only':'payment'})</span><b>${usd(piti)}</b></div>`+
       `<div class="r hi"><span>Max payment these rents support @ ${thresh.toFixed(2)}</span><b>${usd(g.maxPiti)}/mo</b></div>`+
       gap;
     // Frank's note
@@ -758,7 +772,8 @@ loadSegments(); setInterval(loadSegments, 20000);
   // ── wire toggles ──
   document.querySelectorAll('#dRentBasis button').forEach(b=>b.onclick=()=>{ basis=b.dataset.v; b.parentNode.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b)); recompute(); });
   document.querySelectorAll('#dPitiMode button').forEach(b=>b.onclick=()=>{ pitiMode=b.dataset.v; b.parentNode.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b)); document.getElementById('dBuild').style.display=pitiMode==='build'?'':'none'; document.getElementById('dDirect').style.display=pitiMode==='direct'?'':'none'; recompute(); });
-  ['dLoan','dRate','dAmort','dTax','dIns','dHoa','dPiti','dThresh','dAddr'].forEach(id=>{ const el=document.getElementById(id); if(el) el.addEventListener('input',recompute); });
+  document.querySelectorAll('#dPayType button').forEach(b=>b.onclick=()=>{ payType=b.dataset.v; b.parentNode.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x===b)); recompute(); });
+  ['dLoan','dRate','dAmort','dTax','dIns','dHoa','dPiti','dThresh','dAddr','dVac'].forEach(id=>{ const el=document.getElementById(id); if(el) el.addEventListener('input',recompute); });
   document.getElementById('dAddUnit').onclick=()=>{ addUnit(1800); recompute(); };
   document.getElementById('dCopy').onclick=()=>{ const t=document.getElementById('dNote').textContent; navigator.clipboard&&navigator.clipboard.writeText(t); const b=document.getElementById('dCopy'); b.textContent='✓ Copied'; setTimeout(()=>b.textContent='📋 Copy note',1200); };
   document.getElementById('dQueue').onclick=()=>{

← 03cae43 CRCP: /api/property/:ain + /api/property/by-address enrichme  ·  back to Commercialrealestate  ·  enrich endpoints: Cody Cycle-2 hardening (timeout + address 2d61ca7 →