Coverage for apps/workflow/schemas.py: 87%

131 statements  

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

1"""Ninja schemas for the workflow API (trackers, key dates, tasks, templates, rules).""" 

2 

3from __future__ import annotations 

4 

5import datetime 

6from datetime import date 

7 

8from ninja import ModelSchema, Schema 

9from pydantic import field_validator 

10 

11from apps.workflow.models import AutomationRule, KeyDates, LoanTask, LoanTracker, TaskTemplate 

12 

13 

14# --- Trackers ------------------------------------------------------------------ 

15class TrackerOut(ModelSchema): 

16 class Meta: 

17 model = LoanTracker 

18 exclude = ["loan"] 

19 

20 

21class TrackerPatch(Schema): 

22 status: str | None = None 

23 target_date: date | None = None 

24 

25 

26# --- Key dates ----------------------------------------------------------------- 

27class KeyDatesOut(ModelSchema): 

28 class Meta: 

29 model = KeyDates 

30 exclude = ["loan"] 

31 

32 

33class KeyDatesPatch(Schema): 

34 key: str | None = None 

35 date: datetime.date | None = None 

36 application_date: datetime.date | None = None 

37 le_disclosed_date: datetime.date | None = None 

38 le_signed_date: datetime.date | None = None 

39 intent_to_proceed_date: datetime.date | None = None 

40 appraisal_ordered_date: datetime.date | None = None 

41 appraisal_received_date: datetime.date | None = None 

42 lock_date: datetime.date | None = None 

43 lock_expiration_date: datetime.date | None = None 

44 submitted_to_uw_date: datetime.date | None = None 

45 approved_date: datetime.date | None = None 

46 clear_to_close_date: datetime.date | None = None 

47 cd_disclosed_date: datetime.date | None = None 

48 cd_signed_date: datetime.date | None = None 

49 closing_date: datetime.date | None = None 

50 funding_date: datetime.date | None = None 

51 first_payment_date: datetime.date | None = None 

52 

53 

54# --- Tasks --------------------------------------------------------------------- 

55class TaskOut(ModelSchema): 

56 class Meta: 

57 model = LoanTask 

58 exclude = ["loan"] 

59 

60 

61class TaskCreate(Schema): 

62 title: str 

63 description: str = "" 

64 assignee_id: int | None = None 

65 due_date: date | None = None 

66 checklists: list = [] 

67 follow_up_on_done: bool = False 

68 follow_up_offset_days: int = 7 

69 

70 @field_validator("title") 

71 @classmethod 

72 def validate_title(cls, v: str) -> str: 

73 s = v.strip() if v else "" 

74 if not s: 74 ↛ 75line 74 didn't jump to line 75 because the condition on line 74 was never true

75 raise ValueError("Task title cannot be blank.") 

76 if len(s) > 255: 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true

77 raise ValueError("Task title cannot exceed 255 characters.") 

78 return s 

79 

80 

81class TaskPatch(Schema): 

82 title: str | None = None 

83 description: str | None = None 

84 assignee_id: int | None = None 

85 due_date: date | None = None 

86 completed: bool | None = None 

87 checklists: list | None = None 

88 

89 @field_validator("title") 

90 @classmethod 

91 def validate_title(cls, v: str | None) -> str | None: 

92 if v is None: 

93 return None 

94 s = v.strip() 

95 if not s: 

96 raise ValueError("Task title cannot be blank.") 

97 if len(s) > 255: 

98 raise ValueError("Task title cannot exceed 255 characters.") 

99 return s 

100 

101 

102class TaskFromTemplateIn(Schema): 

103 template_id: int 

104 assignee_id: int | None = None 

105 

106 

107# --- Tasks hub (cross-loan) ------------------------------------------------------ 

108class TaskHubRowOut(Schema): 

109 """A LoanTask row flattened across loans, for GET /api/tasks (Tasks Hub).""" 

110 

111 id: int 

112 title: str 

113 description: str 

114 assignee_id: int | None 

115 due_date: date | None 

116 status: str 

117 checklists: list 

118 loan_guid: str 

119 loan_number: str 

120 borrower_name: str 

121 

122 

123class TaskHubListOut(Schema): 

124 count: int 

125 rows: list[TaskHubRowOut] 

126 

127 

128# --- Templates + rules (config) ------------------------------------------------ 

129class TaskTemplateOut(ModelSchema): 

130 class Meta: 

131 model = TaskTemplate 

132 fields = "__all__" 

133 

134 

135class TaskTemplateCreate(Schema): 

136 title: str 

137 description: str = "" 

138 due_offset_days: int = 3 

139 checklists: list = [] 

140 follow_up: dict = {} 

141 

142 

143class AutomationRuleOut(ModelSchema): 

144 class Meta: 

145 model = AutomationRule 

146 fields = "__all__" 

147 

148 

149class AutomationRuleCreate(Schema): 

150 rule_id: str 

151 name: str 

152 trigger: str 

153 conditions: list = [] 

154 actions: list = [] 

155 is_active: bool = True 

156 

157 

158class RuleEvaluateIn(Schema): 

159 trigger: str 

160 target_stage: str | None = None 

161 date_key: str | None = None 

162 

163 

164class RuleEffectOut(Schema): 

165 rule_id: str 

166 action_type: str 

167 message: str = "" 

168 task_title: str = "" 

169 

170 

171class RuleErrorOut(Schema): 

172 code: str 

173 message: str 

174 field: str | None = None 

175 

176 

177class RuleOutcomeOut(Schema): 

178 ok: bool 

179 errors: list[RuleErrorOut] = [] 

180 effects: list[RuleEffectOut] = [] 

181 created_task_ids: list[int] = []