Coverage for apps/client_needs/models.py: 94%

50 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-05 19:46 +0000

1"""Reference-only document records and borrower client-need requests. 

2 

3Per ADR-001 (openspec/DECISIONS.md): ``Document`` stores a REFERENCE (hash, 

4classification, external id), never the file's binary content — real storage and 

5fulfillment land later with the Tireless connection. 

6""" 

7 

8from __future__ import annotations 

9 

10from django.db import models 

11 

12from domain.enums import ClientNeedStatus, ClientNeedType, enum_choices 

13 

14 

15class DocumentClassification(models.Model): 

16 """What a document reference can be classified as — a DB-backed, admin-editable 

17 catalog (add-document-classification-db-config), not a hardcoded list, so staff 

18 can add a new document type without a code change. 

19 

20 ``system`` marks the 152 built-ins seeded by ``seed_document_classifications``; 

21 they can't be deleted (``CLASSIFICATION_PROTECTED``), only deactivated, same as 

22 anything else. ``is_active=False`` removes an entry from the catalog without 

23 ever breaking a ``Document`` row that already references its ``code``. 

24 """ 

25 

26 code = models.CharField(max_length=100, unique=True) 

27 name = models.CharField(max_length=200) 

28 category = models.CharField(max_length=100) 

29 system = models.BooleanField(default=False) 

30 is_active = models.BooleanField(default=True) 

31 created_at = models.DateTimeField(auto_now_add=True) 

32 

33 class Meta: 

34 db_table = "document_classifications" 

35 ordering = ["category", "name"] 

36 

37 def __str__(self) -> str: 

38 return f"{self.code} ({self.category})" 

39 

40 

41class Document(models.Model): 

42 """A reference to a document — classification + hash + optional external id, no blob.""" 

43 

44 loan = models.ForeignKey("core_los.Loan", on_delete=models.CASCADE, related_name="documents") 

45 classification = models.CharField(max_length=100) 

46 content_hash = models.CharField(max_length=128) 

47 external_doc_id = models.CharField(max_length=100, null=True, blank=True) 

48 # The file name the borrower or staff gave, and who supplied the document 

49 # (add-borrower-portal-api design.md D12). Still a reference only — no bytes. 

50 name = models.CharField(max_length=255, blank=True, default="") 

51 UPLOADED_VIA = [("staff", "Staff"), ("invite", "Upload link"), ("portal", "Borrower portal")] 

52 uploaded_via = models.CharField(max_length=10, choices=UPLOADED_VIA, default="staff") 

53 reviewed = models.BooleanField(default=False) 

54 private = models.BooleanField(default=False) 

55 created_at = models.DateTimeField(auto_now_add=True) 

56 

57 class Meta: 

58 db_table = "client_needs_documents" 

59 

60 def __str__(self) -> str: 

61 return f"Document {self.pk} ({self.classification})" 

62 

63 

64class ClientNeed(models.Model): 

65 """A document/signature request made of a loan's borrowers.""" 

66 

67 loan = models.ForeignKey("core_los.Loan", on_delete=models.CASCADE, related_name="needs") 

68 name = models.CharField(max_length=200) 

69 type = models.CharField(max_length=20, choices=enum_choices(ClientNeedType)) 

70 borrowers = models.ManyToManyField("application_1003.Borrower", related_name="client_needs") 

71 status = models.CharField( 

72 max_length=20, 

73 choices=enum_choices(ClientNeedStatus), 

74 default=ClientNeedStatus.REQUESTED.value, 

75 ) 

76 required = models.BooleanField(default=True) 

77 description = models.TextField(blank=True) 

78 due_date = models.DateField(null=True, blank=True) 

79 document = models.ForeignKey( 

80 Document, on_delete=models.SET_NULL, null=True, blank=True, related_name="fulfilled_needs" 

81 ) 

82 created_at = models.DateTimeField(auto_now_add=True) 

83 updated_at = models.DateTimeField(auto_now=True) 

84 

85 class Meta: 

86 db_table = "client_needs" 

87 

88 def __str__(self) -> str: 

89 return f"{self.name} ({self.status})" 

90 

91 def fulfill_with(self, document: Document) -> None: 

92 self.document = document 

93 self.status = ClientNeedStatus.RECEIVED.value 

94 self.save(update_fields=["document", "status", "updated_at"])