Coverage for apps/fees_trid/models.py: 97%

36 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-04 14:06 +0000

1"""Django models for TRID fees and closing costs (spec §16.1).""" 

2 

3from __future__ import annotations 

4 

5from decimal import Decimal 

6 

7from django.db import models 

8 

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 

19 

20 

21class Fee(models.Model): 

22 """A single closing fee item associated with a Loan (TRID LE/CD).""" 

23 

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() 

28 

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) 

62 

63 class Meta: 

64 db_table = "fees" 

65 indexes = [ 

66 models.Index(fields=["loan", "section"]), 

67 ] 

68 ordering = ["section", "id"] 

69 

70 def __str__(self) -> str: 

71 return f"[{self.section}] {self.name} (${self.total_amt})" 

72 

73 @property 

74 def description(self) -> str: 

75 """Alias for name for parity with arive-spec.""" 

76 return self.name 

77 

78 @property 

79 def effective_tolerance_bucket(self) -> ToleranceBucket: 

80 """Resolved tolerance bucket (explicit or section default).""" 

81 return bucket_of(self) 

82 

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)