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
« 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.
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 reviewed = models.BooleanField(default=False)
54 private = models.BooleanField(default=False)
55 created_at = models.DateTimeField(auto_now_add=True)
57 class Meta:
58 db_table = "client_needs_documents"
60 def __str__(self) -> str:
61 return f"Document {self.pk} ({self.classification})"
64class ClientNeed(models.Model):
65 """A document/signature request made of a loan's borrowers."""
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)
85 class Meta:
86 db_table = "client_needs"
88 def __str__(self) -> str:
89 return f"{self.name} ({self.status})"
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"])