← back to Commercialrealestate
scripts/db/assumable-schema.sql
58 lines
-- assumable-schema.sql — the assumable-loan ESTIMATE layer for the CRE explorer (local Postgres "cre").
--
-- WHAT THIS IS: FHA & VA mortgages are assumable — a qualified buyer can take over the seller's existing
-- low-rate loan instead of originating a new one at today's rate. That is a live, notable signal:
-- a listing that likely carries a sub-4% assumable FHA/VA note is worth flagging for buyers.
-- The AUTHORITATIVE source for "does this property carry an assumable FHA/VA loan" is the
-- recorded deed of trust in TITLE RECORDS (lender + loan type + recording date). We do NOT have title
-- access yet (see scripts/sources/title-records.js — gated adapter).
--
-- HONEST LABELING (HARD RULE — matches the condo warrantability rule): every row here is a HEURISTIC
-- ESTIMATE, never a title-verified fact. assumable_likelihood is a screening signal only. confidence is
-- ALWAYS 'heuristic — verify in title records' until scripts/sources/title-records.js returns real data,
-- at which point the basis flips to 'title' and confidence becomes 'title-verified'. The heuristic can
-- misfire (a 2021 purchase may have been all-cash or conventional, not FHA/VA) — RECORDED DISSENT, by
-- design: this is a call-prioritization screen, NOT an authoritative assumption finding.
-- Recording-date -> FHA/VA 30yr rate PROXY. Approximate annual-average 30yr fixed rates by program-year,
-- used to infer the rate a loan recorded in year Y likely carries. Rates are APPROXIMATE historical
-- averages (Freddie Mac PMMS-grade ballpark for FHA/VA 30yr) and are LABELLED approximate everywhere.
CREATE TABLE IF NOT EXISTS assumable_rate_proxy (
rate_year int PRIMARY KEY,
est_rate numeric NOT NULL, -- approximate avg FHA/VA 30yr fixed rate for loans recorded that year
note text -- provenance / caveat
);
-- Per-property assumable estimate. One row per condo/sfr id. Recomputed (idempotent UPSERT) by
-- scripts/assumable-heuristic.js. src distinguishes which for-sale table the id lives in.
DO $$ BEGIN
CREATE TYPE assumable_likelihood AS ENUM ('low', 'medium', 'high');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
CREATE TABLE IF NOT EXISTS assumable_estimate (
property_id text PRIMARY KEY, -- condo.id or sfr.id
src text NOT NULL, -- 'condo' | 'sfr'
assumable_likelihood assumable_likelihood NOT NULL,
est_assumable_rate numeric, -- proxy rate inferred from last-sale era (may be NULL)
est_sale_year int, -- the purchase era used for the rate proxy, when matched
basis text NOT NULL, -- human-readable why ("condo $850k <FHA limit; last sale 2021 -> ~3.1%")
confidence text NOT NULL DEFAULT 'heuristic — verify in title records',
signals jsonb, -- full factor breakdown for transparency on the card
computed_at timestamptz DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_assum_src ON assumable_estimate(src);
CREATE INDEX IF NOT EXISTS idx_assum_likelihood ON assumable_estimate(assumable_likelihood);
-- Card view: assumable estimate joined back to its for-sale property (condo + sfr union), ready for
-- /api/assumable. Carries the address/price/city so the front end can render a badge with no extra join.
CREATE OR REPLACE VIEW assumable_card AS
SELECT a.property_id, a.src, a.assumable_likelihood, a.est_assumable_rate, a.est_sale_year,
a.basis, a.confidence, a.signals, a.computed_at,
p.address, p.city, p.zip, p.price, p.year_built
FROM assumable_estimate a
JOIN (
SELECT id, address, city, zip, price, year_built, 'condo'::text src FROM condo
UNION ALL
SELECT id, address, city, zip, price, year_built, 'sfr'::text src FROM sfr
) p ON p.id = a.property_id AND p.src = a.src;