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
« 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.
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"""
8from __future__ import annotations
10from django.db import models
12from domain.enums import ClientNeedStatus, ClientNeedType, enum_choices
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.
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 """
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)
33 class Meta:
34 db_table = "document_classifications"
35 ordering = ["category", "name"]
37 def __str__(self) -> str:
38 return f"{self.code} ({self.category})"
41class Document(models.Model):
42 """A reference to a document — classification + hash + optional external id, no blob."""
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)
55 class Meta:
56 db_table = "client_needs_documents"
58 def __str__(self) -> str:
59 return f"Document {self.pk} ({self.classification})"
62class ClientNeed(models.Model):
63 """A document/signature request made of a loan's borrowers."""
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)
83 class Meta:
84 db_table = "client_needs"
86 def __str__(self) -> str:
87 return f"{self.name} ({self.status})"
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"])