Coverage for apps/core_los/leads_search_api.py: 100%
22 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"""Client dedup search for the New Loan form: GET /api/leads-search?q= (mounted at "").
3Searches existing loan borrowers only — no Lead model exists yet, so this never
4returns a `kind: "lead"` row (design.md D4, confirmed with the user).
5"""
7from django.db.models import Q
8from ninja import Router, Schema
10from apps.accounts.auth import requires, staff_loan_queryset
11from apps.application_1003.models import Borrower
13router = Router(tags=["leads-search"])
16class ClientMatchOut(Schema):
17 name: str
18 email: str | None
19 kind: str
20 loan_guid: str | None = None
21 loan_number: str | None = None
24@router.get("/leads-search", response=list[ClientMatchOut], auth=requires("LOAN_VIEW"))
25def search_leads(request, q: str = ""):
26 words = [w for w in q.lower().split() if len(w) >= 3]
27 if not words:
28 return []
30 loans = staff_loan_queryset(request.principal, "all")
31 filt = Q()
32 for word in words:
33 filt |= Q(first_name__icontains=word) | Q(last_name__icontains=word) | Q(
34 email__icontains=word
35 )
36 borrowers = (
37 Borrower.objects.filter(loan__in=loans, is_primary=True)
38 .filter(filt)
39 .select_related("loan")
40 )
41 return [
42 {
43 "name": f"{b.first_name} {b.last_name}",
44 "email": b.email or None,
45 "kind": "loan",
46 "loan_guid": str(b.loan.guid),
47 "loan_number": b.loan.arive_loan_no,
48 }
49 for b in borrowers
50 ]