Coverage for apps/underwriting/aus_conditions.py: 98%

38 statements  

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

1"""Import an AUS run's findings into the loan's conditions (add-aus-preflight-and-condition-import). 

2 

3A finding is "on file" when a condition on the loan has the same name, ignoring case and 

4spacing (``domain.aus.normalize_condition_text``). The same rule drives the import's skip logic 

5and ``AusRunOut.importable_count``, so the button state and the server always agree. 

6""" 

7 

8from __future__ import annotations 

9 

10from django.db import transaction 

11 

12from apps.core_los.models import Loan 

13from apps.underwriting.models import AusRun, Condition 

14from domain.aus import normalize_condition_text 

15from domain.enums import ConditionSource, ConditionStatus, PriorToCategory 

16 

17NAME_MAX = Condition._meta.get_field("name").max_length 

18ENGINE_LABEL = {"du": "DU", "lpa": "LPA"} 

19 

20 

21def condition_name_for(finding: str) -> str: 

22 """The condition name an imported finding gets: single-spaced and cut to the column size.""" 

23 return " ".join((finding or "").split())[:NAME_MAX] 

24 

25 

26def _key(finding: str) -> str: 

27 return normalize_condition_text(condition_name_for(finding)) 

28 

29 

30def names_on_file(loan_id: int) -> set[str]: 

31 """Normalized names of every condition already on the loan (imported or hand-added).""" 

32 return { 

33 normalize_condition_text(n) 

34 for n in Condition.objects.filter(loan_id=loan_id).values_list("name", flat=True) 

35 } 

36 

37 

38def pending_findings(run: AusRun, on_file: set[str]) -> list[str]: 

39 """The run's findings not yet on the loan file, without repeats, in their original order.""" 

40 seen = set(on_file) 

41 pending = [] 

42 for finding in run.findings_conditions or []: 

43 key = _key(finding) 

44 if not key or key in seen: 

45 continue 

46 seen.add(key) 

47 pending.append(finding) 

48 return pending 

49 

50 

51def import_findings(loan: Loan, run: AusRun) -> tuple[list[Condition], int]: 

52 """Create a condition for each finding not yet on file. Returns (created, skipped count). 

53 

54 The loan row is locked for the duration, so a double-click (two concurrent imports) is 

55 serialized and the second one sees the first one's conditions. 

56 """ 

57 findings = [f for f in (run.findings_conditions or []) if condition_name_for(f)] 

58 with transaction.atomic(): 

59 Loan.objects.select_for_update().filter(pk=loan.pk).first() 

60 pending = pending_findings(run, names_on_file(loan.pk)) 

61 source = f"Imported from {ENGINE_LABEL.get(run.engine, run.engine.upper())} findings" 

62 if run.casefile_id: 62 ↛ 64line 62 didn't jump to line 64 because the condition on line 62 was always true

63 source += f" · casefile {run.casefile_id}" 

64 created = [] 

65 for finding in pending: 

66 name = condition_name_for(finding) 

67 full = " ".join(finding.split()) 

68 created.append( 

69 Condition.objects.create( 

70 loan=loan, 

71 name=name, 

72 description=source if full == name else f"{source}\n\n{full}", 

73 category=PriorToCategory.PRIOR_TO_DOCS.value, 

74 status=ConditionStatus.ADDED.value, 

75 source=ConditionSource.UW.value, 

76 ) 

77 ) 

78 return created, len(findings) - len(created)