Files
MarketingTool/apps/api/schema.sql
T

257 lines
18 KiB
SQL

PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS organizations (
id TEXT PRIMARY KEY, name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_salt TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('viewer','owner','admin','researcher')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_org ON users(organization_id);
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token_hash);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT REFERENCES organizations(id), user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL, details TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_audit_org ON audit_log(organization_id,created_at);
CREATE TABLE IF NOT EXISTS businesses (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL, name TEXT NOT NULL, website TEXT NOT NULL DEFAULT '', website_domain TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', score INTEGER NOT NULL DEFAULT 0,
score_version TEXT NOT NULL DEFAULT 'mvp-1', score_factors TEXT NOT NULL DEFAULT '[]', website_class TEXT NOT NULL DEFAULT 'missing',
verified INTEGER NOT NULL DEFAULT 0, verified_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Phase 6 normalized location and reversible merge ledger.
CREATE TABLE IF NOT EXISTS merge_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id),
source_business_id INTEGER NOT NULL REFERENCES businesses(id), target_business_id INTEGER NOT NULL REFERENCES businesses(id),
source_snapshot_json TEXT NOT NULL, child_reassignment_json TEXT NOT NULL DEFAULT '{}', actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
reversible INTEGER NOT NULL DEFAULT 1, reversed_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_merge_history_org ON merge_history(organization_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_businesses_org ON businesses(organization_id);
CREATE INDEX IF NOT EXISTS idx_businesses_score ON businesses(organization_id,score DESC,id);
CREATE INDEX IF NOT EXISTS idx_businesses_class ON businesses(organization_id,website_class);
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_domain ON businesses(organization_id, website_domain) WHERE website_domain <> '';
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_email ON businesses(organization_id, email) WHERE email <> '';
CREATE UNIQUE INDEX IF NOT EXISTS uq_business_phone ON businesses(organization_id, phone) WHERE phone <> '';
CREATE TABLE IF NOT EXISTS suppressions (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('email','domain','phone')),
value TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id, kind, value)
);
CREATE TABLE IF NOT EXISTS business_identifiers (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, value TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '', phone TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL DEFAULT '', do_not_contact INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS domains (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), domain TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'other', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS websites (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), url TEXT NOT NULL, website_class TEXT NOT NULL DEFAULT 'business_site', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS evidence (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, url TEXT NOT NULL DEFAULT '', claim TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS pipeline_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), stage TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS interactions (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), kind TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT, business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), body TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_contacts_business ON contacts(business_id);
CREATE INDEX IF NOT EXISTS idx_contacts_org_business ON contacts(organization_id,business_id);
CREATE INDEX IF NOT EXISTS idx_identifiers_business ON business_identifiers(business_id,kind);
CREATE INDEX IF NOT EXISTS idx_domains_business ON domains(business_id);
CREATE INDEX IF NOT EXISTS idx_websites_business ON websites(business_id);
CREATE INDEX IF NOT EXISTS idx_evidence_business ON evidence(business_id);
CREATE INDEX IF NOT EXISTS idx_pipeline_business ON pipeline_entries(business_id,created_at);
CREATE INDEX IF NOT EXISTS idx_pipeline_stage ON pipeline_entries(organization_id,stage);
CREATE INDEX IF NOT EXISTS idx_notes_business ON notes(business_id,created_at);
CREATE INDEX IF NOT EXISTS idx_interactions_business ON interactions(business_id,created_at);
-- Phase 12 CRM workflow metadata and safe, auditable state.
CREATE TABLE IF NOT EXISTS pipeline_stages (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL, position INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id,name)
);
CREATE INDEX IF NOT EXISTS idx_pipeline_stages_org ON pipeline_stages(organization_id,position,id);
CREATE INDEX IF NOT EXISTS idx_interactions_org_created ON interactions(organization_id,created_at DESC,id DESC);
-- Phase 4 durable background jobs (additive-safe for existing databases).
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
organization_id TEXT NOT NULL REFERENCES organizations(id),
idempotency_key TEXT NOT NULL,
type TEXT NOT NULL,
payload TEXT NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'queued' CHECK(status IN ('queued','running','succeeded','failed','cancelled')),
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 3,
progress INTEGER NOT NULL DEFAULT 0 CHECK(progress BETWEEN 0 AND 100),
error_code TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
started_at TEXT,
completed_at TEXT,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id,idempotency_key)
);
CREATE INDEX IF NOT EXISTS idx_jobs_org_created ON jobs(organization_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_jobs_queue ON jobs(status,created_at,id);
CREATE TABLE IF NOT EXISTS job_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id),
sequence INTEGER NOT NULL,
event_type TEXT NOT NULL,
message TEXT NOT NULL DEFAULT '',
progress INTEGER NOT NULL DEFAULT 0 CHECK(progress BETWEEN 0 AND 100),
error_code TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(job_id,sequence)
);
CREATE INDEX IF NOT EXISTS idx_job_events_job_sequence ON job_events(job_id,sequence);
-- Phase 5 source framework (additive-safe; credentials contain metadata only).
CREATE TABLE IF NOT EXISTS sources (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id),
name TEXT NOT NULL, kind TEXT NOT NULL CHECK(kind IN ('csv','manual')), enabled INTEGER NOT NULL DEFAULT 0,
config_json TEXT NOT NULL DEFAULT '{}', health_status TEXT NOT NULL DEFAULT 'unknown',
consecutive_failures INTEGER NOT NULL DEFAULT 0, circuit_open INTEGER NOT NULL DEFAULT 0,
last_success_at TEXT, last_failure_at TEXT, last_error TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id,name)
);
CREATE INDEX IF NOT EXISTS idx_sources_org ON sources(organization_id,id);
CREATE TABLE IF NOT EXISTS source_credentials (
id INTEGER PRIMARY KEY AUTOINCREMENT, source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
organization_id TEXT NOT NULL REFERENCES organizations(id), provider TEXT NOT NULL, key_name TEXT NOT NULL,
secret_ref TEXT NOT NULL DEFAULT '', metadata_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(source_id,provider,key_name)
);
CREATE TABLE IF NOT EXISTS discovery_queries (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
name TEXT NOT NULL, query_json TEXT NOT NULL DEFAULT '{}', enabled INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id,name)
);
CREATE INDEX IF NOT EXISTS idx_discovery_queries_org ON discovery_queries(organization_id,id);
CREATE TABLE IF NOT EXISTS source_records (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), source_id INTEGER NOT NULL REFERENCES sources(id) ON DELETE CASCADE,
discovery_query_id INTEGER REFERENCES discovery_queries(id) ON DELETE SET NULL, content_hash TEXT NOT NULL, raw_json TEXT NOT NULL,
normalized_json TEXT NOT NULL, source_url TEXT NOT NULL DEFAULT '', query_context_json TEXT NOT NULL DEFAULT '{}', processing_status TEXT NOT NULL DEFAULT 'raw',
cursor_json TEXT NOT NULL DEFAULT '{}', rate_policy_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id,source_id,content_hash)
);
CREATE INDEX IF NOT EXISTS idx_source_records_org ON source_records(organization_id,id DESC);
-- Phase 7 domain intelligence (additive-safe; DNS results are explicitly stateful).
CREATE TABLE IF NOT EXISTS domain_checks (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id),
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, domain TEXT NOT NULL,
status TEXT NOT NULL, result_json TEXT NOT NULL DEFAULT '{}', cache_key TEXT NOT NULL,
checked_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, cache_expires_at TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id, business_id, cache_key)
);
CREATE INDEX IF NOT EXISTS idx_domain_checks_org ON domain_checks(organization_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_domain_checks_business ON domain_checks(organization_id,business_id,created_at DESC);
CREATE TABLE IF NOT EXISTS domain_candidates (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id), business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
domain TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'generated', rank INTEGER NOT NULL DEFAULT 0, metadata_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(organization_id,business_id,domain)
);
CREATE INDEX IF NOT EXISTS idx_domain_candidates_business ON domain_candidates(organization_id,business_id,rank,id);
-- Phase 8 passive website analysis history; each scan is retained.
CREATE TABLE IF NOT EXISTS website_scans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
organization_id TEXT NOT NULL REFERENCES organizations(id),
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
website_id INTEGER REFERENCES websites(id) ON DELETE SET NULL,
input_url TEXT NOT NULL,
classification TEXT NOT NULL,
result_json TEXT NOT NULL DEFAULT '{}',
cache_key TEXT NOT NULL,
scanned_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
cache_expires_at TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_website_scans_org ON website_scans(organization_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_website_scans_business ON website_scans(organization_id,business_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_website_scans_cache ON website_scans(organization_id,cache_key,cache_expires_at);
-- Phase 9 contact extraction provenance. MX is intentionally a state value, not a probe.
CREATE TABLE IF NOT EXISTS contact_extractions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
organization_id TEXT NOT NULL REFERENCES organizations(id),
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE,
website_scan_id INTEGER REFERENCES website_scans(id) ON DELETE SET NULL,
extraction_key TEXT NOT NULL,
kind TEXT NOT NULL,
value TEXT NOT NULL,
label TEXT NOT NULL DEFAULT '',
classification TEXT NOT NULL DEFAULT 'unknown',
confidence REAL NOT NULL DEFAULT 0,
source_url TEXT NOT NULL,
public_business INTEGER NOT NULL DEFAULT 1,
mx_status TEXT NOT NULL DEFAULT 'unknown',
suppressed INTEGER NOT NULL DEFAULT 0,
do_not_contact INTEGER NOT NULL DEFAULT 0,
provenance TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id,business_id,extraction_key,kind,value)
);
CREATE INDEX IF NOT EXISTS idx_contact_extractions_org ON contact_extractions(organization_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_contact_extractions_business ON contact_extractions(organization_id,business_id,id DESC);
-- Phase 10 deterministic qualification rules and immutable score audit.
CREATE TABLE IF NOT EXISTS score_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
code TEXT NOT NULL, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', condition_json TEXT NOT NULL DEFAULT '{}',
points INTEGER NOT NULL DEFAULT 0, max_applications INTEGER NOT NULL DEFAULT 1, enabled INTEGER NOT NULL DEFAULT 1,
version INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id,code)
);
CREATE INDEX IF NOT EXISTS idx_score_rules_org ON score_rules(organization_id,enabled,code);
CREATE TABLE IF NOT EXISTS score_history (
id INTEGER PRIMARY KEY AUTOINCREMENT, organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
business_id INTEGER NOT NULL REFERENCES businesses(id) ON DELETE CASCADE, score INTEGER NOT NULL, eligible INTEGER NOT NULL,
priority_band TEXT NOT NULL, score_version TEXT NOT NULL, explanations_json TEXT NOT NULL DEFAULT '[]', signals_json TEXT NOT NULL DEFAULT '{}',
override_score INTEGER, override_eligible INTEGER, override_reason TEXT, actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_score_history_business ON score_history(organization_id,business_id,created_at DESC,id DESC);
CREATE INDEX IF NOT EXISTS idx_score_history_org ON score_history(organization_id,created_at DESC,id DESC);
-- Phase 11 operator workflow: saved views and review state.
CREATE TABLE IF NOT EXISTS saved_filters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
organization_id TEXT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
filters_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(organization_id,user_id,name)
);
CREATE INDEX IF NOT EXISTS idx_saved_filters_org_user ON saved_filters(organization_id,user_id,updated_at DESC,id DESC);