Coverage for apps/mismo/api.py: 95%

37 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-04 17:24 +0000

1"""MISMO export router: pre-export readiness + the MISMO 3.4 XML download (under /loans).""" 

2 

3from uuid import UUID 

4 

5from django.http import HttpResponse 

6from ninja import Router 

7 

8from apps.accounts.auth import ensure_staff_scope, requires 

9from apps.application_1003.models import Declaration, Demographics, SubjectProperty 

10from apps.core_los.models import Loan 

11from apps.mismo.generator import TARGETS, generate_mismo_xml 

12from apps.mismo.schemas import MismoReadinessOut 

13from domain.mismo_readiness import ( 

14 MismoBorrowerReadiness, 

15 MismoReadinessContext, 

16 check_mismo_readiness, 

17) 

18from domain.result import AppError, DomainError 

19 

20router = Router(tags=["mismo"]) 

21 

22 

23def _scoped_loan(request, guid: UUID) -> Loan: 

24 loan = Loan.objects.filter(guid=guid).first() 

25 if loan is None: 25 ↛ 26line 25 didn't jump to line 26 because the condition on line 25 was never true

26 raise DomainError([AppError("NOT_FOUND", "Loan not found")]) 

27 ensure_staff_scope(request.principal, loan) 

28 return loan 

29 

30 

31def _build_readiness_ctx(loan: Loan) -> MismoReadinessContext: 

32 borrowers = [ 

33 MismoBorrowerReadiness( 

34 first_name=b.first_name, 

35 last_name=b.last_name, 

36 has_declarations=Declaration.objects.filter(borrower=b).exists(), 

37 has_demographics=Demographics.objects.filter(borrower=b).exists(), 

38 has_current_residence=b.residences.filter(residence_type="Current").exists(), 

39 ) 

40 for b in loan.borrowers.all() 

41 ] 

42 return MismoReadinessContext( 

43 borrowers=borrowers, 

44 has_subject_property=SubjectProperty.objects.filter(loan=loan).exists(), 

45 has_down_payment_source=loan.down_payment_sources.exists(), 

46 has_loan_identifier=loan.loan_identifiers.filter( 

47 identifier_type="UniversalLoanIdentifier" 

48 ).exists(), 

49 ) 

50 

51 

52def _readiness_payload(readiness) -> dict: 

53 return { 

54 "findings": [ 

55 {"bucket": f.bucket, "code": f.code, "message": f.message} 

56 for f in readiness.findings 

57 ] 

58 } 

59 

60 

61@router.get("/{guid}/mismo/readiness", response=MismoReadinessOut, auth=requires("LOAN_VIEW")) 

62def mismo_readiness(request, guid: UUID): 

63 loan = _scoped_loan(request, guid) 

64 return _readiness_payload(check_mismo_readiness(_build_readiness_ctx(loan))) 

65 

66 

67@router.get("/{guid}/mismo", auth=requires("LOAN_VIEW")) 

68def mismo_export(request, guid: UUID, target: str = "GENERIC"): 

69 loan = _scoped_loan(request, guid) 

70 target = target.upper() 

71 if target not in TARGETS: 

72 raise DomainError( 

73 [AppError("VALIDATION", f"Unknown MISMO export target: {target!r}")], 

74 status_code=422, 

75 ) 

76 xml_bytes = generate_mismo_xml(loan, target=target) 

77 response = HttpResponse(xml_bytes, content_type="application/xml") 

78 filename = f"{loan.arive_loan_no}-{target.lower()}.xml" 

79 response["Content-Disposition"] = f'attachment; filename="{filename}"' 

80 return response