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

48 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-04 14:06 +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 created_at = models.DateTimeField(auto_now_add=True) 

54 

55 class Meta: 

56 db_table = "client_needs_documents" 

57 

58 def __str__(self) -> str: 

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

60 

61 

62class ClientNeed(models.Model): 

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

64 

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

66 name = models.CharField(max_length=200) 

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

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

69 status = models.CharField( 

70 max_length=20, 

71 choices=enum_choices(ClientNeedStatus), 

72 default=ClientNeedStatus.REQUESTED.value, 

73 ) 

74 required = models.BooleanField(default=True) 

75 description = models.TextField(blank=True) 

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

77 document = models.ForeignKey( 

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

79 ) 

80 created_at = models.DateTimeField(auto_now_add=True) 

81 updated_at = models.DateTimeField(auto_now=True) 

82 

83 class Meta: 

84 db_table = "client_needs" 

85 

86 def __str__(self) -> str: 

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

88 

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

90 self.document = document 

91 self.status = ClientNeedStatus.RECEIVED.value 

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