Coverage for apps/pricing_ppe/schemas.py: 100%
96 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 14:06 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-04 14:06 +0000
1"""Ninja schemas for the Product & Pricing Engine API.
3Priced output mirrors the pure-domain dataclasses in ``domain/pricing.py`` (served via
4pydantic ``from_attributes``); ``RateLock`` read/write are ``ModelSchema`` over the ORM row.
5Money fields are plain ``Decimal`` — pydantic renders them as exact strings in JSON.
6"""
8from __future__ import annotations
10from decimal import Decimal
12from ninja import ModelSchema, Schema
14from apps.pricing_ppe.models import RateLock
17class ProductRefOut(Schema):
18 id: str
19 investor: str
20 name: str
21 mortgage_type: str
22 term_months: int
23 base_rate: Decimal
24 base_price: Decimal
25 min_fico: int
26 max_ltv: Decimal
29class AdjustmentOut(Schema):
30 code: str
31 description: str
32 adjustment_type: str
33 amount: Decimal
36class ClosingCostItemOut(Schema):
37 label: str
38 amount: Decimal
41class ClosingCostOut(Schema):
42 origination_amt: Decimal
43 third_party_amt: Decimal
44 prepaids_amt: Decimal
45 total: Decimal
46 items: list[ClosingCostItemOut] = []
49class LoCompDetailOut(Schema):
50 comp_type: str
51 pct: Decimal
52 amt: Decimal
55class RateStackItemOut(Schema):
56 rate: Decimal
57 price: Decimal
58 points_pct: Decimal
61class PricedProductOut(Schema):
62 product: ProductRefOut
63 rate: Decimal
64 price: Decimal
65 pi: Decimal
66 apr: Decimal
67 piti: Decimal
68 lo_comp: Decimal
69 lo_comp_detail: LoCompDetailOut
70 closing_costs: ClosingCostOut
71 adjustments: list[AdjustmentOut] = []
72 rate_stack: list[RateStackItemOut] = []
73 lock_period_days: int
74 rate_sheet_id: str
75 eligible: bool
76 reasons: list[str] = []
79class IneligibleProductOut(Schema):
80 product_id: str
81 product_name: str
82 reasons: list[str] = []
85class PricingResultOut(Schema):
86 count: int
87 products: list[PricedProductOut] = []
88 ineligible_products: list[IneligibleProductOut] = []
91class RecommendationOut(Schema):
92 best: PricedProductOut
93 rationale: str
94 alternatives: list[PricedProductOut] = []
97class PricingResponseOut(PricingResultOut):
98 """Quote response: a pricing result plus an optional agentic recommendation."""
100 recommendation: RecommendationOut | None = None
103class QuoteIn(Schema):
104 """All-optional what-if overrides; any field left unset falls back to the loan."""
106 fico: int | None = None
107 loan_amount: Decimal | None = None
108 property_value: Decimal | None = None
109 occupancy: str | None = None
110 term_months: int | None = None
111 monthly_taxes: Decimal | None = None
112 monthly_hazard: Decimal | None = None
113 monthly_hoa: Decimal | None = None
114 monthly_mi: Decimal | None = None
115 goal: str | None = None
118class RateLockOut(ModelSchema):
119 rate: Decimal | None = None
120 product_id: int | None = None
122 class Meta:
123 model = RateLock
124 exclude = ["loan"]
126 @staticmethod
127 def resolve_rate(obj):
128 return obj.locked_rate
130 @staticmethod
131 def resolve_product_id(obj):
132 return obj.product_id
135class RateLockReadOut(Schema):
136 """Wrapper so the read endpoint can return 200 with a null lock."""
138 lock: RateLockOut | None = None
141class RateLockCreate(Schema):
142 product_id: int
143 rate: Decimal | None = None
144 price: Decimal | None = None
145 lock_period_days: int | None = None