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).
This commit is contained in:
root
2026-08-26 02:26:33 -04:00
parent 7a5603b495
commit 7a62b0b340
46 changed files with 11004 additions and 35 deletions
+86
View File
@@ -0,0 +1,86 @@
# DRE Customer Portal - Static Frontend
Dependency-free HTML/CSS/JS frontend for the Debt Recovery Experts (DRE)
customer portal. No build step, no frameworks, no npm. Every page loads
`css/dre.css` and (where interactive) `js/dre-api.js` via plain `<script>`
tags, and calls the backend using relative `/api/*` paths only. This is
intended to be dropped behind Caddy, which proxies `/api/*` to the FastAPI
backend and serves everything else as static files on the two subdomains
below.
This directory is a DESIGN-ONLY deliverable: no deploy, no Caddy/systemd
config, no backend changes were made or are included here.
## Files created
```
/root/projects/dre/frontend/
index.html Public intake form
login.html Client login (request magic link)
verify.html Magic-link verification landing page
dashboard.html Client dashboard (claims list + detail + upload + messages)
css/dre.css Shared stylesheet for all four pages
js/dre-api.js Shared JS helper: fetch wrapper, session storage, formatting
README.md This file
```
## Route / subdomain mapping
| File | Serves at | Auth required |
|------------------|--------------------------------------------------|---------------|
| `index.html` | `portal.debtrecoveryexperts.com/` (site root) | No (public) |
| `login.html` | `my.debtrecoveryexperts.com/` (site root) | No (public) |
| `verify.html` | `my.debtrecoveryexperts.com/verify` | No (public, consumes a one-time token) |
| `dashboard.html` | `my.debtrecoveryexperts.com/dashboard` | Yes (redirects to `login.html` if no session) |
| `css/dre.css` | `/css/dre.css` on both subdomains | - |
| `js/dre-api.js` | `/js/dre-api.js` on both subdomains | - |
Caddy is expected to:
1. Serve `portal.debtrecoveryexperts.com` from this directory with `index.html` as the site index.
2. Serve `my.debtrecoveryexperts.com` from this directory with `login.html` as the site index, `verify.html` at `/verify`, and `dashboard.html` at `/dashboard`.
3. Reverse-proxy `/api/*` on both subdomains to the FastAPI backend (same-origin so the JS `fetch()` calls need no CORS config and no hardcoded backend host).
No Caddyfile is included per the design-only constraint; this table is the
spec for whoever wires up routing.
## API contract implemented (verified against backend/main.py, backend/models.py, backend/claims.py, backend/intake.py)
- `POST /api/intake` - body `{client:{company_name, contact_name, email, phone?}, debtor:{name, business_type, contact_email?, contact_phone?, physical_address?}, claim:{amount_cents, description?, client_reference?, invoice_date?}, tos_accepted:true}`. Response `{claim_number, client_number, status, message}`. Amount is collected as dollars in the UI and converted to integer cents client-side before posting.
- `POST /api/auth/request` - body `{email}`. Always returns the anti-enumeration message `{message: "If an account exists..."}`; UI always shows the "check your email" state on a 2xx response regardless of whether the account exists.
- `POST /api/auth/verify` - body `{token}`. Response `{session_token, expires_at, client:{client_number, company_name, contact_name}}`. Token is read from `?token=` in the URL, stripped from the address bar immediately (history.replaceState) before the API call, and the session token is stored in `localStorage` under the key `dre_session_token`.
- `GET /api/auth/me` - `Authorization: Bearer <token>`. Response `{client:{...}, claim_count}`.
- `POST /api/auth/logout` - `Authorization: Bearer <token>`. Clears local storage and redirects to login regardless of response.
- `GET /api/claims` - Response `{claims:[{claim_number, status, status_label, tier, amount_cents, amount_display, debtor_name, created_at, date_resolved}]}`.
- `GET /api/claims/{claim_number}` - Response includes `status_label`, `tier_step` (used to render the 4-step progress bar), `debtor:{name, business_type}`, `documents:[...]`, `notes:[...]`. Note: this endpoint does not return a claim `created_at`; the UI shows `date_assigned` (or "Not yet assigned") instead of a submission date.
- `POST /api/claims/{claim_number}/documents` - multipart `FormData` with field name `file` (matches `UploadFile = File(...)` param name in `claims.py`). Client-side pre-checks: 20 MB max, extensions `.pdf .jpg .jpeg .png .doc .docx` (mirrors `ALLOWED_EXT` in `claims.py`).
- `POST /api/claims/{claim_number}/messages` - body `{subject, content}`. `subject` must be one of the fixed `MESSAGE_SUBJECTS` enum from `models.py`; rendered as a `<select>` with those exact values. (The backend's client-facing endpoint for adding case correspondence is `/messages`, not `/notes` - the dashboard's "Message the Team" panel targets this and refreshes the notes list on success, since messages are stored as shared case notes.)
Error envelope handled uniformly everywhere: `{"error": {"code", "message"}}`.
Specific codes handled: `validation_error` (422, including per-field mapping
on the intake form for `client.*` / `debtor.*` / `claim.*` / `tos_accepted`
locations), `unauthorized` (401, clears session + redirects to login),
`not_found` (404), `rate_limited` (429), `payload_too_large` /
`unsupported_media_type` / `conflict` (document upload).
## Compliance / safety notes
- No field anywhere collects SSN, full bank account, or card numbers. The intake form has an explicit on-page warning, and the backend's PII regex rejection (`validation_error`) is surfaced inline on the matching form field when triggered.
- All user-authored or backend-sourced free text (case notes, messages) is rendered via `textContent`, never `innerHTML`, so it cannot execute as markup even though the backend also HTML-escapes it server-side.
- Wording throughout intake/login/dashboard is neutral and professional (e.g. "recovery review", "claim", "case notes") - no aggressive or threatening collector language, consistent with FDCPA/TDCPA constraints.
- Dollar amounts are always rendered from integer cents (`amount_cents / 100`), formatted as USD via `DRE.formatCentsUSD()`.
## What was NOT done (out of scope for this seat)
- No deployment: nothing was copied to `/opt/dre-portal` or any web root.
- No Caddy or systemd configuration was created or modified.
- No backend files were modified (only read for contract verification).
- No database was created, seeded, or touched.
## Manual smoke test performed
Served this directory locally with `python3 -m http.server` (throwaway, not
part of the deliverable) and confirmed all six files return HTTP 200,
all inline `<script>` blocks parse without syntax errors (`node -c`
equivalent check), all HTML tags balance, and every `getElementById()`
reference in the JS resolves to an element that actually exists in the
corresponding HTML file.