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

1"""Client dedup search for the New Loan form: GET /api/leads-search?q= (mounted at ""). 

2 

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""" 

6 

7from django.db.models import Q 

8from ninja import Router, Schema 

9 

10from apps.accounts.auth import requires, staff_loan_queryset 

11from apps.application_1003.models import Borrower 

12 

13router = Router(tags=["leads-search"]) 

14 

15 

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 

22 

23 

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 [] 

29 

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 ]