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
« 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)."""
3from __future__ import annotations
5from decimal import Decimal
7from django.db import models
9from domain.enums import (
10 MortgageType,
11 comp_type_choices,
12)
13from domain.pricing import RateProductInput
16class RateProduct(models.Model):
17 """Investor mortgage program with base pricing and eligibility criteria."""
19 MORTGAGE_CHOICES = [(m.value, m.value) for m in MortgageType]
20 COMP_CHOICES = comp_type_choices()
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)
40 class Meta:
41 db_table = "rate_products"
42 ordering = ["investor", "base_rate"]
43 indexes = [
44 models.Index(fields=["is_active", "mortgage_type"]),
45 ]
47 def __str__(self) -> str:
48 return f"{self.investor} - {self.name} ({self.base_rate}%)"
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 )
68class RateLock(models.Model):
69 """Locked rate agreement on a specific loan."""
71 LOCK_STATUS_CHOICES = [
72 ("LOCKED", "Locked"),
73 ("EXPIRED", "Expired"),
74 ("FLOATED", "Floated"),
75 ("RELOCKED", "Re-locked"),
76 ]
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)
99 class Meta:
100 db_table = "rate_locks"
101 ordering = ["-locked_at"]
103 def __str__(self) -> str:
104 return f"Lock on Loan #{self.loan_id}: {self.locked_rate}% ({self.status})"
107class Quote(models.Model):
108 """Saved pricing quote or competing offer entered for a loan."""
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)
127 class Meta:
128 db_table = "quotes"
129 ordering = ["rate", "-created_at"]
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}"