Coverage for apps/disclosures/providers.py: 89%
36 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
1"""Pluggable DocMagic adapter.
3A ``DevDocMagicProvider`` returns deterministic audit findings + a stub document
4reference for local/dev; the real DocMagic adapter is a drop-in once org credentials
5exist, the same seam as the credit/AUS providers in ``apps/underwriting/providers.py``.
6"""
8from __future__ import annotations
10import uuid
11from dataclasses import dataclass
12from typing import Protocol
15@dataclass
16class AuditFinding:
17 code: str
18 severity: str # "fatal" | "warning"
19 message: str
22class DocMagicProvider(Protocol):
23 def audit(self, loan, package_type: str) -> list[AuditFinding]: ...
24 def generate(self, loan, package_type: str) -> str: ...
27class DevDocMagicProvider:
28 """Deterministic dev stub — NOT a real DocMagic audit/generation."""
30 def audit(self, loan, package_type: str) -> list[AuditFinding]:
31 findings: list[AuditFinding] = []
32 txn = getattr(loan, "transaction", None)
33 if package_type == "LoanEstimate" and txn is None:
34 findings.append(
35 AuditFinding(
36 "DEV-NO-TXN",
37 "fatal",
38 "No loan transaction data — cannot estimate figures.",
39 )
40 )
41 return findings
42 if package_type in ("ClosingDisclosure", "ClosingPackage") and loan.fees.count() == 0: 42 ↛ 43line 42 didn't jump to line 43 because the condition on line 42 was never true
43 findings.append(
44 AuditFinding(
45 "DEV-NO-FEES",
46 "fatal",
47 "No fee data recorded — final figures are required for this package.",
48 )
49 )
50 return findings
51 if not loan.borrowers.exists():
52 findings.append(
53 AuditFinding("DEV-NO-BORROWER", "warning", "No borrower recorded on this loan.")
54 )
55 return findings
57 def generate(self, loan, package_type: str) -> str:
58 return f"dev-docmagic://{package_type.lower()}/{loan.id}/{uuid.uuid4().hex[:8]}"
61class RealDocMagicProvider:
62 """Real DocMagic adapter — wired at integration (org credentials via admin config)."""
64 def audit(self, loan, package_type: str) -> list[AuditFinding]:
65 raise NotImplementedError("DocMagic not configured; set DOCMAGIC_PROVIDER=dev for local.")
67 def generate(self, loan, package_type: str) -> str:
68 raise NotImplementedError("DocMagic not configured; set DOCMAGIC_PROVIDER=dev for local.")
71def get_docmagic_provider(org=None) -> DocMagicProvider:
72 """Resolve the DocMagic adapter for this org (org creds -> env default); fall back
73 to the dev stub when nothing real is configured."""
74 from apps.integrations.resolver import resolve_credentials
76 cred = resolve_credentials("docmagic", org=org)
77 if cred is not None and cred.provider != "dev":
78 return RealDocMagicProvider()
79 return DevDocMagicProvider()