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
+250
View File
@@ -0,0 +1,250 @@
"""LetterStream mail-fulfillment integration.
Send endpoint: POST https://www.letterstream.com/apis/ (form-encoded or multipart).
Auth (verified live 2026-08-25):
t = unique numeric id (10-18 digits), unique per request
s = t[-6:] + api_key + t[:6]
h = md5(base64_encode(s))
Response: XML <messages><message type><code><details>...</messages>.
Key codes: -100 success, -199 AUTHOK, -200 preauth success, -911 insufficient
funding, -950 auth fail, -957 DUP, -958 IDOK, -998 improper submission, -999 error.
"""
from __future__ import annotations
import base64
import hashlib
import os
import time
import uuid
import xml.etree.ElementTree as ET
import urllib.error
import urllib.parse
import urllib.request
ENDPOINT = "https://www.letterstream.com/apis/"
API_ID = os.environ.get("LETTERSTREAM_API_ID", "")
API_KEY = os.environ.get("LETTERSTREAM_API_KEY", "")
UA = "DRE-integration/1.0"
class LetterStreamError(Exception):
"""Raised when LetterStream returns an error or is unreachable."""
def auth() -> dict:
"""Return {'a', 'h', 't'} for a single request. Call once per request."""
if not API_ID or not API_KEY:
raise LetterStreamError("LETTERSTREAM_API_ID/KEY not configured")
t = str(int(time.time() * 1000)) # 13-digit millis, within 10-18 digit spec
s = t[-6:] + API_KEY + t[:6]
h = hashlib.md5(base64.b64encode(s.encode())).hexdigest()
return {"a": API_ID, "h": h, "t": t}
def _multipart(items: list[tuple[str, str]], files: dict) -> tuple[bytes, str]:
"""Build a multipart/form-data body. files: {field: (filename, bytes, content_type)}."""
boundary = "----DRE" + uuid.uuid4().hex
chunks: list[bytes] = []
def add(data) -> None:
chunks.append(data.encode("utf-8") if isinstance(data, str) else data)
for k, v in items:
add(f"--{boundary}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n")
add(str(v))
add("\r\n")
for k, (filename, data, ctype) in files.items():
add(f"--{boundary}\r\nContent-Disposition: form-data; name=\"{k}\"; "
f"filename=\"{filename}\"\r\nContent-Type: {ctype}\r\n\r\n")
add(data)
add("\r\n")
add(f"--{boundary}--\r\n")
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
def _post(items: list[tuple[str, str]], files: dict | None = None,
timeout: int = 60) -> bytes:
if files:
body, ctype = _multipart(items, files)
else:
body = urllib.parse.urlencode(items).encode()
ctype = "application/x-www-form-urlencoded"
req = urllib.request.Request(
ENDPOINT, data=body, headers={"Content-Type": ctype, "User-Agent": UA},
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.read()
except urllib.error.HTTPError as e:
return e.read()
except urllib.error.URLError as e:
raise LetterStreamError(f"LetterStream unreachable: {e.reason}") from e
def _auth_items() -> list[tuple[str, str]]:
a = auth()
return [("a", a["a"]), ("h", a["h"]), ("t", a["t"])]
def parse_xml(raw: bytes | str) -> dict:
"""Parse the XML response into a convenient dict. Streams (PDF) pass through."""
if isinstance(raw, bytes):
raw = raw.decode("utf-8", "replace")
out: dict = {"raw": raw, "messages": []}
if raw.startswith("%PDF"):
out["stream"] = "pdf"
out["data"] = raw
return out
try:
root = ET.fromstring(raw)
except ET.ParseError:
out["stream"] = "unknown"
return out
out["id"] = root.get("id", "")
for msg in root.findall("message"):
m: dict = {"type": msg.get("type", "")}
for tag in ("code", "details", "batch", "quantity", "cost", "authcode",
"date", "balance", "testmode", "id", "job"):
el = msg.find(tag)
if el is not None and el.text is not None:
m[tag] = el.text.strip()
docs = []
for d in msg.findall("doc"):
docs.append({c.tag: (c.text or "").strip() for c in d})
if docs:
m["docs"] = docs
out["messages"].append(m)
return out
def _first(response: dict) -> dict:
return response["messages"][0] if response.get("messages") else {}
def _raise_on_error(response: dict) -> None:
"""Raise LetterStreamError if the response is an error message."""
for m in response.get("messages", []):
code = m.get("code")
if m.get("type") == "error" and code not in (None, "-100", "-200"):
raise LetterStreamError(f"LetterStream {code}: {m.get('details', '')}")
def account_status() -> dict:
"""Return {'date','id','balance','testmode'}."""
items = _auth_items() + [("accountstatus", "1")]
resp = parse_xml(_post(items))
_raise_on_error(resp)
for m in resp.get("messages", []):
if m.get("type") == "accountstatus":
return {k: m.get(k) for k in ("date", "id", "balance", "testmode")}
raise LetterStreamError(f"Unexpected account status response: {resp.get('raw', '')[:200]}")
def send_single(pdf_bytes: bytes, filename: str, job: str, sender: str,
recipients: list[str], pages: int, mailtype: str = "firstclass",
coversheet: str | None = None, duplex: str | None = None,
ink: str | None = None, paper: str | None = None,
returnenv: str | None = None, preauth: bool = False) -> dict:
"""Submit one PDF to one or more recipients (method 2).
sender: 'name_1:name_2:addr_1:addr_2:city:state:zip'
recipients: list of 'doc_id:name_1:name_2:addr_1:addr_2:city:state:zip'
"""
items = _auth_items() + [("job", job), ("from", sender), ("pages", str(pages))]
for r in recipients:
items.append(("to[]", r))
if mailtype:
items.append(("mailtype", mailtype))
if coversheet is not None:
items.append(("coversheet", coversheet))
if duplex is not None:
items.append(("duplex", duplex))
if ink is not None:
items.append(("ink", ink))
if paper is not None:
items.append(("paper", paper))
if returnenv is not None:
items.append(("returnenv", returnenv))
if preauth:
items.append(("preauth", "1"))
files = {"single_file": (filename, pdf_bytes, "application/pdf")}
resp = parse_xml(_post(items, files=files))
_raise_on_error(resp)
return resp
def send_batch(zip_bytes: bytes, filename: str) -> dict:
"""Submit a ZIP archive (PDFs + CSV) via batch method (method 1)."""
items = _auth_items()
files = {"multi_file": (filename, zip_bytes, "application/zip")}
resp = parse_xml(_post(items, files=files))
_raise_on_error(resp)
return resp
def doauth(authcode: str) -> dict:
"""Release a preauth job into production."""
items = _auth_items() + [("doauth", authcode)]
resp = parse_xml(_post(items))
_raise_on_error(resp)
return resp
def _tracking_query(**kwargs) -> dict:
items = _auth_items() + [(k, v) for k, v in kwargs.items() if v]
resp = parse_xml(_post(items))
_raise_on_error(resp)
return resp
def tracking(cert: str | None = None, doc_id: str | None = None,
fmt: str = "xml") -> dict:
"""Tracking info by certified number or doc_id. fmt: 'html'|'xml'|'json'."""
args = {}
if cert:
args["cert"] = cert
args["getinfo"] = "trackx" if fmt == "xml" else "track"
elif doc_id:
args["doc_id"] = doc_id
args["getinfo"] = "trackx" if fmt == "xml" else "track"
else:
raise LetterStreamError("tracking() requires cert or doc_id")
if fmt == "json":
args["responseformat"] = "json"
return _tracking_query(**args)
def signature(cert: str | None = None, doc_id: str | None = None) -> bytes:
"""Return the certified signature file as PDF bytes."""
args = {"getinfo": "sig"}
if cert:
args["cert"] = cert
elif doc_id:
args["doc_id"] = doc_id
else:
raise LetterStreamError("signature() requires cert or doc_id")
items = _auth_items() + list(args.items())
raw = _post(items)
if raw[:4] == b"%PDF":
return raw
resp = parse_xml(raw)
_raise_on_error(resp)
raise LetterStreamError("No signature file returned")
def proof(doc_id: str) -> bytes:
"""Return the document proof as PDF bytes (base64-decoded when needed)."""
items = _auth_items() + [("doc_id", doc_id), ("getinfo", "proof")]
raw = _post(items)
if raw[:4] == b"%PDF":
return raw
text = raw.decode("utf-8", "replace").strip()
if len(text) > 10000:
try:
return base64.b64decode(text)
except Exception as e:
raise LetterStreamError(f"Proof base64 decode failed: {e}") from e
resp = parse_xml(raw)
_raise_on_error(resp)
raise LetterStreamError("No proof returned")