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
« 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).
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"""
8from __future__ import annotations
10from django.db import transaction
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
17NAME_MAX = Condition._meta.get_field("name").max_length
18ENGINE_LABEL = {"du": "DU", "lpa": "LPA"}
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]
26def _key(finding: str) -> str:
27 return normalize_condition_text(condition_name_for(finding))
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 }
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
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).
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)