← back to Re Flyer Aggregator

db/001_external_marketing_asset.sql

59 lines

-- TK-10708  external_marketing_asset + asset_subject_link
-- DRAFT staging schema for the re-flyer-aggregator. NOT applied to prod.
-- (codex-revised) Assets are PROPERTY/LISTING-scoped and may link to 0..n deals,
-- so an asset table + a subject-link table replaces the too-narrow deal_attachment.
-- Only VALIDATED rows are later PROMOTED into usre as the canonical cross-build
-- dataset; CRCP/RENTV consume a view/API, never these scraper/staging tables.
--
-- Link-only posture: source_url + document_url + provenance, NO content_hash and
-- NO local_path unless a Steve-GATED tier-3 download actually happened.

CREATE TABLE IF NOT EXISTS external_marketing_asset (
  id             bigserial PRIMARY KEY,
  asset_type     text NOT NULL
     CHECK (asset_type IN ('offering_memorandum','marketing_flyer','property_spotlight','property_brief','market_report','deal_recap')),
  tier           smallint NOT NULL CHECK (tier IN (1,2,3)),   -- 1 first-party/broker-landing, 2 self-generated recap, 3 marketplace/rehost (GATED)
  title          text,
  description     text,

  -- WHERE it was found (link-only friendly; no download implied)
  source_name    text,                    -- 'CBRE', 'RENTV', 'self-generated', ...
  source_landing_url text,                -- the human-viewable listing/landing page
  document_url   text,                    -- the OM/flyer document URL if the page exposes one
  discovery_method text,                  -- 'broker_domain_join' | 'press_room' | 'sitemap' | 'manual' | 'generated'

  -- RIGHTS / COMPLIANCE (codex): store the basis, don't assume 'public = ok'
  rights_basis   text,                    -- 'first_party' | 'public_record' | 'self_generated' | 'GATED_needs_review'
  access_status  text,                    -- 'public' | 'robots_disallowed' | 'login_required' | 'gated'
  robots_ok      boolean,                 -- did robots.txt permit automated discovery of this path?

  -- Only populated for TIER 2 generated or a GATED tier-3 rehost
  local_path     text,
  file_format    text CHECK (file_format IN ('pdf','html','markdown')),

  source_firm_id bigint,                  -- optional: resolved broker/firm
  last_verified_at timestamptz,           -- link freshness (replaces content_hash for link-only)
  created_at     timestamptz NOT NULL DEFAULT now()
);

-- An asset can be the subject of a PROPERTY (parcel) and/or 0..n DEALS.
CREATE TABLE IF NOT EXISTS asset_subject_link (
  id            bigserial PRIMARY KEY,
  asset_id      bigint NOT NULL REFERENCES external_marketing_asset(id) ON DELETE CASCADE,
  subject_type  text NOT NULL CHECK (subject_type IN ('parcel','deal')),
  -- parcel subject: (county_fips, ain) ; deal subject: (county_fips, doc_number). Deal key nullable.
  county_fips   text NOT NULL,
  ain           text,
  doc_number    text,
  match_confidence numeric,               -- 0..1 from the provenance probe
  created_at    timestamptz NOT NULL DEFAULT now(),
  CHECK (subject_type <> 'parcel' OR ain IS NOT NULL),
  CHECK (subject_type <> 'deal'   OR doc_number IS NOT NULL)
);

CREATE INDEX IF NOT EXISTS idx_ema_type ON external_marketing_asset(asset_type);
CREATE INDEX IF NOT EXISTS idx_ema_tier ON external_marketing_asset(tier);
CREATE INDEX IF NOT EXISTS idx_asl_asset ON asset_subject_link(asset_id);
CREATE INDEX IF NOT EXISTS idx_asl_deal ON asset_subject_link(county_fips, doc_number);
CREATE INDEX IF NOT EXISTS idx_asl_parcel ON asset_subject_link(county_fips, ain);