Files
dre/backend/intake.py
T
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

160 lines
7.0 KiB
Python

"""Intake endpoint — POST /api/intake. Creates client (+reuse by email) + debtor + claim.
Writes audit rows, SYSTEM note, emails the DRE team (best-effort).
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, Request, status
from pydantic import ValidationError
from . import auth as authmod
from . import db
from . import dreemail
from . import analysis
from .db import get_conn, new_uuid, next_sequence_number, utcnow_iso
from .models import IntakeRequest
logger = logging.getLogger("dre.intake")
router = APIRouter()
def _money_display(cents: int) -> str:
return f"${cents / 100.0:,.2f}"
@router.post("/api/intake")
async def intake(request: Request):
# Rate limit
ip = authmod.get_client_ip(request)
if not authmod.rate_limit_intake(ip):
return _json_error("rate_limited", "Too many requests. Please try again later.",
status.HTTP_429_TOO_MANY_REQUESTS)
# Parse JSON body
try:
body = await request.json()
except Exception:
return _json_error("validation_error", "Invalid JSON body.", status.HTTP_422_UNPROCESSABLE_ENTITY)
# Validate
try:
req = IntakeRequest.model_validate(body)
except ValidationError as exc:
return _json_error("validation_error", _format_validation_error(exc),
status.HTTP_422_UNPROCESSABLE_ENTITY)
client_in = req.client
debtor_in = req.debtor
claim_in = req.claim
email_lc = client_in.email.lower()
with get_conn() as conn:
now = utcnow_iso()
# Reuse client by email or create new
existing = conn.execute("SELECT id, client_number FROM clients WHERE email = ?", (email_lc,)).fetchone()
if existing is not None:
client_id = existing["id"]
client_number = existing["client_number"]
conn.execute(
"UPDATE clients SET company_name = ?, contact_name = ?, phone = ?, tos_accepted_at = COALESCE(tos_accepted_at, ?), updated_at = ? WHERE id = ?",
(client_in.company_name, client_in.contact_name, client_in.phone, now, now, client_id),
)
# audit update
conn.execute(
"INSERT INTO audit_log (id, entity_type, entity_id, action, field, old_value, new_value, actor, reason, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(new_uuid(), "client", client_id, "update", "contact_info", None, None, "intake", "reused existing client", now),
)
else:
client_id = new_uuid()
client_number = next_sequence_number(conn, "CLT")
conn.execute(
"INSERT INTO clients (id, client_number, company_name, contact_name, email, phone, tos_accepted_at, twentycrm_id, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)",
(client_id, client_number, client_in.company_name, client_in.contact_name,
email_lc, client_in.phone, now, now, now),
)
conn.execute(
"INSERT INTO audit_log (id, entity_type, entity_id, action, actor, created_at) VALUES (?, ?, ?, ?, ?, ?)",
(new_uuid(), "client", client_id, "create", "intake", now),
)
# Debtor (always new per spec — denormalized avoided, but new claim = new debtor row)
debtor_id = new_uuid()
conn.execute(
"INSERT INTO debtors (id, name, business_type, contact_email, contact_phone, physical_address, twentycrm_id, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)",
(debtor_id, debtor_in.name, debtor_in.business_type, debtor_in.contact_email,
debtor_in.contact_phone, debtor_in.physical_address, now, now),
)
conn.execute(
"INSERT INTO audit_log (id, entity_type, entity_id, action, actor, created_at) VALUES (?, ?, ?, ?, ?, ?)",
(new_uuid(), "debtor", debtor_id, "create", "intake", now),
)
# Claim
claim_id = new_uuid()
claim_number = next_sequence_number(conn, "DRE")
conn.execute(
"INSERT INTO claims (id, claim_number, client_id, debtor_id, amount_cents, currency, status, tier, description, client_reference, invoice_date, date_assigned, date_resolved, no_other_agency_at, no_prior_action_at, twentycrm_id, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, 'USD', 'NEW', 'TIER_1', ?, ?, ?, NULL, NULL, ?, ?, NULL, ?, ?)",
(claim_id, claim_number, client_id, debtor_id, claim_in.amount_cents,
claim_in.description, claim_in.client_reference, claim_in.invoice_date,
now if req.no_other_agency_accepted else None,
now if req.no_prior_action_accepted else None, now, now),
)
conn.execute(
"INSERT INTO audit_log (id, entity_type, entity_id, action, old_value, new_value, actor, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(new_uuid(), "claim", claim_id, "create", None, claim_number, "intake", now),
)
# SYSTEM shared note
note_id = new_uuid()
conn.execute(
"INSERT INTO case_notes (id, claim_id, author_type, author_name, subject, content, visibility, twentycrm_id, created_at) "
"VALUES (?, ?, 'SYSTEM', 'System', NULL, 'Claim received.', 'SHARED', NULL, ?)",
(note_id, claim_id, now),
)
conn.execute(
"INSERT INTO audit_log (id, entity_type, entity_id, action, actor, created_at) VALUES (?, ?, ?, ?, ?, ?)",
(new_uuid(), "note", note_id, "note_add", "system", now),
)
# Auto-run AI analysis on submission (score + recommended tier + approval=PENDING).
# Deterministic rules engine; no external call. Best-effort: a failure must not
# block intake, so we guard and let the claim persist without a score if it errors.
try:
analysis.analyze_and_store(conn, claim_id, actor="system")
except Exception as exc: # noqa: BLE001
logger.error("auto-analysis failed for claim %s: %s", claim_number, exc)
conn.commit()
# Best-effort team notification
try:
dreemail.notify_team_intake(claim_number, client_number, client_in.company_name,
claim_in.amount_cents, debtor_in.name)
except Exception as exc: # noqa: BLE001
logger.error("intake team notify failed: %s", exc)
return {
"claim_number": claim_number,
"client_number": client_number,
"status": "NEW",
"message": "Claim received. Our team will review and contact you shortly.",
}
def _format_validation_error(exc: ValidationError) -> str:
parts = []
for err in exc.errors():
loc = ".".join(str(x) for x in err["loc"])
parts.append(f"{loc}: {err['msg']}")
return "; ".join(parts)
def _json_error(code: str, message: str, status_code: int):
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=status_code,
content={"error": {"code": code, "message": message}},
)