1use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum InstanceStatus {
11 Created,
12 Running,
13 Suspended,
14 Terminated,
15 Completed,
16 Withdrawn,
17 Error,
18}
19
20impl InstanceStatus {
21 pub fn is_handleable(&self) -> bool {
23 matches!(self, Self::Running)
24 }
25}
26
27impl std::fmt::Display for InstanceStatus {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.write_str(match self {
30 Self::Created => "created",
31 Self::Running => "running",
32 Self::Suspended => "suspended",
33 Self::Terminated => "terminated",
34 Self::Completed => "completed",
35 Self::Withdrawn => "withdrawn",
36 Self::Error => "error",
37 })
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum TaskStatus {
45 Pending,
46 Completed,
47 Rejected,
48 Transferred,
49 Invalidated,
50}
51
52impl std::fmt::Display for TaskStatus {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(match self {
55 Self::Pending => "pending",
56 Self::Completed => "completed",
57 Self::Rejected => "rejected",
58 Self::Transferred => "transferred",
59 Self::Invalidated => "invalidated",
60 })
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
66#[serde(rename_all = "snake_case")]
67pub enum TaskAction {
68 Approve,
69 Reject,
70 Transfer,
71 AddSign,
72 Withdraw,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct FlowInstance {
78 pub instance_id: String,
79 pub flow_key: String,
80 pub version: semver::Version,
81 pub status: InstanceStatus,
82 pub current_nodes: Vec<String>,
84 pub context: serde_json::Value,
86 pub version_lock: u64,
88 pub created_at: DateTime<Utc>,
89 pub updated_at: DateTime<Utc>,
90 pub initiator: String,
91}
92
93impl FlowInstance {
94 pub fn new(
95 instance_id: impl Into<String>,
96 flow_key: impl Into<String>,
97 version: semver::Version,
98 initiator: impl Into<String>,
99 context: serde_json::Value,
100 start_node: impl Into<String>,
101 ) -> Self {
102 let now = Utc::now();
103 Self {
104 instance_id: instance_id.into(),
105 flow_key: flow_key.into(),
106 version,
107 status: InstanceStatus::Running,
108 current_nodes: vec![start_node.into()],
109 context,
110 version_lock: 0,
111 created_at: now,
112 updated_at: now,
113 initiator: initiator.into(),
114 }
115 }
116
117 pub fn bump_version(&mut self) {
119 self.version_lock += 1;
120 self.updated_at = Utc::now();
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct Task {
127 pub task_id: String,
128 pub instance_id: String,
129 pub node_id: String,
130 pub candidates: Vec<String>,
131 pub status: TaskStatus,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub assignee: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub action: Option<TaskAction>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub handled_at: Option<DateTime<Utc>>,
138 pub created_at: DateTime<Utc>,
139}
140
141impl Task {
142 pub fn new_pending(
143 task_id: impl Into<String>,
144 instance_id: impl Into<String>,
145 node_id: impl Into<String>,
146 candidates: Vec<String>,
147 ) -> Self {
148 Self {
149 task_id: task_id.into(),
150 instance_id: instance_id.into(),
151 node_id: node_id.into(),
152 candidates,
153 status: TaskStatus::Pending,
154 assignee: None,
155 action: None,
156 handled_at: None,
157 created_at: Utc::now(),
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct ApprovalRecord {
165 pub record_id: String,
166 pub instance_id: String,
167 pub task_id: String,
168 pub node_id: String,
169 pub actor: String,
170 pub action: TaskAction,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub comment: Option<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub target_user: Option<String>,
175 pub timestamp: DateTime<Utc>,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum HistoryEntryType {
182 Transition,
183 NodeEnter,
184 NodeLeave,
185 TaskHandled,
186 InstanceLifecycle,
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct HistoryEntry {
192 pub entry_id: String,
193 pub instance_id: String,
194 pub entry_type: HistoryEntryType,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub from_node: Option<String>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub to_node: Option<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub from_state: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub to_state: Option<String>,
203 pub context_snapshot: serde_json::Value,
204 pub timestamp: DateTime<Utc>,
205}
206
207impl HistoryEntry {
208 pub fn transition(
209 entry_id: impl Into<String>,
210 instance_id: impl Into<String>,
211 from_state: impl Into<String>,
212 to_state: impl Into<String>,
213 context_snapshot: serde_json::Value,
214 ) -> Self {
215 Self {
216 entry_id: entry_id.into(),
217 instance_id: instance_id.into(),
218 entry_type: HistoryEntryType::Transition,
219 from_node: None,
220 to_node: None,
221 from_state: Some(from_state.into()),
222 to_state: Some(to_state.into()),
223 context_snapshot,
224 timestamp: Utc::now(),
225 }
226 }
227
228 pub fn node_event(
229 entry_id: impl Into<String>,
230 instance_id: impl Into<String>,
231 from_node: Option<String>,
232 to_node: impl Into<String>,
233 entry_type: HistoryEntryType,
234 ) -> Self {
235 Self {
236 entry_id: entry_id.into(),
237 instance_id: instance_id.into(),
238 entry_type,
239 from_node,
240 to_node: Some(to_node.into()),
241 from_state: None,
242 to_state: None,
243 context_snapshot: serde_json::Value::Null,
244 timestamp: Utc::now(),
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct PageRequest {
252 pub page: u32,
253 pub page_size: u32,
254}
255
256impl Default for PageRequest {
257 fn default() -> Self {
258 Self {
259 page: 1,
260 page_size: 20,
261 }
262 }
263}
264
265impl PageRequest {
266 pub fn offset(&self) -> usize {
267 ((self.page.saturating_sub(1)) as usize) * (self.page_size as usize)
268 }
269 pub fn limit(&self) -> usize {
270 self.page_size as usize
271 }
272}
273
274#[derive(Debug, Clone, Serialize)]
276pub struct PageResult<T> {
277 pub items: Vec<T>,
278 pub total: u64,
279 pub page: u32,
280 pub page_size: u32,
281}
282
283impl<T> PageResult<T> {
284 pub fn new(items: Vec<T>, total: u64, req: &PageRequest) -> Self {
285 Self {
286 items,
287 total,
288 page: req.page,
289 page_size: req.page_size,
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn instance_status_is_handleable() {
300 assert!(InstanceStatus::Running.is_handleable());
301 assert!(!InstanceStatus::Suspended.is_handleable());
302 assert!(!InstanceStatus::Created.is_handleable());
303 assert!(!InstanceStatus::Terminated.is_handleable());
304 }
305
306 #[test]
307 fn flow_instance_new() {
308 let inst = FlowInstance::new(
309 "i1",
310 "leave",
311 semver::Version::new(1, 0, 0),
312 "user1",
313 serde_json::json!({}),
314 "start",
315 );
316 assert_eq!(inst.instance_id, "i1");
317 assert_eq!(inst.status, InstanceStatus::Running);
318 assert_eq!(inst.current_nodes, vec!["start"]);
319 assert_eq!(inst.version_lock, 0);
320 }
321
322 #[test]
323 fn flow_instance_bump_version() {
324 let mut inst = FlowInstance::new(
325 "i1",
326 "leave",
327 semver::Version::new(1, 0, 0),
328 "u1",
329 serde_json::json!({}),
330 "s",
331 );
332 let old = inst.updated_at;
333 std::thread::sleep(std::time::Duration::from_millis(1));
334 inst.bump_version();
335 assert_eq!(inst.version_lock, 1);
336 assert!(inst.updated_at > old);
337 }
338
339 #[test]
340 fn task_new_pending() {
341 let t = Task::new_pending("t1", "i1", "n1", vec!["u1".into(), "u2".into()]);
342 assert_eq!(t.status, TaskStatus::Pending);
343 assert_eq!(t.candidates, vec!["u1", "u2"]);
344 assert!(t.assignee.is_none());
345 }
346
347 #[test]
348 fn page_request_offset() {
349 let r = PageRequest {
350 page: 3,
351 page_size: 10,
352 };
353 assert_eq!(r.offset(), 20);
354 assert_eq!(r.limit(), 10);
355
356 let r2 = PageRequest::default();
357 assert_eq!(r2.offset(), 0);
358 assert_eq!(r2.limit(), 20);
359 }
360
361 #[test]
362 fn history_entry_transition() {
363 let e = HistoryEntry::transition("e1", "i1", "draft", "review", serde_json::json!({}));
364 assert_eq!(e.entry_type, HistoryEntryType::Transition);
365 assert_eq!(e.from_state.as_deref(), Some("draft"));
366 assert_eq!(e.to_state.as_deref(), Some("review"));
367 }
368
369 #[test]
370 fn history_entry_node_event() {
371 let e = HistoryEntry::node_event(
372 "e1",
373 "i1",
374 Some("n0".into()),
375 "n1",
376 HistoryEntryType::NodeEnter,
377 );
378 assert_eq!(e.entry_type, HistoryEntryType::NodeEnter);
379 assert_eq!(e.from_node.as_deref(), Some("n0"));
380 assert_eq!(e.to_node.as_deref(), Some("n1"));
381 }
382
383 #[test]
384 fn enums_serde() {
385 assert_eq!(
386 serde_json::to_string(&InstanceStatus::Running).unwrap(),
387 "\"running\""
388 );
389 assert_eq!(
390 serde_json::to_string(&TaskStatus::Pending).unwrap(),
391 "\"pending\""
392 );
393 assert_eq!(
394 serde_json::to_string(&TaskAction::Approve).unwrap(),
395 "\"approve\""
396 );
397 }
398}