Coverage for apps/fees_trid/schemas.py: 95%
78 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"""Ninja schemas for the TRID fees / tolerance / QM API.
3``FeeOut`` is a ``ModelSchema`` over the ORM row; summary / tolerance / QM outputs are
4hand-written ``Schema``s over the pure-domain results. Money is plain ``Decimal`` (rendered
5as exact strings by pydantic).
6"""
8from __future__ import annotations
10from decimal import Decimal
12from ninja import ModelSchema, Schema
13from pydantic import field_validator
15from apps.fees_trid.models import Fee
17VALID_FEE_SECTIONS = frozenset(["A", "B", "C", "E", "F", "G", "H"])
20class FeeOut(ModelSchema):
21 class Meta:
22 model = Fee
23 exclude = ["loan"]
26class FeeIn(Schema):
27 section: str
28 name: str
29 total_amt: Decimal
30 tolerance_bucket: str | None = None
31 paid_by: str = "Borrower"
32 paid_to: str = "ThirdParty"
33 poc_ind: bool = False
35 @field_validator("name")
36 @classmethod
37 def validate_name(cls, v: str) -> str:
38 clean = v.strip() if v else ""
39 if not clean:
40 raise ValueError("Fee name cannot be blank")
41 if len(clean) > 255: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true
42 raise ValueError("Fee name cannot exceed 255 characters")
43 return clean
45 @field_validator("total_amt")
46 @classmethod
47 def validate_total_amt(cls, v: Decimal) -> Decimal:
48 if v < Decimal("0.00"):
49 raise ValueError("Fee total amount cannot be negative")
50 if v > Decimal("999999999.99"): 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 raise ValueError("Fee total amount cannot exceed $999,999,999.99")
52 return v
54 @field_validator("section")
55 @classmethod
56 def validate_section(cls, v: str) -> str:
57 s = v.strip().upper() if v else ""
58 if s not in VALID_FEE_SECTIONS:
59 raise ValueError(
60 f"Invalid TRID fee section: '{s}'. Expected one of {sorted(VALID_FEE_SECTIONS)}"
61 )
62 return s
65class FeeSetIn(Schema):
66 fees: list[FeeIn] = []
69class BucketTotalOut(Schema):
70 bucket: str
71 total: Decimal
74class SectionTotalOut(Schema):
75 section: str
76 total: Decimal
79class FeeSummaryOut(Schema):
80 by_bucket: list[BucketTotalOut]
81 by_section: list[SectionTotalOut]
82 grand_total: Decimal
83 poc_total: Decimal
86class BucketCureOut(Schema):
87 bucket: str
88 baseline: Decimal
89 revised: Decimal
90 variance: Decimal
91 cure: Decimal
94class ToleranceOut(Schema):
95 by_bucket: list[BucketCureOut]
96 total_cure: Decimal
99class QmFindingOut(Schema):
100 code: str
101 severity: str
102 message: str
105class QmResultOut(Schema):
106 loan_amount: Decimal
107 points_and_fees: Decimal
108 threshold: Decimal
109 pct: Decimal
110 passed: bool
111 qm_type: str
112 apr_spread: Decimal
113 findings: list[QmFindingOut] = []
114 points_and_fees_source: str