Coverage for apps/fees_trid/services.py: 90%

17 statements  

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

1"""Shared fee-replace logic used by both ``set_fees`` and the Change-of-Circumstance 

2endpoint (design.md D4, add-coc-gate).""" 

3 

4from __future__ import annotations 

5 

6from django.db import transaction as db_transaction 

7 

8from apps.core_los.models import Loan 

9from apps.fees_trid.models import Fee 

10from apps.fees_trid.schemas import FeeIn 

11from domain.enums import FeeSection 

12from domain.result import AppError, DomainError 

13 

14_VALID_SECTIONS = {s.value for s in FeeSection} 

15 

16 

17def replace_fees(loan: Loan, fees: list[FeeIn]) -> list[Fee]: 

18 """Delete the loan's current fees and recreate them from ``fees``, validating 

19 each fee's TRID section first. Raises ``DomainError`` and changes nothing if any 

20 section is invalid.""" 

21 bad = [f.section for f in fees if f.section not in _VALID_SECTIONS] 

22 if bad: 22 ↛ 23line 22 didn't jump to line 23 because the condition on line 22 was never true

23 raise DomainError([AppError("VALIDATION", f"Invalid fee section(s): {', '.join(bad)}")]) 

24 with db_transaction.atomic(): 

25 loan.fees.all().delete() 

26 for f in fees: 

27 Fee.objects.create( 

28 loan=loan, 

29 section=f.section, 

30 name=f.name, 

31 total_amt=f.total_amt, 

32 tolerance_bucket=f.tolerance_bucket or "", # Fee.save() fills from section 

33 paid_by=f.paid_by, 

34 paid_to=f.paid_to, 

35 poc_ind=f.poc_ind, 

36 ) 

37 return list(loan.fees.all())