← 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> · 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&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 →