# Letter Queue - Backend Scope (draft) Date: 2026-08-24 Author: Sho'Nuff (draft for Germaine review) ## Current state (ground truth, verified 2026-08-24) - **Send pipeline is BUILT and verified end-to-end.** Two new modules: - `/opt/dre-portal/app/letterstream.py` — LetterStream integration (auth, account status, send single/batch, preauth/doauth, tracking, signature, proof). - `/opt/dre-portal/app/letters.py` — router: letters queue lifecycle, PDF render (fpdf2 2.8.8), LetterStream send, and the callback receiver. - **Live DB tables exist:** `letters` and `letter_events` (added to `schema.sql`, applied on restart). - **LetterStream API contract fully recovered** from the user-supplied `api_fulfillment.pdf` (21 pages): `POST https://www.letterstream.com/apis/` with `a`=api_id, `h`=hash, `t`=unique_id. Hash = `md5(base64_encode(last6(t) . api_key . first6(t)))`. Verified three ways: PDF formula + live `AUTHOK` + user screenshot sample vector. - **Account live-verified:** balance `$100.00`, `testmode=disabled`. No documents submitted, no charges incurred. - Letter *content* still lives on `claims` (subject/body/tier) and is generated by `analysis.recommend_letter()` (deterministic, no LLM) — the `letters` queue is the physical-send layer on top. - **Queue UI is wired and smoke-tested (2026-08-25, task ls7).** `letter-queue.html` is a functional queue: filterable list, status badges, Approve / Price & Queue / Confirm & Mail / Track actions, and a New Letter modal (claim picker + structured recipient + mail class). `GET /api/staff/letters` now joins `claims` to expose `claim_number`. Verified via live API lifecycle + Node render harness + 9-page HTTP smoke (all 200). - LetterStream callback is **NOT enabled yet** — the receiver is live and tested, but the callback toggle in the LetterStream dashboard stays off until after the first real send is confirmed. ## Target state A durable letter queue: many letters per claim, each with a lifecycle (draft -> approved -> queued -> sent -> delivered/failed), sendable via LetterStream certified mail, with FDCPA-compliant content and a full audit trail. ## 1. Data model - new `letters` table | Column | Type | Purpose | |---|---|---| | id | TEXT PK | uuid4 | | claim_id | TEXT FK -> claims.id | owning claim | | letter_type | TEXT | demand / escalation / validation / custom | | tier | TEXT | tier the letter was generated for | | subject | TEXT | letter subject | | body | TEXT | letter body (markdown or plain) | | status | TEXT | draft / approved / queued / sending / sent / delivered / failed | | recipient_name | TEXT | debtor or registered-agent name | | addr1, addr2, city, state, zip | TEXT | mailing address | | letterstream_job_id | TEXT | LetterStream job reference (nullable) | | tracking_number | TEXT | USPS tracking (nullable) | | sent_at | TEXT | ISO timestamp (nullable) | | delivered_at | TEXT | ISO timestamp (nullable) | | error | TEXT | last send error (nullable) | | created_by | TEXT | RBAC actor name | | created_at / updated_at | TEXT | ISO timestamps | Migration: keep the four claim columns as the "current draft" during transition, then deprecate them once the queue is live. No destructive drop until the queue is proven in production. ## 2. API - `GET /api/staff/claims/{n}/letters` - list letters for a claim - `POST /api/staff/claims/{n}/letters/generate` - generate a draft row via `recommend_letter()` (inserts, does not overwrite the claim columns) - `PUT /api/staff/letters/{id}` - edit a draft - `POST /api/staff/letters/{id}/queue` - mark queued (requires full mailing address) - `POST /api/staff/letters/{id}/send` - call LetterStream, store job_id + tracking - `POST /api/letters/webhook` - LetterStream status callback (delivered / failed) - `GET /api/staff/letters` - global queue across claims (feeds letter-queue.html) Idempotency: `send` is guarded by status (only `queued` -> `sending`), so a double-click cannot mail a letter twice. Store `letterstream_job_id` before marking sent. ## 3. Send pipeline (LetterStream) Order of work: 1. Verify the existing `LETTERSTREAM_API_KEY` against their API (is it valid, what account, what products are enabled). 2. Map their REST surface: auth method, endpoint shape, certified vs first-class, PDF upload vs HTML/plain rendering, return address handling, tracking + status webhook. Do not assume - confirm from their docs or a test call. 3. PDF generation: render the letter body (reportlab or weasyprint) with DRE letterhead, or pass content to LetterStream to render. 4. Address handling: return address (DRE office / PO box) and debtor mailing address must both be resolved before send. 5. Status sync: webhook or poll updates `status`, `tracking_number`, `delivered_at`. ## 4. Compliance (FDCPA) - Every first-contact letter MUST carry the 1692g validation notice: amount of debt, creditor name, 30-day dispute right, right to request verification. - No false, deceptive, or misleading language (1692e); no threats of action DRE does not intend to take. - Human sign-off gate: a letter cannot move to `queued` until status is `approved` (actor recorded). - Full immutable audit log (actor + timestamp + old/new) - the `audit_log` pattern already exists and extends here. ## 5. UI (letter-queue.html) — DONE 2026-08-25 (ls7) `/var/www/internal/letter-queue.html` is live: - filterable list (status) with per-status summary chips - Approve (DRAFT) / Price & Queue (APPROVED/PREAUTH/ERROR) / Confirm & Mail (PREAUTH) / Reject / Cancel / Track actions - status badges (DRAFT/APPROVED/PREAUTH/SENT/REJECTED/CANCELLED/ERROR) - tracking timeline (USPS scan events) in the detail panel - New Letter modal: claim picker (prefills debtor name), structured recipient, mail class Reject/cancel implemented 2026-08-25: `POST /api/staff/letters/{id}/reject` (requires `reason`) and `POST /api/staff/letters/{id}/cancel` (optional `reason`) move DRAFT/APPROVED/PREAUTH/ERROR letters to REJECTED/CANCELLED, persist the reason in `letters.note`, and write an `audit_log` row. Guards return 409 for SENT/REJECTED/CANCELLED and 422 for a missing reject reason. Queue UI has Reject (red) / Cancel buttons for all non-terminal states. ## 6. Decisions locked (2026-08-24) 1. **LetterStream key** — valid and live; `$100.00` balance, `testmode=disabled`. 2. **Return address** — Germaine provides it 2026-08-25. Set as `LETTERSTREAM_RETURN_ADDRESS` in `.env`; drafting does NOT block on it, only `send` does (clean 409 until configured). 3. **Signatory** — `Debt Recovery Experts LLC` (no named individual). Default in code; overridable via `LETTERSTREAM_SIGNATORY` in `.env`. 4. **Address verification / NCOA** — none. Pull debtor/return-address data from the client's claim info + Super Search. 5. **FDCPA 1692g notice** — mandatory on first contact (DRE is a third-party debt collector). Content is generated by `analysis.recommend_letter()`; final validation- notice wording vs welcome-packet copy still to be finalized (task ls8).