Coverage for apps/fees_trid/models.py: 97%
36 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 17:24 +0000
1"""Django models for TRID fees and closing costs (spec §16.1)."""
3from __future__ import annotations
5from decimal import Decimal
7from django.db import models
9from domain.enums import (
10 PaidByType,
11 PaidToType,
12 ToleranceBucket,
13 fee_section_choices,
14 paid_by_choices,
15 paid_to_choices,
16 tolerance_bucket_choices,
17)
18from domain.fees import bucket_of
21class Fee(models.Model):
22 """A single closing fee item associated with a Loan (TRID LE/CD)."""
24 SECTION_CHOICES = fee_section_choices()
25 TOLERANCE_CHOICES = tolerance_bucket_choices()
26 PAID_BY_CHOICES = paid_by_choices()
27 PAID_TO_CHOICES = paid_to_choices()
29 loan = models.ForeignKey(
30 "core_los.Loan",
31 on_delete=models.CASCADE,
32 related_name="fees",
33 )
34 section = models.CharField(max_length=2, choices=SECTION_CHOICES)
35 name = models.CharField(max_length=255)
36 total_amt = models.DecimalField(
37 max_digits=12,
38 decimal_places=2,
39 default=Decimal("0.00"),
40 )
41 tolerance_bucket = models.CharField(
42 max_length=20,
43 choices=TOLERANCE_CHOICES,
44 blank=True,
45 )
46 paid_by = models.CharField(
47 max_length=20,
48 choices=PAID_BY_CHOICES,
49 default=PaidByType.BORROWER.value,
50 )
51 paid_to = models.CharField(
52 max_length=20,
53 choices=PAID_TO_CHOICES,
54 default=PaidToType.THIRD_PARTY.value,
55 )
56 poc_ind = models.BooleanField(
57 default=False,
58 help_text="Paid Outside of Closing (excluded from tolerance calculation)",
59 )
60 created_at = models.DateTimeField(auto_now_add=True)
61 updated_at = models.DateTimeField(auto_now=True)
63 class Meta:
64 db_table = "fees"
65 indexes = [
66 models.Index(fields=["loan", "section"]),
67 ]
68 ordering = ["section", "id"]
70 def __str__(self) -> str:
71 return f"[{self.section}] {self.name} (${self.total_amt})"
73 @property
74 def description(self) -> str:
75 """Alias for name for parity with arive-spec."""
76 return self.name
78 @property
79 def effective_tolerance_bucket(self) -> ToleranceBucket:
80 """Resolved tolerance bucket (explicit or section default)."""
81 return bucket_of(self)
83 def save(self, *args, **kwargs) -> None:
84 if not self.tolerance_bucket:
85 self.tolerance_bucket = self.effective_tolerance_bucket.value
86 super().save(*args, **kwargs)