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

1"""Ninja schemas for the Product & Pricing Engine API. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10from decimal import Decimal 

11 

12from ninja import ModelSchema, Schema 

13 

14from apps.pricing_ppe.models import RateLock 

15 

16 

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 

27 

28 

29class AdjustmentOut(Schema): 

30 code: str 

31 description: str 

32 adjustment_type: str 

33 amount: Decimal 

34 

35 

36class ClosingCostItemOut(Schema): 

37 label: str 

38 amount: Decimal 

39 

40 

41class ClosingCostOut(Schema): 

42 origination_amt: Decimal 

43 third_party_amt: Decimal 

44 prepaids_amt: Decimal 

45 total: Decimal 

46 items: list[ClosingCostItemOut] = [] 

47 

48 

49class LoCompDetailOut(Schema): 

50 comp_type: str 

51 pct: Decimal 

52 amt: Decimal 

53 

54 

55class RateStackItemOut(Schema): 

56 rate: Decimal 

57 price: Decimal 

58 points_pct: Decimal 

59 

60 

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] = [] 

77 

78 

79class IneligibleProductOut(Schema): 

80 product_id: str 

81 product_name: str 

82 reasons: list[str] = [] 

83 

84 

85class PricingResultOut(Schema): 

86 count: int 

87 products: list[PricedProductOut] = [] 

88 ineligible_products: list[IneligibleProductOut] = [] 

89 

90 

91class RecommendationOut(Schema): 

92 best: PricedProductOut 

93 rationale: str 

94 alternatives: list[PricedProductOut] = [] 

95 

96 

97class PricingResponseOut(PricingResultOut): 

98 """Quote response: a pricing result plus an optional agentic recommendation.""" 

99 

100 recommendation: RecommendationOut | None = None 

101 

102 

103class QuoteIn(Schema): 

104 """All-optional what-if overrides; any field left unset falls back to the loan.""" 

105 

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 

116 

117 

118class RateLockOut(ModelSchema): 

119 rate: Decimal | None = None 

120 product_id: int | None = None 

121 

122 class Meta: 

123 model = RateLock 

124 exclude = ["loan"] 

125 

126 @staticmethod 

127 def resolve_rate(obj): 

128 return obj.locked_rate 

129 

130 @staticmethod 

131 def resolve_product_id(obj): 

132 return obj.product_id 

133 

134 

135class RateLockReadOut(Schema): 

136 """Wrapper so the read endpoint can return 200 with a null lock.""" 

137 

138 lock: RateLockOut | None = None 

139 

140 

141class RateLockCreate(Schema): 

142 product_id: int 

143 rate: Decimal | None = None 

144 price: Decimal | None = None 

145 lock_period_days: int | None = None