Coverage for domain/disclosures.py: 88%

33 statements  

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

1"""Disclosure package ordering & sequential status gating (spec §13.3–§13.5). 

2 

3Enforces federal TRID sequencing: 

4- Initial Disclosures (Loan Estimate) before Closing Disclosures (CD). 

5- Closing Disclosure requires the loan to be at or after 'APPROVED_WITH_CONDITION'. 

6- Closing Package requires the loan to be at or after 'CLEAR_TO_CLOSE'. 

7""" 

8 

9from __future__ import annotations 

10 

11from collections.abc import Sequence 

12from typing import Any 

13 

14from domain.result import AppError, Result, err, ok 

15 

16_STAGE_ORDER: tuple[str, ...] = ( 

17 "APPLICATION_INTAKE", 

18 "PREAPPROVED", 

19 "QUALIFICATION", 

20 "LOAN_SETUP", 

21 "DISCLOSURE_SENT", 

22 "UNDERWRITING_SUBMITTED", 

23 "APPROVED_WITH_CONDITION", 

24 "RE_SUBMITTAL", 

25 "CLEAR_TO_CLOSE", 

26 "DOCS_OUT", 

27 "DOCS_SIGNED", 

28 "LOAN_FUNDED", 

29) 

30 

31 

32def _is_at_or_after(loan_stage: str, target_stage: str) -> bool: 

33 try: 

34 idx_current = _STAGE_ORDER.index(loan_stage) 

35 idx_target = _STAGE_ORDER.index(target_stage) 

36 return idx_current >= idx_target 

37 except ValueError: 

38 return False 

39 

40 

41def _get_field(obj: Any, key: str, default: Any = None) -> Any: 

42 if isinstance(obj, dict): 42 ↛ 44line 42 didn't jump to line 44 because the condition on line 42 was always true

43 return obj.get(key, default) 

44 return getattr(obj, key, default) 

45 

46 

47def can_generate_package( 

48 loan_stage: str, 

49 existing_packages: Sequence[Any], 

50 package_type: str, 

51) -> Result: 

52 """Validate whether a disclosure package can be legally and operationally generated. 

53 

54 Returns ``ok(True)`` on success, or ``err(AppError(...))`` with error codes 

55 ``DISC_ORDER`` or ``DISC_STAGE``. 

56 """ 

57 has_initials = any( 

58 _get_field(p, "type", _get_field(p, "package_type")) in ("LoanEstimate", "InitialDisclosure") 

59 and _get_field(p, "status") != "Draft" 

60 for p in existing_packages 

61 ) 

62 

63 if package_type in ("PreApproval", "ChangeOfCircumstance", "LoanEstimate", "InitialDisclosure"): 

64 return ok(True) 

65 

66 if package_type == "ClosingDisclosure": 

67 if not has_initials: 

68 return err( 

69 AppError( 

70 code="DISC_ORDER", 

71 message="Generate Initial Disclosures before a Closing Disclosure.", 

72 ) 

73 ) 

74 if loan_stage not in ("APPROVED_WITH_CONDITION", "RE_SUBMITTAL") and not _is_at_or_after( 

75 loan_stage, "APPROVED_WITH_CONDITION" 

76 ): 

77 return err( 

78 AppError( 

79 code="DISC_STAGE", 

80 message='Loan must be "Approved w/ Conditions" before a Closing Disclosure.', 

81 ) 

82 ) 

83 return ok(True) 

84 

85 if package_type == "ClosingPackage": 85 ↛ 102line 85 didn't jump to line 102 because the condition on line 85 was always true

86 if not has_initials: 

87 return err( 

88 AppError( 

89 code="DISC_ORDER", 

90 message="Generate Initial Disclosures before a Closing Package.", 

91 ) 

92 ) 

93 if not _is_at_or_after(loan_stage, "CLEAR_TO_CLOSE") and loan_stage != "CLEAR_TO_CLOSE": 

94 return err( 

95 AppError( 

96 code="DISC_STAGE", 

97 message='Loan must be "Clear to Close" before a Closing Package.', 

98 ) 

99 ) 

100 return ok(True) 

101 

102 return ok(True)