1use chrono::{DateTime, Utc};
2use forge_core::workflow::{StepStatus, WorkflowStatus};
3use uuid::Uuid;
4
5#[derive(Debug, Clone)]
7pub struct WorkflowRecord {
8 pub id: Uuid,
10 pub workflow_name: String,
12 pub workflow_version: String,
14 pub workflow_signature: String,
16 pub owner_subject: Option<String>,
18 pub input: serde_json::Value,
20 pub output: Option<serde_json::Value>,
22 pub status: WorkflowStatus,
24 pub blocking_reason: Option<String>,
26 pub resolution_reason: Option<String>,
28 pub current_step: Option<String>,
30 pub step_results: serde_json::Value,
32 pub started_at: DateTime<Utc>,
34 pub completed_at: Option<DateTime<Utc>>,
36 pub error: Option<String>,
38 pub trace_id: Option<String>,
40}
41
42impl WorkflowRecord {
43 pub fn new(
45 workflow_name: impl Into<String>,
46 workflow_version: impl Into<String>,
47 workflow_signature: impl Into<String>,
48 input: serde_json::Value,
49 owner_subject: Option<String>,
50 ) -> Self {
51 Self {
52 id: Uuid::new_v4(),
53 workflow_name: workflow_name.into(),
54 workflow_version: workflow_version.into(),
55 workflow_signature: workflow_signature.into(),
56 owner_subject,
57 input,
58 output: None,
59 status: WorkflowStatus::Created,
60 blocking_reason: None,
61 resolution_reason: None,
62 current_step: None,
63 step_results: serde_json::json!({}),
64 started_at: Utc::now(),
65 completed_at: None,
66 error: None,
67 trace_id: None,
68 }
69 }
70
71 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
73 self.trace_id = Some(trace_id.into());
74 self
75 }
76
77 pub fn start(&mut self) {
79 self.status = WorkflowStatus::Running;
80 }
81
82 pub fn complete(&mut self, output: serde_json::Value) {
84 self.status = WorkflowStatus::Completed;
85 self.output = Some(output);
86 self.completed_at = Some(Utc::now());
87 }
88
89 pub fn fail(&mut self, error: impl Into<String>) {
91 self.status = WorkflowStatus::Failed;
92 self.error = Some(error.into());
93 self.completed_at = Some(Utc::now());
94 }
95
96 pub fn compensating(&mut self) {
98 self.status = WorkflowStatus::Compensating;
99 }
100
101 pub fn compensated(&mut self) {
103 self.status = WorkflowStatus::Compensated;
104 self.completed_at = Some(Utc::now());
105 }
106
107 pub fn set_current_step(&mut self, step: impl Into<String>) {
109 self.current_step = Some(step.into());
110 }
111
112 pub fn add_step_result(&mut self, step_name: &str, result: serde_json::Value) {
114 if let Some(obj) = self.step_results.as_object_mut() {
115 obj.insert(step_name.to_string(), result);
116 }
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct WorkflowStepRecord {
123 pub id: Uuid,
125 pub workflow_run_id: Uuid,
127 pub step_name: String,
129 pub status: StepStatus,
131 pub result: Option<serde_json::Value>,
133 pub error: Option<String>,
135 pub started_at: Option<DateTime<Utc>>,
137 pub completed_at: Option<DateTime<Utc>>,
139}
140
141impl WorkflowStepRecord {
142 pub fn new(workflow_run_id: Uuid, step_name: impl Into<String>) -> Self {
144 Self {
145 id: Uuid::new_v4(),
146 workflow_run_id,
147 step_name: step_name.into(),
148 status: StepStatus::Pending,
149 result: None,
150 error: None,
151 started_at: None,
152 completed_at: None,
153 }
154 }
155
156 pub fn start(&mut self) {
158 self.status = StepStatus::Running;
159 self.started_at = Some(Utc::now());
160 }
161
162 pub fn complete(&mut self, result: serde_json::Value) {
164 self.status = StepStatus::Completed;
165 self.result = Some(result);
166 self.completed_at = Some(Utc::now());
167 }
168
169 pub fn fail(&mut self, error: impl Into<String>) {
171 self.status = StepStatus::Failed;
172 self.error = Some(error.into());
173 self.completed_at = Some(Utc::now());
174 }
175
176 pub fn compensate(&mut self) {
178 self.status = StepStatus::Compensated;
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_workflow_record_creation() {
188 let record =
189 WorkflowRecord::new("test_workflow", "v1", "abc123", serde_json::json!({}), None);
190 assert_eq!(record.workflow_name, "test_workflow");
191 assert_eq!(record.workflow_version, "v1");
192 assert_eq!(record.workflow_signature, "abc123");
193 assert_eq!(record.status, WorkflowStatus::Created);
194 }
195
196 #[test]
197 fn test_workflow_record_transitions() {
198 let mut record = WorkflowRecord::new("test", "v1", "sig", serde_json::json!({}), None);
199
200 record.start();
201 assert_eq!(record.status, WorkflowStatus::Running);
202
203 record.complete(serde_json::json!({"result": "ok"}));
204 assert_eq!(record.status, WorkflowStatus::Completed);
205 assert!(record.completed_at.is_some());
206 }
207
208 #[test]
209 fn test_workflow_step_record() {
210 let workflow_id = Uuid::new_v4();
211 let mut step = WorkflowStepRecord::new(workflow_id, "step1");
212
213 assert_eq!(step.step_name, "step1");
214 assert_eq!(step.status, StepStatus::Pending);
215
216 step.start();
217 assert_eq!(step.status, StepStatus::Running);
218
219 step.complete(serde_json::json!({}));
220 assert_eq!(step.status, StepStatus::Completed);
221 }
222
223 #[test]
224 fn test_workflow_record_failure_path() {
225 let mut record = WorkflowRecord::new("test", "v1", "sig", serde_json::json!({}), None);
226 record.start();
227 record.fail("something went wrong");
228
229 assert_eq!(record.status, WorkflowStatus::Failed);
230 assert_eq!(record.error.as_deref(), Some("something went wrong"));
231 assert!(record.completed_at.is_some());
232 }
233
234 #[test]
235 fn test_workflow_record_compensation_path() {
236 let mut record = WorkflowRecord::new("test", "v1", "sig", serde_json::json!({}), None);
237 record.start();
238 record.compensating();
239 assert_eq!(record.status, WorkflowStatus::Compensating);
240
241 record.compensated();
242 assert_eq!(record.status, WorkflowStatus::Compensated);
243 assert!(record.completed_at.is_some());
244 }
245
246 #[test]
247 fn test_workflow_step_results_accumulate() {
248 let mut record = WorkflowRecord::new("test", "v1", "sig", serde_json::json!({}), None);
249 record.add_step_result("step1", serde_json::json!({"user_id": 42}));
250 record.add_step_result("step2", serde_json::json!({"verified": true}));
251
252 let results = record.step_results.as_object().expect("should be object");
253 assert_eq!(results.len(), 2);
254 assert_eq!(
255 results.get("step1").and_then(|v| v.get("user_id")),
256 Some(&serde_json::json!(42))
257 );
258 assert_eq!(
259 results.get("step2").and_then(|v| v.get("verified")),
260 Some(&serde_json::json!(true))
261 );
262 }
263
264 #[test]
265 fn test_workflow_current_step_tracking() {
266 let mut record = WorkflowRecord::new("test", "v1", "sig", serde_json::json!({}), None);
267 assert!(record.current_step.is_none());
268
269 record.set_current_step("validate_input");
270 assert_eq!(record.current_step.as_deref(), Some("validate_input"));
271
272 record.set_current_step("process_payment");
273 assert_eq!(record.current_step.as_deref(), Some("process_payment"));
274 }
275
276 #[test]
277 fn test_workflow_record_with_trace_id() {
278 let record = WorkflowRecord::new("test", "v1", "sig", serde_json::json!({}), None)
279 .with_trace_id("trace-abc-123");
280 assert_eq!(record.trace_id.as_deref(), Some("trace-abc-123"));
281 }
282
283 #[test]
284 fn test_workflow_record_with_owner() {
285 let record = WorkflowRecord::new(
286 "onboarding",
287 "v1",
288 "sig",
289 serde_json::json!({}),
290 Some("user-alice".into()),
291 );
292 assert_eq!(record.owner_subject.as_deref(), Some("user-alice"));
293 }
294
295 #[test]
296 fn test_step_record_failure() {
297 let mut step = WorkflowStepRecord::new(Uuid::new_v4(), "charge_card");
298 step.start();
299 step.fail("card declined");
300
301 assert_eq!(step.status, StepStatus::Failed);
302 assert_eq!(step.error.as_deref(), Some("card declined"));
303 assert!(step.completed_at.is_some());
304 }
305
306 #[test]
307 fn test_step_record_compensate() {
308 let mut step = WorkflowStepRecord::new(Uuid::new_v4(), "reserve_inventory");
309 step.start();
310 step.complete(serde_json::json!({"reserved": 5}));
311
312 step.compensate();
314 assert_eq!(step.status, StepStatus::Compensated);
315 }
316}