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

68 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-05 19:46 +0000

1"""Django models for the Product & Pricing Engine (PPE) and Rate Locks (spec §16.2).""" 

2 

3from __future__ import annotations 

4 

5from decimal import Decimal 

6 

7from django.db import models 

8 

9from domain.enums import ( 

10 MortgageType, 

11 comp_type_choices, 

12) 

13from domain.pricing import RateProductInput 

14 

15 

16class RateProduct(models.Model): 

17 """Investor mortgage program with base pricing and eligibility criteria.""" 

18 

19 MORTGAGE_CHOICES = [(m.value, m.value) for m in MortgageType] 

20 COMP_CHOICES = comp_type_choices() 

21 

22 investor = models.CharField(max_length=100) 

23 name = models.CharField(max_length=150) 

24 mortgage_type = models.CharField(max_length=30, choices=MORTGAGE_CHOICES) 

25 term_months = models.PositiveIntegerField(default=360) 

26 base_rate = models.DecimalField(max_digits=5, decimal_places=3) 

27 base_price = models.DecimalField(max_digits=6, decimal_places=3) 

28 min_fico = models.PositiveIntegerField(default=620) 

29 max_ltv = models.DecimalField(max_digits=5, decimal_places=2) 

30 occupancies = models.JSONField( 

31 default=list, 

32 help_text="Allowed occupancy types e.g. ['PrimaryResidence', 'SecondHome']", 

33 ) 

34 comp_type = models.CharField(max_length=20, choices=COMP_CHOICES, default="LenderPaid") 

35 comp_pct = models.DecimalField(max_digits=4, decimal_places=2, default=Decimal("1.25")) 

36 is_active = models.BooleanField(default=True) 

37 created_at = models.DateTimeField(auto_now_add=True) 

38 updated_at = models.DateTimeField(auto_now=True) 

39 

40 class Meta: 

41 db_table = "rate_products" 

42 ordering = ["investor", "base_rate"] 

43 indexes = [ 

44 models.Index(fields=["is_active", "mortgage_type"]), 

45 ] 

46 

47 def __str__(self) -> str: 

48 return f"{self.investor} - {self.name} ({self.base_rate}%)" 

49 

50 def to_domain(self) -> RateProductInput: 

51 """Convert ORM record into pure domain RateProductInput.""" 

52 return RateProductInput( 

53 id=str(self.id), 

54 investor=self.investor, 

55 name=self.name, 

56 mortgage_type=self.mortgage_type, 

57 term_months=self.term_months, 

58 base_rate=self.base_rate, 

59 base_price=self.base_price, 

60 min_fico=self.min_fico, 

61 max_ltv=self.max_ltv, 

62 occupancies=tuple(self.occupancies), 

63 comp_type=self.comp_type, 

64 comp_pct=self.comp_pct, 

65 ) 

66 

67 

68class RateLock(models.Model): 

69 """Locked rate agreement on a specific loan.""" 

70 

71 LOCK_STATUS_CHOICES = [ 

72 ("LOCKED", "Locked"), 

73 ("EXPIRED", "Expired"), 

74 ("FLOATED", "Floated"), 

75 ("RELOCKED", "Re-locked"), 

76 ] 

77 

78 loan = models.OneToOneField( 

79 "core_los.Loan", 

80 on_delete=models.CASCADE, 

81 related_name="rate_lock", 

82 ) 

83 product = models.ForeignKey( 

84 RateProduct, 

85 on_delete=models.PROTECT, 

86 related_name="locks", 

87 ) 

88 locked_rate = models.DecimalField(max_digits=5, decimal_places=3) 

89 locked_price = models.DecimalField(max_digits=6, decimal_places=3) 

90 lock_period_days = models.PositiveIntegerField(default=30) 

91 locked_at = models.DateTimeField(auto_now_add=True) 

92 expires_at = models.DateTimeField() 

93 lo_comp_amt = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal("0.00")) 

94 lo_comp_type = models.CharField(max_length=20, default="LenderPaid") 

95 status = models.CharField(max_length=20, choices=LOCK_STATUS_CHOICES, default="LOCKED") 

96 created_at = models.DateTimeField(auto_now_add=True) 

97 updated_at = models.DateTimeField(auto_now=True) 

98 

99 class Meta: 

100 db_table = "rate_locks" 

101 ordering = ["-locked_at"] 

102 

103 def __str__(self) -> str: 

104 return f"Lock on Loan #{self.loan_id}: {self.locked_rate}% ({self.status})" 

105 

106 

107class Quote(models.Model): 

108 """Saved pricing quote or competing offer entered for a loan.""" 

109 

110 loan = models.ForeignKey( 

111 "core_los.Loan", 

112 on_delete=models.CASCADE, 

113 related_name="quotes", 

114 ) 

115 product_id = models.CharField(max_length=100) 

116 product_name = models.CharField(max_length=150) 

117 investor = models.CharField(max_length=100) 

118 rate = models.DecimalField(max_digits=5, decimal_places=3) 

119 price = models.DecimalField(max_digits=6, decimal_places=3) 

120 pi = models.DecimalField(max_digits=10, decimal_places=2) 

121 eligible = models.BooleanField(default=True) 

122 competing = models.BooleanField(default=False) 

123 note = models.CharField(max_length=255, blank=True, default="") 

124 created_at = models.DateTimeField(auto_now_add=True) 

125 updated_at = models.DateTimeField(auto_now=True) 

126 

127 class Meta: 

128 db_table = "quotes" 

129 ordering = ["rate", "-created_at"] 

130 

131 def __str__(self) -> str: 

132 tag = " [competing]" if self.competing else "" 

133 return f"Quote #{self.id} on Loan #{self.loan_id}: {self.product_name} @ {self.rate}%{tag}" 

134