Coverage for apps/underwriting/api.py: 90%
55 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"""Credit router: order credit, scores, liability import (mounted under /api/loans)."""
3from uuid import UUID
5from django.db import transaction as db_transaction
6from ninja import Router
8from apps.accounts.auth import ensure_staff_scope, requires
9from apps.application_1003.models import Liability
10from apps.core_los.models import Loan
11from apps.underwriting.models import CreditOrder, CreditScore
12from apps.underwriting.providers import get_credit_provider
13from apps.underwriting.schemas import (
14 CreditOrderCreate,
15 CreditOrderOut,
16 CreditScoreOut,
17 ImportLiabilitiesIn,
18 ImportLiabilitiesOut,
19)
20from domain.result import AppError, DomainError
22router = Router(tags=["credit"])
25def _scoped_loan(request, guid: UUID) -> Loan:
26 loan = Loan.objects.filter(guid=guid).first()
27 if loan is None: 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true
28 raise DomainError([AppError("NOT_FOUND", "Loan not found")])
29 ensure_staff_scope(request.principal, loan)
30 return loan
33def _order_payload(order: CreditOrder) -> dict:
34 # Scalar order fields from model_dump; nested scores as ORM objects so Ninja
35 # serializes them once via CreditScoreOut (double-dumping drops the FK alias).
36 data = CreditOrderOut.from_orm(order).model_dump()
37 data["provider_status"] = "live"
38 data["scores"] = list(order.scores.select_related("borrower").all())
39 return data
42@router.post("/{guid}/credit-order", response={201: CreditOrderOut}, auth=requires("LOAN_EDIT"))
43def order_credit(request, guid: UUID, payload: CreditOrderCreate):
44 loan = _scoped_loan(request, guid)
45 borrowers = list(loan.borrowers.all())
46 provider = get_credit_provider(org=loan.org_unit)
47 with db_transaction.atomic():
48 order = CreditOrder.objects.create(
49 loan=loan,
50 credit_company_id=payload.credit_company_id,
51 request_type=payload.request_type,
52 status="Ordered",
53 )
54 result = provider.order(loan, borrowers, payload.request_type)
55 order.report_id = result.report_id
56 order.status = "Completed"
57 order.save(update_fields=["report_id", "status"])
58 for score in result.scores:
59 CreditScore.objects.create(
60 order=order,
61 borrower_id=score.borrower_id,
62 equifax=score.equifax,
63 experian=score.experian,
64 transunion=score.transunion,
65 mid=score.mid,
66 )
67 for tl in result.tradelines:
68 liability = Liability.objects.create(
69 loan=loan,
70 liability_type=tl.liability_type,
71 creditor_name=tl.creditor_name,
72 monthly_payment_amt=tl.monthly_payment_amt,
73 unpaid_balance_amt=tl.unpaid_balance_amt,
74 )
75 liability.borrowers.set([tl.borrower_id])
76 if not loan.credit_pulled: 76 ↛ 79line 76 didn't jump to line 79
77 loan.credit_pulled = True
78 loan.save(update_fields=["credit_pulled"])
79 return 201, _order_payload(order)
82@router.get("/{guid}/credit-order", response=list[CreditOrderOut], auth=requires("LOAN_VIEW"))
83def list_credit_orders(request, guid: UUID):
84 loan = _scoped_loan(request, guid)
85 return [_order_payload(o) for o in loan.credit_orders.all().order_by("-created_at")]
88@router.get("/{guid}/credit-score", response=list[CreditScoreOut], auth=requires("LOAN_VIEW"))
89def list_credit_scores(request, guid: UUID):
90 loan = _scoped_loan(request, guid)
91 return list(CreditScore.objects.filter(order__loan=loan))
94@router.post(
95 "/{guid}/import-liabilities", response=ImportLiabilitiesOut, auth=requires("LOAN_EDIT")
96)
97def import_liabilities(request, guid: UUID, payload: ImportLiabilitiesIn):
98 loan = _scoped_loan(request, guid)
99 count = loan.liabilities.count()
100 return {"imported": count}