venthrive-api / sql /news_schema.sql
GitHub Actions
Sync from GitHub · 2026-10-20 19:36 UTC
a32af27
Raw History Blame Contribute Delete
6.53 kB
-- VenThrive business-news system — run this once against your Supabase project's
-- SQL editor (or `psql`). See docs/blueprint/adr/0015-business-news-system-design.md
-- for the reasoning behind every choice below.
create extension if not exists pgcrypto;
-- ─── Sources ────────────────────────────────────────────────────────────────
-- Sources are data, not code, so an admin can add/disable one without a deploy.
-- 'rss' sources are polled with feedparser; 'html' sources with a generic
-- title + first-paragraph extractor (see backend/news/scrape.py).
create table if not exists news_sources (
id uuid primary key default gen_random_uuid(),
name text not null,
url text not null unique,
source_type text not null check (source_type in ('rss', 'html')),
-- Which of the three sourcing strategies this feeds (ADR-0015) — purely
-- informational, doesn't change scrape behavior.
source_group text not null check (source_group in ('national_news', 'agency_bulletin', 'funder_program')),
active boolean not null default true,
created_at timestamptz not null default now()
);
-- A handful of real, well-known starter sources per ADR-0015's three source
-- groups. Add more through this table directly — no code change needed.
insert into news_sources (name, url, source_type, source_group) values
('BusinessDay', 'https://businessday.ng/feed/', 'rss', 'national_news'),
('Nairametrics', 'https://nairametrics.com/feed/', 'rss', 'national_news'),
('Premium Times — Business', 'https://www.premiumtimesng.com/category/business/feed', 'rss', 'national_news'),
('NiMet — Weather Alerts', 'https://www.nimet.gov.ng', 'html', 'agency_bulletin'),
('NEMA — Advisories', 'https://nema.gov.ng', 'html', 'agency_bulletin'),
('SMEDAN', 'https://smedan.gov.ng', 'html', 'funder_program'),
('Bank of Industry — News', 'https://www.boi.ng/category/news/', 'html', 'funder_program'),
('Tony Elumelu Foundation', 'https://www.tonyelumelufoundation.org/news', 'html', 'funder_program')
on conflict (url) do nothing;
-- ─── Items ──────────────────────────────────────────────────────────────────
create table if not exists news_items (
id uuid primary key default gen_random_uuid(),
source_id uuid references news_sources(id) on delete set null,
url text not null,
url_hash text not null unique,
title text not null,
raw_text text not null default '',
published_at timestamptz,
scraped_at timestamptz not null default now(),
status text not null default 'pending'
check (status in ('pending', 'classified', 'needs_review', 'rejected')),
-- Classification output (ADR-0015) — all null until classified. No 'threat' value:
-- reporting something bad that already happened has no value once it can't be acted
-- on; a predictive risk system is a separate, forward-looking feature.
impact text check (impact in ('opportunity', 'neutral')),
category text check (category in ('policy', 'weather', 'infrastructure', 'funding', 'security', 'other')),
business_types text[], -- subsector ids from data/sectors.ts, see backend/news/subsectors.json
location text,
deadline date, -- only set for an opportunity with an explicit deadline in the source text
expires_at timestamptz, -- deadline if set, else scraped_at + the category's default window
confidence numeric,
review_reason text, -- why this needs_review, for the admin queue
classified_at timestamptz,
-- The completeness bar from ADR-0015, enforced here, not just in application
-- code: a classified item must have a category, at least one business type,
-- and a location.
constraint classified_items_are_complete check (
status <> 'classified'
or (category is not null and location is not null
and business_types is not null and array_length(business_types, 1) > 0)
)
);
create index if not exists news_items_status_idx on news_items (status);
create index if not exists news_items_expires_at_idx on news_items (expires_at);
create index if not exists news_items_business_types_idx on news_items using gin (business_types);
-- ─── Poll run log ───────────────────────────────────────────────────────────
-- One row per admin-triggered poll (ADR-0015: admin-triggered only, daily
-- auto-poll deferred) — an audit trail and a place to surface run-level errors.
create table if not exists news_polls (
id uuid primary key default gen_random_uuid(),
triggered_by text not null, -- admin's Firebase UID
since_days int not null check (since_days between 1 and 30),
started_at timestamptz not null default now(),
finished_at timestamptz,
items_scraped int not null default 0,
items_deduped_out int not null default 0,
items_classified int not null default 0,
items_flagged int not null default 0,
items_rejected int not null default 0, -- auto-discarded: incomplete and the model wasn't confident either
current_source text, -- name of the source being scraped right now, for a live run console
error text
);
alter table news_polls add column if not exists items_rejected int not null default 0;
alter table news_polls add column if not exists current_source text;
-- 'threat' removed from impact (ADR-0015 addendum): a reactive "this already happened"
-- label has no value to a business owner who can't act on it anymore. Existing rows
-- are reclassified neutral; a predictive risk/forecast system is planned separately.
update news_items set impact = 'neutral' where impact = 'threat';
alter table news_items drop constraint if exists news_items_impact_check;
alter table news_items add constraint news_items_impact_check check (impact in ('opportunity', 'neutral'));