-- ============================================================ -- DRE Customer Portal — SQLite Schema (spec §1) -- DB file: /opt/dre-portal/data/dre.db -- Pragmas (set on every connection): foreign_keys=ON, journal_mode=WAL, busy_timeout=5000 -- All timestamps ISO-8601 UTC TEXT. Money as INTEGER cents. PKs TEXT UUID4. -- ============================================================ CREATE TABLE IF NOT EXISTS clients ( id TEXT PRIMARY KEY, -- uuid4 client_number TEXT UNIQUE NOT NULL, -- CLT-YYYY-NNNN company_name TEXT NOT NULL, contact_name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, -- lowercased; magic-link identity phone TEXT, tos_accepted_at TEXT, -- set when ToS accepted at intake twentycrm_id TEXT, -- nullable; set by future CRM sync created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_clients_email ON clients(email); CREATE TABLE IF NOT EXISTS debtors ( id TEXT PRIMARY KEY, -- uuid4 name TEXT NOT NULL, -- business or individual name business_type TEXT NOT NULL DEFAULT 'OTHER' CHECK (business_type IN ('INDIVIDUAL','SOLE_PROPRIETORSHIP','LLC','CORPORATION','PARTNERSHIP','OTHER')), contact_email TEXT, contact_phone TEXT, physical_address TEXT, -- free-text single line twentycrm_id TEXT, -- nullable created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS claims ( id TEXT PRIMARY KEY, -- uuid4 claim_number TEXT UNIQUE NOT NULL, -- DRE-YYYY-NNNN client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE RESTRICT, debtor_id TEXT NOT NULL REFERENCES debtors(id) ON DELETE RESTRICT, amount_cents INTEGER NOT NULL CHECK (amount_cents > 0), currency TEXT NOT NULL DEFAULT 'USD', status TEXT NOT NULL DEFAULT 'NEW' CHECK (status IN ('NEW','UNDER_REVIEW','ACTIVE','NEGOTIATION','LEGAL','SETTLED','CLOSED','WRITE_OFF','REJECTED')), tier TEXT NOT NULL DEFAULT 'TIER_1' CHECK (tier IN ('TIER_1','TIER_2','TIER_2_5','TIER_3','TIER_4')), description TEXT, client_reference TEXT, invoice_date TEXT, -- ISO date date_assigned TEXT, -- set when moved out of NEW date_resolved TEXT, -- set on SETTLED/CLOSED/WRITE_OFF twentycrm_id TEXT, -- nullable created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_claims_client ON claims(client_id); CREATE INDEX IF NOT EXISTS idx_claims_status ON claims(status); CREATE INDEX IF NOT EXISTS idx_claims_debtor ON claims(debtor_id); CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, -- uuid4 claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE, original_name TEXT NOT NULL, -- sanitized display name stored_path TEXT NOT NULL, -- absolute path on disk (uuid-named) mime_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, sha256 TEXT NOT NULL, -- integrity + dedupe uploaded_by TEXT NOT NULL DEFAULT 'CLIENT' -- CLIENT | STAFF CHECK (uploaded_by IN ('CLIENT','STAFF')), twentycrm_id TEXT, -- nullable created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_documents_claim ON documents(claim_id); CREATE TABLE IF NOT EXISTS case_notes ( id TEXT PRIMARY KEY, -- uuid4 claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE, author_type TEXT NOT NULL CHECK (author_type IN ('CLIENT','STAFF','SYSTEM')), author_name TEXT NOT NULL, subject TEXT, -- for client->team structured messages content TEXT NOT NULL, -- plaintext; rendered escaped visibility TEXT NOT NULL DEFAULT 'SHARED' CHECK (visibility IN ('SHARED','INTERNAL')), twentycrm_id TEXT, -- nullable created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_notes_claim ON case_notes(claim_id); CREATE TABLE IF NOT EXISTS auth_tokens ( id TEXT PRIMARY KEY, -- uuid4 client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE, token_hash TEXT UNIQUE NOT NULL, -- sha256 of raw token expires_at TEXT NOT NULL, -- created_at + 15 min consumed_at TEXT, -- NULL = unused requested_ip TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_tokens_hash ON auth_tokens(token_hash); CREATE INDEX IF NOT EXISTS idx_tokens_client ON auth_tokens(client_id); CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, -- uuid4 client_id TEXT NOT NULL REFERENCES clients(id) ON DELETE CASCADE, session_hash TEXT UNIQUE NOT NULL, -- sha256 of raw session token expires_at TEXT NOT NULL, -- + 7 days revoked_at TEXT, created_at TEXT NOT NULL, last_seen_at TEXT ); CREATE INDEX IF NOT EXISTS idx_sessions_hash ON sessions(session_hash); CREATE TABLE IF NOT EXISTS audit_log ( id TEXT PRIMARY KEY, -- uuid4 entity_type TEXT NOT NULL, -- 'claim'|'client'|'document'|'note' entity_id TEXT NOT NULL, action TEXT NOT NULL, -- 'create'|'status_change'|'update'|'upload'|'note_add' field TEXT, old_value TEXT, new_value TEXT, actor TEXT NOT NULL, reason TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_audit_entity ON audit_log(entity_type, entity_id); CREATE TABLE IF NOT EXISTS number_sequences ( prefix TEXT NOT NULL, -- 'DRE'|'CLT' year INTEGER NOT NULL, last_value INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (prefix, year) ); -- Migration tracking table CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, applied_at TEXT NOT NULL );