Files
dre/backend/schema.sql
T
root be0750d001 DRE customer portal: FastAPI + SQLite backend (18 endpoints)
- Magic-link auth (sha256-only token storage, 15-min single-use, 7-day sessions)
- Staff-key auth via X-DRE-Staff-Key (constant-time compare)
- SQLite WAL, foreign_keys, parameterized queries, atomic DRE/CLT sequence allocation
- Intake validator rejects SSN/PAN patterns (FDCPA/TDCPA compliance)
- Document upload allowlist + magic-byte check, 20MB cap
- Unified error envelope, money as integer cents
- systemd unit (port 8093, User=root, hardening directives)
- Fixes import bug (auth.py relative imports) and audit_log placeholder mismatch
2026-08-21 18:43:33 -04:00

140 lines
6.7 KiB
SQL

-- ============================================================
-- 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
);