Coverage for config/api.py: 81%
157 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 14:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 14:06 +0000
1"""Django Ninja API root.
3Mounted at ``/api`` by ``config/urls.py``. Capability routers are added per sprint.
4The OpenAPI schema exposed at ``/api/openapi.json`` is the source for the generated
5TypeScript client (``frontend/packages/api-client``).
6"""
8import logging
10from django.core.exceptions import (
11 ObjectDoesNotExist,
12 PermissionDenied,
13)
14from django.core.exceptions import (
15 ValidationError as DjangoValidationError,
16)
17from django.db import DatabaseError, DataError, IntegrityError
18from django.http import Http404
19from ninja import NinjaAPI
20from ninja.errors import ValidationError as NinjaValidationError
22from apps.accounts.admin_api import (
23 admin_console_router,
24 billing_router,
25 compliance_router,
26 provision_router,
27)
28from apps.accounts.admin_api import router as accounts_router
29from apps.accounts.directory_api import router as directory_router
30from apps.accounts.local_auth_api import router as local_auth_router
31from apps.accounts.setup_api import router as setup_router
32from apps.accounts.tireless_api import router as tireless_router
33from apps.application_1003.api import router as financials_router
34from apps.borrower_portal.api import router as borrower_auth_router
35from apps.borrower_portal.invite_api import router as pos_invite_router
36from apps.borrower_portal.pos_api import geocode_router
37from apps.borrower_portal.pos_api import router as pos_router
38from apps.borrower_portal.upload_api import (
39 portal_router as pos_documents_router,
40)
41from apps.borrower_portal.upload_api import public_router as upload_public_router
42from apps.borrower_portal.upload_api import revoke_router as upload_revoke_router
43from apps.borrower_portal.upload_api import staff_router as upload_invites_router
44from apps.client_needs.api import classifications_router
45from apps.client_needs.api import router as client_needs_router
46from apps.communications.api import router as communications_router
47from apps.communications.webhooks import router as webhooks_router
48from apps.core_los.api import router as loans_router
49from apps.core_los.contacts_api import router as contacts_router
50from apps.core_los.custom_fields_api import custom_fields_router, loan_custom_fields_router
51from apps.core_los.dashboard_api import router as dashboard_router
52from apps.core_los.leads_api import router as leads_router
53from apps.core_los.leads_search_api import router as leads_search_router
54from apps.core_los.public_api import router as public_router
55from apps.core_los.report_builder_api import router as report_builder_router
56from apps.core_los.reports_api import router as reports_router
57from apps.core_los.saved_views_api import router as saved_views_router
58from apps.disclosures.api import router as disclosures_router
59from apps.fees_trid.api import router as fees_router
60from apps.integrations.admin_api import router as admin_integrations_router
61from apps.integrations.settings_api import router as settings_router
62from apps.mismo.api import router as mismo_router
63from apps.pricing_ppe.api import router as pricing_router
64from apps.underwriting.api import router as credit_router
65from apps.underwriting.aus_api import router as aus_router
66from apps.underwriting.condition_api import router as condition_router
67from apps.workflow.api import config_router as workflow_config_router
68from apps.workflow.api import router as workflow_router
69from apps.workflow.api import task_templates_router
70from apps.workflow.rules_api import router as rules_router
71from apps.workflow.tasks_hub_api import router as tasks_hub_router
72from domain.result import AppError, DomainError, format_db_error, to_envelope
74logger = logging.getLogger(__name__)
76api = NinjaAPI(
77 title="Lumen Lending OS API",
78 version="0.1.0",
79 description="Enterprise mortgage loan origination platform API.",
80)
83@api.exception_handler(DomainError)
84def _domain_error_handler(request, exc: DomainError):
85 """Render every governed domain error as the shared envelope."""
86 return api.create_response(
87 request, to_envelope(exc.status_code, exc.errors), status=exc.status_code
88 )
91@api.exception_handler(NinjaValidationError)
92def _ninja_validation_error_handler(request, exc: NinjaValidationError):
93 """Render Pydantic / Ninja request validation errors in the standard envelope."""
94 app_errors = []
95 for err in exc.errors:
96 loc = err.get("loc", [])
97 field_name = str(loc[-1]) if loc else "field"
98 msg = err.get("msg", "Invalid value")
99 clean_field = field_name.replace("_", " ").capitalize()
100 app_errors.append(
101 AppError("VALIDATION_ERROR", f"{clean_field}: {msg}", field=field_name)
102 )
103 envelope = to_envelope(422, app_errors)
104 envelope["detail"] = exc.errors
105 return api.create_response(request, envelope, status=422)
108@api.exception_handler(DjangoValidationError)
109def _django_validation_error_handler(request, exc: DjangoValidationError):
110 """Render Django model / form validation errors in the standard envelope."""
111 app_errors = []
112 if hasattr(exc, "message_dict"):
113 for field_name, msgs in exc.message_dict.items():
114 clean_field = field_name.replace("_", " ").capitalize()
115 for m in msgs:
116 app_errors.append(
117 AppError("VALIDATION_ERROR", f"{clean_field}: {m}", field=field_name)
118 )
119 elif hasattr(exc, "messages"):
120 for m in exc.messages:
121 app_errors.append(AppError("VALIDATION_ERROR", m))
122 else:
123 app_errors.append(AppError("VALIDATION_ERROR", str(exc)))
124 return api.create_response(request, to_envelope(422, app_errors), status=422)
127@api.exception_handler(DataError)
128def _data_error_handler(request, exc: DataError):
129 """Gracefully handle database numeric overflow or data length truncation."""
130 msg = format_db_error(exc)
131 return api.create_response(
132 request,
133 to_envelope(422, [AppError("DATA_ERROR", msg)]),
134 status=422,
135 )
138@api.exception_handler(IntegrityError)
139def _integrity_error_handler(request, exc: IntegrityError):
140 """Gracefully handle database integrity / constraint violations."""
141 msg = format_db_error(exc)
142 return api.create_response(
143 request,
144 to_envelope(422, [AppError("INTEGRITY_ERROR", msg)]),
145 status=422,
146 )
149@api.exception_handler(DatabaseError)
150def _database_error_handler(request, exc: DatabaseError):
151 """Gracefully handle unexpected database errors."""
152 logger.exception("Database error on %s %s: %s", request.method, request.path, exc)
153 msg = format_db_error(exc)
154 return api.create_response(
155 request,
156 to_envelope(422, [AppError("DATABASE_ERROR", msg)]),
157 status=422,
158 )
161@api.exception_handler(Http404)
162def _http_404_handler(request, exc: Http404):
163 """Render 404 Not Found in the standard envelope."""
164 return api.create_response(
165 request,
166 to_envelope(404, [AppError("NOT_FOUND", str(exc) or "The requested resource was not found.")]),
167 status=404,
168 )
171@api.exception_handler(ObjectDoesNotExist)
172def _object_does_not_exist_handler(request, exc: ObjectDoesNotExist):
173 """Render Django DoesNotExist in the standard envelope."""
174 return api.create_response(
175 request,
176 to_envelope(404, [AppError("NOT_FOUND", "The requested record was not found.")]),
177 status=404,
178 )
181@api.exception_handler(PermissionDenied)
182def _permission_denied_handler(request, exc: PermissionDenied):
183 """Render 403 Forbidden in the standard envelope."""
184 return api.create_response(
185 request,
186 to_envelope(403, [AppError("FORBIDDEN", str(exc) or "You do not have permission to perform this action.")]),
187 status=403,
188 )
191@api.exception_handler(Exception)
192def _unhandled_exception_handler(request, exc: Exception):
193 """Global catch-all preventing raw tracebacks or code errors from leaking to API clients."""
194 logger.exception("Unhandled error processing %s %s: %s", request.method, request.path, exc)
195 return api.create_response(
196 request,
197 to_envelope(
198 500,
199 [AppError("SERVER_ERROR", "An unexpected error occurred while processing your request. Please check your input and try again.")],
200 ),
201 status=500,
202 )
205@api.get("/health", tags=["system"])
206def health(request):
207 """Liveness probe and contract-bridge smoke endpoint."""
208 return {"status": "ok", "service": "lumen-backend"}
211api.add_router("/loans", loans_router)
212api.add_router("/loans", financials_router)
213api.add_router("/loans", credit_router)
214api.add_router("/loans", aus_router)
215api.add_router("/loans", condition_router)
216api.add_router("/loans", client_needs_router)
217api.add_router("/loans", pricing_router)
218api.add_router("/loans", fees_router)
219api.add_router("/loans", workflow_router)
220api.add_router("/loans", mismo_router)
221api.add_router("/loans", disclosures_router)
222api.add_router("/loans", loan_custom_fields_router)
223api.add_router("/loans", pos_invite_router)
224api.add_router("/loans", upload_invites_router)
225api.add_router("/workflow", workflow_config_router)
226api.add_router("/accounts", accounts_router)
227api.add_router("/admin", admin_integrations_router)
228api.add_router("/admin", admin_console_router)
229api.add_router("/settings", settings_router)
230api.add_router("/public", public_router)
231api.add_router("/setup", setup_router)
232api.add_router("", tireless_router)
233api.add_router("", dashboard_router)
234api.add_router("", tasks_hub_router)
235api.add_router("", saved_views_router)
236api.add_router("", contacts_router)
237api.add_router("", directory_router)
238api.add_router("", leads_search_router)
239api.add_router("", leads_router)
240api.add_router("", reports_router)
241api.add_router("", report_builder_router)
242api.add_router("", classifications_router)
243api.add_router("", communications_router)
244api.add_router("", webhooks_router)
245api.add_router("", rules_router)
246api.add_router("", custom_fields_router)
247api.add_router("", local_auth_router)
248api.add_router("", borrower_auth_router)
249api.add_router("/pos", pos_router)
250api.add_router("/pos", pos_documents_router)
251api.add_router("", upload_revoke_router)
252api.add_router("", upload_public_router)
253api.add_router("", geocode_router)
254api.add_router("", task_templates_router)
255api.add_router("", compliance_router)
256api.add_router("", billing_router)
257api.add_router("", provision_router)