Files
root 7a62b0b340 Backend Aug 22-25: AI analysis, welcome-packet templating, LetterStream, DocuSeal, staff RBAC + tier gate
- analysis.py: deterministic claim scorer + /analyze /approve /letter /advance-tier endpoints (auto-runs on intake)
- packet.py + packet_fields.json: welcome-packet templating engine (6 onboarding docs, field catalog)
- letterstream.py + letters.py: certified-mail send pipeline + letter lifecycle (webhook verified)
- docuseal.py: DocuSeal signing integration
- staff.py/models.py/schema.sql/auth.py: approval actor from staff key, tier gate (APPROVED+ACTIVE+onboarding docs), onboarding_docs table
- frontend/: dependency-free static portal (intake, magic-link login/verify, dashboard)
- landing-mockups/: 4 design-stance mockups + favicons
- legal/: aup/privacy/sms-terms/terms HTML
- docs/: letter-queue scope, letterstream API contract, 6 welcome-packet templates
- review-dre-landing-2026-08-21.md: 3-variant landing feedback sprint
- compliance/DRE_Compliance_Manual.md: updated

Source synced from deployed /opt/dre-portal/app/ (was 4 days ahead of git).
2026-08-26 02:26:33 -04:00

235 lines
12 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
-- Client signed representations at intake (added via db._migrate on existing DBs)
no_other_agency_at TEXT, -- confirmed no other agency/attorney engaged
no_prior_action_at TEXT, -- confirmed no prior litigation/judgment/bankruptcy
-- AI analysis + approval (added via db._migrate on existing DBs)
analysis_score INTEGER, -- 0-100, NULL until analyzed
analysis_summary TEXT, -- plain-English narrative
analysis_components TEXT, -- JSON breakdown of component scores
analysis_at TEXT, -- ISO timestamp of analysis
recommended_tier TEXT, -- AI-recommended starting tier
approval_status TEXT NOT NULL DEFAULT 'NONE'
CHECK (approval_status IN ('NONE','PENDING','APPROVED','REJECTED')),
approval_decision_by TEXT, -- staff name who approved/rejected
approval_decision_at TEXT,
-- AI-recommended letter (staff-customizable)
letter_subject TEXT,
letter_body TEXT,
letter_tier TEXT, -- tier the letter was generated for
letter_updated_at TEXT,
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
);
-- Welcome-packet field overrides: client's final value per profile field,
-- merged over pre-populated submission values at render time.
CREATE TABLE IF NOT EXISTS packet_field_overrides (
id TEXT PRIMARY KEY,
claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
profile_key TEXT NOT NULL,
value TEXT,
updated_by TEXT NOT NULL DEFAULT 'CLIENT' CHECK (updated_by IN ('CLIENT','STAFF')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(claim_id, profile_key)
);
CREATE INDEX IF NOT EXISTS idx_packet_overrides_claim ON packet_field_overrides(claim_id);
-- Onboarding paperwork receipt tracking. One row per welcome-packet document
-- per claim; staff mark each doc "received" once the client returns it.
CREATE TABLE IF NOT EXISTS onboarding_docs (
id TEXT PRIMARY KEY,
claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
doc_key TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING','RECEIVED')),
received_by TEXT,
received_at TEXT,
created_at TEXT NOT NULL,
UNIQUE(claim_id, doc_key)
);
CREATE INDEX IF NOT EXISTS idx_onboarding_claim ON onboarding_docs(claim_id);
-- Physical mail queue (LetterStream). Lifecycle: DRAFT -> APPROVED -> PREAUTH -> SENT
-- (+ REJECTED / CANCELLED / ERROR). One row per mailed letter.
CREATE TABLE IF NOT EXISTS letters (
id TEXT PRIMARY KEY, -- uuid4
claim_id TEXT NOT NULL REFERENCES claims(id) ON DELETE CASCADE,
letter_type TEXT NOT NULL DEFAULT 'demand',
subject TEXT,
body TEXT, -- rendered letter body snapshot
recipient_json TEXT NOT NULL, -- structured {name,name2,addr1,addr2,city,state,zip}
sender_json TEXT NOT NULL, -- structured {name,addr1,addr2,city,state,zip}
mailtype TEXT NOT NULL DEFAULT 'firstclass',
status TEXT NOT NULL DEFAULT 'DRAFT'
CHECK (status IN ('DRAFT','APPROVED','PREAUTH','SENT','REJECTED','CANCELLED','ERROR')),
job_id TEXT, -- LetterStream unique job name
batch_id TEXT, -- LetterStream batch id
doc_id TEXT, -- LetterStream doc id
tracking_no TEXT, -- USPS tracking (certified mail)
cost_cents INTEGER, -- pricing from preauth/response
authcode TEXT, -- preauth authcode (release via doauth)
pages INTEGER,
pdf_path TEXT, -- rendered PDF on disk
error TEXT,
note TEXT, -- reject/cancel reason (terminal states)
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
sent_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_letters_claim ON letters(claim_id);
CREATE INDEX IF NOT EXISTS idx_letters_status ON letters(status);
CREATE INDEX IF NOT EXISTS idx_letters_doc ON letters(doc_id);
-- LetterStream tracking scan events (pushed via callback every 4h).
CREATE TABLE IF NOT EXISTS letter_events (
id TEXT PRIMARY KEY,
letter_id TEXT NOT NULL REFERENCES letters(id) ON DELETE CASCADE,
scan_code TEXT,
scan_status TEXT,
scan_date TEXT,
scan_zip TEXT,
scan_facility TEXT,
tracking_id TEXT,
batch_id TEXT,
job_id TEXT,
doc_id TEXT,
raw_json TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_letter_events_letter ON letter_events(letter_id);