Coverage for apps/application_1003/schemas.py: 80%

531 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-04 14:06 +0000

1"""Ninja schemas for the 1003 financial entities + qualification.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from decimal import Decimal 

7 

8from ninja import ModelSchema, Schema 

9from pydantic import field_validator 

10 

11from apps.application_1003.models import ( 

12 Asset, 

13 Declaration, 

14 Demographics, 

15 DownPaymentSource, 

16 Employer, 

17 GiftGrant, 

18 HousingExpense, 

19 Income, 

20 Liability, 

21 LoanIdentifier, 

22 RealEstate, 

23 Residence, 

24 SubjectProperty, 

25) 

26from domain.state_compliance import STATE_COMPLIANCE 

27 

28VALID_STATE_CODES = frozenset(r["state_code"] for r in STATE_COMPLIANCE) 

29ZIP_RE = re.compile(r"^\d{5}(-\d{4})?$") 

30CITY_RE = re.compile(r"^[A-Za-z\s.'\-]+$") 

31COUNTY_RE = re.compile(r"^[A-Za-z\s.'\-]+$") 

32ADDRESS_RE = re.compile(r"^[A-Za-z0-9\s.,'#\-/]+$") 

33 

34 

35def _clean_phone(v: str | None) -> str: 

36 if not v or not v.strip(): 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true

37 return "" 

38 digits = "".join(c for c in v if c.isdigit()) 

39 if len(digits) != 10: 

40 raise ValueError(f"Phone number must contain exactly 10 digits (got {len(digits)})") 

41 return v.strip() 

42 

43 

44def _clean_state(v: str | None) -> str: 

45 if not v or not v.strip(): 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true

46 return "" 

47 code = v.strip().upper() 

48 if code not in VALID_STATE_CODES: 

49 raise ValueError(f"Invalid US state code: '{code}'") 

50 return code 

51 

52 

53def _clean_zip(v: str | None) -> str: 

54 if not v or not v.strip(): 54 ↛ 55line 54 didn't jump to line 55 because the condition on line 54 was never true

55 return "" 

56 clean = v.strip() 

57 if not ZIP_RE.match(clean): 

58 raise ValueError(f"Invalid US postal code: '{clean}'") 

59 return clean 

60 

61 

62def _clean_city(v: str | None) -> str: 

63 if not v or not v.strip(): 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true

64 return "" 

65 clean = v.strip() 

66 if len(clean) < 2 or len(clean) > 100: 66 ↛ 67line 66 didn't jump to line 67 because the condition on line 66 was never true

67 raise ValueError("City must be between 2 and 100 characters") 

68 if not CITY_RE.match(clean): 

69 raise ValueError("City must contain only letters, spaces, hyphens, periods, or apostrophes") 

70 return clean 

71 

72 

73def _clean_county(v: str | None) -> str: 

74 if not v or not v.strip(): 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true

75 return "" 

76 clean = v.strip() 

77 if len(clean) < 2 or len(clean) > 100: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true

78 raise ValueError("County must be between 2 and 100 characters") 

79 if not COUNTY_RE.match(clean): 

80 raise ValueError("County must contain only letters, spaces, hyphens, periods, or apostrophes") 

81 return clean 

82 

83 

84def _clean_address_line(v: str | None) -> str: 

85 if not v or not v.strip(): 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true

86 return "" 

87 clean = v.strip() 

88 if len(clean) < 3 or len(clean) > 255: 

89 raise ValueError("Street address must be between 3 and 255 characters") 

90 if not ADDRESS_RE.match(clean): 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true

91 raise ValueError("Street address contains invalid characters") 

92 return clean 

93 

94 

95def _clean_non_negative(v: Decimal | None, max_val: Decimal = Decimal("9999999999.99")) -> Decimal | None: 

96 if v is not None: 96 ↛ 101line 96 didn't jump to line 101 because the condition on line 96 was always true

97 if v < Decimal("0.00"): 

98 raise ValueError("Amount cannot be negative") 

99 if v > max_val: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 raise ValueError(f"Amount cannot exceed {max_val}") 

101 return v 

102 

103 

104class EmployerOut(ModelSchema): 

105 class Meta: 

106 model = Employer 

107 exclude = ["loan"] 

108 

109 

110class EmployerIn(ModelSchema): 

111 class Meta: 

112 model = Employer 

113 exclude = ["id", "loan", "created_at"] 

114 fields_optional = ["borrower"] 

115 

116 @field_validator("employer_name", check_fields=False) 

117 @classmethod 

118 def validate_name(cls, v: str) -> str: 

119 clean = v.strip() if v else "" 

120 if not clean: 

121 raise ValueError("Employer name cannot be blank") 

122 if len(clean) > 255: 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true

123 raise ValueError("Employer name cannot exceed 255 characters") 

124 return clean 

125 

126 @field_validator("position_title", check_fields=False) 

127 @classmethod 

128 def validate_position(cls, v: str | None) -> str: 

129 if not v: 

130 return "" 

131 clean = v.strip() 

132 if len(clean) > 255: 

133 raise ValueError("Position title cannot exceed 255 characters") 

134 return clean 

135 

136 @field_validator("phone", check_fields=False) 

137 @classmethod 

138 def validate_phone(cls, v: str | None) -> str: 

139 return _clean_phone(v) 

140 

141 @field_validator("state_code", check_fields=False) 

142 @classmethod 

143 def validate_state_code(cls, v: str | None) -> str: 

144 return _clean_state(v) 

145 

146 @field_validator("postal_code", check_fields=False) 

147 @classmethod 

148 def validate_postal_code(cls, v: str | None) -> str: 

149 return _clean_zip(v) 

150 

151 @field_validator("address_line", check_fields=False) 

152 @classmethod 

153 def validate_address(cls, v: str | None) -> str: 

154 return _clean_address_line(v) 

155 

156 @field_validator("city", check_fields=False) 

157 @classmethod 

158 def validate_city(cls, v: str | None) -> str: 

159 return _clean_city(v) 

160 

161 @field_validator("monthly_income_amt", check_fields=False) 

162 @classmethod 

163 def validate_monthly_income(cls, v: Decimal | None) -> Decimal | None: 

164 return _clean_non_negative(v, Decimal("999999999.99")) 

165 

166 @field_validator("years_in_line_of_work", check_fields=False) 

167 @classmethod 

168 def validate_years(cls, v: Decimal | None) -> Decimal | None: 

169 if v is not None: 169 ↛ 172line 169 didn't jump to line 172 because the condition on line 169 was always true

170 if v < Decimal("0.00") or v > Decimal("99.99"): 

171 raise ValueError("Years in line of work must be between 0 and 99.99 (enter duration, not start year)") 

172 return v 

173 

174 @field_validator("ownership_pct", check_fields=False) 

175 @classmethod 

176 def validate_ownership(cls, v: Decimal | None) -> Decimal | None: 

177 if v is not None: 177 ↛ 180line 177 didn't jump to line 180 because the condition on line 177 was always true

178 if v < Decimal("0.00") or v > Decimal("100.00"): 

179 raise ValueError("Ownership percentage must be between 0% and 100%") 

180 return v 

181 

182 

183class EmployerPatch(ModelSchema): 

184 class Meta: 

185 model = Employer 

186 exclude = ["id", "loan", "created_at"] 

187 fields_optional = "__all__" 

188 

189 @field_validator("employer_name", check_fields=False) 

190 @classmethod 

191 def validate_name(cls, v: str | None) -> str | None: 

192 if v is None: 

193 return None 

194 clean = v.strip() 

195 if not clean: 

196 raise ValueError("Employer name cannot be blank") 

197 if len(clean) > 255: 

198 raise ValueError("Employer name cannot exceed 255 characters") 

199 return clean 

200 

201 @field_validator("position_title", check_fields=False) 

202 @classmethod 

203 def validate_position(cls, v: str | None) -> str | None: 

204 if v is None: 

205 return None 

206 clean = v.strip() 

207 if len(clean) > 255: 

208 raise ValueError("Position title cannot exceed 255 characters") 

209 return clean 

210 

211 @field_validator("phone", check_fields=False) 

212 @classmethod 

213 def validate_phone(cls, v: str | None) -> str | None: 

214 return _clean_phone(v) if v else v 

215 

216 @field_validator("state_code", check_fields=False) 

217 @classmethod 

218 def validate_state_code(cls, v: str | None) -> str | None: 

219 return _clean_state(v) if v else v 

220 

221 @field_validator("postal_code", check_fields=False) 

222 @classmethod 

223 def validate_postal_code(cls, v: str | None) -> str | None: 

224 return _clean_zip(v) if v else v 

225 

226 @field_validator("address_line", check_fields=False) 

227 @classmethod 

228 def validate_address(cls, v: str | None) -> str | None: 

229 return _clean_address_line(v) if v else v 

230 

231 @field_validator("city", check_fields=False) 

232 @classmethod 

233 def validate_city(cls, v: str | None) -> str | None: 

234 return _clean_city(v) if v else v 

235 

236 @field_validator("monthly_income_amt", check_fields=False) 

237 @classmethod 

238 def validate_monthly_income(cls, v: Decimal | None) -> Decimal | None: 

239 return _clean_non_negative(v, Decimal("999999999.99")) 

240 

241 @field_validator("years_in_line_of_work", check_fields=False) 

242 @classmethod 

243 def validate_years(cls, v: Decimal | None) -> Decimal | None: 

244 if v is not None: 

245 if v < Decimal("0.00") or v > Decimal("99.99"): 

246 raise ValueError("Years in line of work must be between 0 and 99.99 (enter duration, not start year)") 

247 return v 

248 

249 @field_validator("ownership_pct", check_fields=False) 

250 @classmethod 

251 def validate_ownership(cls, v: Decimal | None) -> Decimal | None: 

252 if v is not None: 

253 if v < Decimal("0.00") or v > Decimal("100.00"): 

254 raise ValueError("Ownership percentage must be between 0% and 100%") 

255 return v 

256 

257 

258class IncomeOut(ModelSchema): 

259 class Meta: 

260 model = Income 

261 exclude = ["loan"] 

262 

263 

264class IncomeIn(ModelSchema): 

265 class Meta: 

266 model = Income 

267 exclude = ["id", "loan", "created_at"] 

268 fields_optional = ["borrower"] 

269 

270 @field_validator("monthly_amt", "annual_amt", check_fields=False) 

271 @classmethod 

272 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

273 return _clean_non_negative(v) 

274 

275 

276class IncomePatch(ModelSchema): 

277 class Meta: 

278 model = Income 

279 exclude = ["id", "loan", "created_at"] 

280 fields_optional = "__all__" 

281 

282 @field_validator("monthly_amt", "annual_amt", check_fields=False) 

283 @classmethod 

284 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

285 return _clean_non_negative(v) 

286 

287 

288class AssetOut(ModelSchema): 

289 class Meta: 

290 model = Asset 

291 exclude = ["loan"] 

292 

293 

294class AssetIn(ModelSchema): 

295 borrowers: list[int] = [] 

296 

297 class Meta: 

298 model = Asset 

299 exclude = ["id", "loan", "created_at"] 

300 

301 @field_validator("cash_or_market_value_amt", "gift_total_value_amt", check_fields=False) 

302 @classmethod 

303 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

304 return _clean_non_negative(v) 

305 

306 

307class AssetPatch(ModelSchema): 

308 class Meta: 

309 model = Asset 

310 exclude = ["id", "loan", "created_at"] 

311 fields_optional = "__all__" 

312 

313 @field_validator("cash_or_market_value_amt", "gift_total_value_amt", check_fields=False) 

314 @classmethod 

315 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

316 return _clean_non_negative(v) 

317 

318 

319class RealEstateOut(ModelSchema): 

320 class Meta: 

321 model = RealEstate 

322 exclude = ["loan"] 

323 

324 

325class RealEstateIn(ModelSchema): 

326 borrowers: list[int] = [] 

327 

328 class Meta: 

329 model = RealEstate 

330 exclude = ["id", "loan", "created_at"] 

331 

332 @field_validator( 

333 "property_value_amt", 

334 "mortgage_unpaid_balance_amt", 

335 "mortgage_monthly_payment_amt", 

336 "gross_rental_income_amt", 

337 check_fields=False, 

338 ) 

339 @classmethod 

340 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

341 return _clean_non_negative(v) 

342 

343 @field_validator("address_line", check_fields=False) 

344 @classmethod 

345 def validate_address(cls, v: str | None) -> str: 

346 return _clean_address_line(v) 

347 

348 @field_validator("city", check_fields=False) 

349 @classmethod 

350 def validate_city(cls, v: str | None) -> str: 

351 return _clean_city(v) 

352 

353 @field_validator("state_code", check_fields=False) 

354 @classmethod 

355 def validate_state(cls, v: str | None) -> str: 

356 return _clean_state(v) 

357 

358 @field_validator("postal_code", check_fields=False) 

359 @classmethod 

360 def validate_zip(cls, v: str | None) -> str: 

361 return _clean_zip(v) 

362 

363 

364class RealEstatePatch(ModelSchema): 

365 class Meta: 

366 model = RealEstate 

367 exclude = ["id", "loan", "created_at"] 

368 fields_optional = "__all__" 

369 

370 @field_validator( 

371 "property_value_amt", 

372 "mortgage_unpaid_balance_amt", 

373 "mortgage_monthly_payment_amt", 

374 "gross_rental_income_amt", 

375 check_fields=False, 

376 ) 

377 @classmethod 

378 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

379 return _clean_non_negative(v) 

380 

381 @field_validator("address_line", check_fields=False) 

382 @classmethod 

383 def validate_address(cls, v: str | None) -> str | None: 

384 return _clean_address_line(v) if v else v 

385 

386 @field_validator("city", check_fields=False) 

387 @classmethod 

388 def validate_city(cls, v: str | None) -> str | None: 

389 return _clean_city(v) if v else v 

390 

391 @field_validator("state_code", check_fields=False) 

392 @classmethod 

393 def validate_state(cls, v: str | None) -> str | None: 

394 return _clean_state(v) if v else v 

395 

396 @field_validator("postal_code", check_fields=False) 

397 @classmethod 

398 def validate_zip(cls, v: str | None) -> str | None: 

399 return _clean_zip(v) if v else v 

400 

401 

402class LiabilityOut(ModelSchema): 

403 class Meta: 

404 model = Liability 

405 exclude = ["loan"] 

406 

407 

408class LiabilityIn(ModelSchema): 

409 borrowers: list[int] = [] 

410 

411 class Meta: 

412 model = Liability 

413 exclude = ["id", "loan", "created_at"] 

414 

415 @field_validator("monthly_payment_amt", "unpaid_balance_amt", check_fields=False) 

416 @classmethod 

417 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

418 return _clean_non_negative(v) 

419 

420 

421class LiabilityPatch(ModelSchema): 

422 class Meta: 

423 model = Liability 

424 exclude = ["id", "loan", "created_at"] 

425 fields_optional = "__all__" 

426 

427 @field_validator("monthly_payment_amt", "unpaid_balance_amt", check_fields=False) 

428 @classmethod 

429 def validate_amounts(cls, v: Decimal | None) -> Decimal | None: 

430 return _clean_non_negative(v) 

431 

432 

433class HousingExpenseOut(ModelSchema): 

434 class Meta: 

435 model = HousingExpense 

436 exclude = ["loan"] 

437 

438 

439class HousingExpenseIn(ModelSchema): 

440 class Meta: 

441 model = HousingExpense 

442 exclude = ["id", "loan", "created_at"] 

443 

444 @field_validator("payment_amt", check_fields=False) 

445 @classmethod 

446 def validate_payment(cls, v: Decimal | None) -> Decimal | None: 

447 return _clean_non_negative(v) 

448 

449 

450class HousingExpensePatch(ModelSchema): 

451 class Meta: 

452 model = HousingExpense 

453 exclude = ["id", "loan", "created_at"] 

454 fields_optional = "__all__" 

455 

456 @field_validator("payment_amt", check_fields=False) 

457 @classmethod 

458 def validate_payment(cls, v: Decimal | None) -> Decimal | None: 

459 return _clean_non_negative(v) 

460 

461 

462class QualificationOut(Schema): 

463 monthly_income: Decimal 

464 monthly_debt: Decimal 

465 housing_expense: Decimal 

466 housing_dti_pct: Decimal 

467 total_dti_pct: Decimal 

468 

469 

470# --- 1003 extension schemas (details/08) --------------------------------------- 

471# Loan-FK collections (registered via _register_crud). 

472class DownPaymentSourceOut(ModelSchema): 

473 class Meta: 

474 model = DownPaymentSource 

475 exclude = ["loan"] 

476 

477 

478class DownPaymentSourceIn(ModelSchema): 

479 class Meta: 

480 model = DownPaymentSource 

481 exclude = ["id", "loan", "created_at"] 

482 

483 

484class DownPaymentSourcePatch(ModelSchema): 

485 class Meta: 

486 model = DownPaymentSource 

487 exclude = ["id", "loan", "created_at"] 

488 fields_optional = "__all__" 

489 

490 

491class GiftGrantOut(ModelSchema): 

492 class Meta: 

493 model = GiftGrant 

494 exclude = ["loan"] 

495 

496 

497class GiftGrantIn(ModelSchema): 

498 class Meta: 

499 model = GiftGrant 

500 exclude = ["id", "loan", "created_at"] 

501 

502 

503class GiftGrantPatch(ModelSchema): 

504 class Meta: 

505 model = GiftGrant 

506 exclude = ["id", "loan", "created_at"] 

507 fields_optional = "__all__" 

508 

509 

510class LoanIdentifierOut(ModelSchema): 

511 class Meta: 

512 model = LoanIdentifier 

513 exclude = ["loan"] 

514 

515 

516class LoanIdentifierIn(ModelSchema): 

517 class Meta: 

518 model = LoanIdentifier 

519 exclude = ["id", "loan", "created_at"] 

520 

521 

522class LoanIdentifierPatch(ModelSchema): 

523 class Meta: 

524 model = LoanIdentifier 

525 exclude = ["id", "loan", "created_at"] 

526 fields_optional = "__all__" 

527 

528 

529# Residence (borrower-FK; custom endpoints). 

530class ResidenceOut(ModelSchema): 

531 class Meta: 

532 model = Residence 

533 fields = "__all__" 

534 

535 

536class ResidenceIn(Schema): 

537 borrower_id: int 

538 residence_type: str 

539 basis_type: str = "" 

540 duration_months: int | None = None 

541 monthly_rent_amt: Decimal | None = None 

542 address_line: str = "" 

543 unit_id: str = "" 

544 city: str = "" 

545 county: str = "" 

546 state_code: str = "" 

547 postal_code: str = "" 

548 

549 @field_validator("address_line") 

550 @classmethod 

551 def validate_address(cls, v: str | None) -> str: 

552 return _clean_address_line(v) 

553 

554 @field_validator("city") 

555 @classmethod 

556 def validate_city(cls, v: str | None) -> str: 

557 return _clean_city(v) 

558 

559 @field_validator("county") 

560 @classmethod 

561 def validate_county(cls, v: str | None) -> str: 

562 return _clean_county(v) 

563 

564 @field_validator("state_code") 

565 @classmethod 

566 def validate_state(cls, v: str | None) -> str: 

567 return _clean_state(v) 

568 

569 @field_validator("postal_code") 

570 @classmethod 

571 def validate_zip(cls, v: str | None) -> str: 

572 return _clean_zip(v) 

573 

574 @field_validator("monthly_rent_amt") 

575 @classmethod 

576 def validate_rent(cls, v: Decimal | None) -> Decimal | None: 

577 return _clean_non_negative(v, Decimal("999999999.99")) 

578 

579 @field_validator("duration_months") 

580 @classmethod 

581 def validate_duration(cls, v: int | None) -> int | None: 

582 if v is not None and not (0 <= v <= 1200): 582 ↛ 583line 582 didn't jump to line 583 because the condition on line 582 was never true

583 raise ValueError("Duration months must be between 0 and 1200") 

584 return v 

585 

586 

587class ResidencePatch(ModelSchema): 

588 class Meta: 

589 model = Residence 

590 exclude = ["id", "borrower", "created_at"] 

591 fields_optional = "__all__" 

592 

593 @field_validator("address_line", check_fields=False) 

594 @classmethod 

595 def validate_address(cls, v: str | None) -> str | None: 

596 return _clean_address_line(v) if v else v 

597 

598 @field_validator("city", check_fields=False) 

599 @classmethod 

600 def validate_city(cls, v: str | None) -> str | None: 

601 return _clean_city(v) if v else v 

602 

603 @field_validator("county", check_fields=False) 

604 @classmethod 

605 def validate_county(cls, v: str | None) -> str | None: 

606 return _clean_county(v) if v else v 

607 

608 @field_validator("state_code", check_fields=False) 

609 @classmethod 

610 def validate_state(cls, v: str | None) -> str | None: 

611 return _clean_state(v) if v else v 

612 

613 @field_validator("postal_code", check_fields=False) 

614 @classmethod 

615 def validate_zip(cls, v: str | None) -> str | None: 

616 return _clean_zip(v) if v else v 

617 

618 @field_validator("monthly_rent_amt", check_fields=False) 

619 @classmethod 

620 def validate_rent(cls, v: Decimal | None) -> Decimal | None: 

621 return _clean_non_negative(v, Decimal("999999999.99")) 

622 

623 @field_validator("duration_months", check_fields=False) 

624 @classmethod 

625 def validate_duration(cls, v: int | None) -> int | None: 

626 if v is not None and not (0 <= v <= 1200): 

627 raise ValueError("Duration months must be between 0 and 1200") 

628 return v 

629 

630 

631# One-to-one sets (get-or-create endpoints). 

632class DeclarationOut(ModelSchema): 

633 class Meta: 

634 model = Declaration 

635 exclude = ["borrower"] 

636 

637 

638class DeclarationPatch(ModelSchema): 

639 class Meta: 

640 model = Declaration 

641 exclude = ["id", "borrower", "created_at", "updated_at"] 

642 fields_optional = "__all__" 

643 

644 

645class DemographicsOut(ModelSchema): 

646 class Meta: 

647 model = Demographics 

648 exclude = ["borrower"] 

649 

650 

651class DemographicsPatch(ModelSchema): 

652 class Meta: 

653 model = Demographics 

654 exclude = ["id", "borrower", "created_at", "updated_at"] 

655 fields_optional = "__all__" 

656 

657 

658class SubjectPropertyOut(ModelSchema): 

659 class Meta: 

660 model = SubjectProperty 

661 exclude = ["loan"] 

662 

663 

664class SubjectPropertyPatch(ModelSchema): 

665 class Meta: 

666 model = SubjectProperty 

667 exclude = ["id", "loan", "created_at", "updated_at"] 

668 fields_optional = "__all__" 

669 

670 @field_validator("address_line", check_fields=False) 

671 @classmethod 

672 def validate_address_line(cls, v: str | None) -> str | None: 

673 return _clean_address_line(v) if v else v 

674 

675 @field_validator("city", check_fields=False) 

676 @classmethod 

677 def validate_city(cls, v: str | None) -> str | None: 

678 return _clean_city(v) if v else v 

679 

680 @field_validator("county", check_fields=False) 

681 @classmethod 

682 def validate_county(cls, v: str | None) -> str | None: 

683 return _clean_county(v) if v else v 

684 

685 @field_validator("state_code", check_fields=False) 

686 @classmethod 

687 def validate_state(cls, v: str | None) -> str | None: 

688 return _clean_state(v) if v else v 

689 

690 @field_validator("financed_unit_count", check_fields=False) 

691 @classmethod 

692 def validate_financed_unit_count(cls, v: int | None) -> int | None: 

693 if v is not None and not (1 <= v <= 4): 693 ↛ 694line 693 didn't jump to line 694 because the condition on line 693 was never true

694 raise ValueError("Financed unit count must be between 1 and 4") 

695 return v 

696 

697 @field_validator("year_built", check_fields=False) 

698 @classmethod 

699 def validate_year_built(cls, v: int | None) -> int | None: 

700 if v is not None and not (1700 <= v <= 2030): 

701 raise ValueError("Year built must be a 4-digit year between 1700 and 2030") 

702 return v 

703 

704 @field_validator("postal_code", check_fields=False) 

705 @classmethod 

706 def validate_postal_code(cls, v: str | None) -> str | None: 

707 if v is None or not v.strip(): 707 ↛ 708line 707 didn't jump to line 708 because the condition on line 707 was never true

708 return v 

709 return _clean_zip(v) 

710 

711 @field_validator( 

712 "real_estate_taxes_annual_amt", 

713 "hazard_insurance_annual_amt", 

714 "estimated_value_amt", 

715 check_fields=False, 

716 ) 

717 @classmethod 

718 def validate_property_amounts(cls, v: Decimal | None) -> Decimal | None: 

719 return _clean_non_negative(v, Decimal("999999999.99")) 

720 

721 @field_validator("hoa_dues_monthly_amt", check_fields=False) 

722 @classmethod 

723 def validate_hoa_dues(cls, v: Decimal | None) -> Decimal | None: 

724 return _clean_non_negative(v, Decimal("9999999.99"))