Skip to main content

track/use_cases/
complete_todo.rs

1use crate::db::Database;
2use crate::models::TodoStatus;
3use crate::services::{TodoService, WorktreeService};
4use crate::utils::{Result, TrackError};
5
6/// Result of completing a TODO, including optional workspace bookmark name.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct CompleteTodoOutcome {
9    pub task_index: i64,
10    pub merged_bookmark: Option<String>,
11}
12
13/// Completes a TODO: merges/removes JJ workspaces, then marks the TODO done in SQLite.
14///
15/// JJ operations cannot participate in the database transaction. The workflow therefore
16/// completes external side effects first, then persists the terminal state. If the DB
17/// update fails after a successful merge, a typed error is returned so the caller can
18/// recover manually.
19pub struct CompleteTodoUseCase<'a> {
20    db: &'a Database,
21}
22
23impl<'a> CompleteTodoUseCase<'a> {
24    pub fn new(db: &'a Database) -> Self {
25        Self { db }
26    }
27
28    /// Complete the TODO identified by task-scoped `task_index` on `task_id`.
29    pub fn execute(&self, task_id: i64, task_index: i64) -> Result<CompleteTodoOutcome> {
30        let todo_service = TodoService::new(self.db);
31        let worktree_service = WorktreeService::new(self.db);
32
33        let todo = todo_service.get_todo_by_index(task_id, task_index)?;
34
35        if todo.status != TodoStatus::Pending {
36            return Err(TrackError::InvalidStatusTransition {
37                from: todo.status.as_str().to_string(),
38                to: TodoStatus::Done.as_str().to_string(),
39            });
40        }
41
42        let merged_bookmark = worktree_service.complete_worktree_for_todo(todo.id)?;
43
44        if let Err(err) = todo_service.mark_done(todo.id) {
45            if let Some(bookmark) = merged_bookmark.clone() {
46                return Err(TrackError::TodoCompletionDbFailed {
47                    todo_index: task_index,
48                    bookmark,
49                    detail: err.to_string(),
50                });
51            }
52            return Err(err);
53        }
54
55        Ok(CompleteTodoOutcome {
56            task_index,
57            merged_bookmark,
58        })
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::db::Database;
66    use crate::services::TaskService;
67
68    fn setup_db() -> Database {
69        Database::new_in_memory().unwrap()
70    }
71
72    #[test]
73    fn complete_todo_rejects_already_done() {
74        let db = setup_db();
75        let task_id = TaskService::new(&db)
76            .create_task("Task", None, None, None)
77            .unwrap()
78            .id;
79        let todo_service = TodoService::new(&db);
80        let todo = todo_service.add_todo(task_id, "Item", false).unwrap();
81        todo_service.mark_done(todo.id).unwrap();
82
83        let result = CompleteTodoUseCase::new(&db).execute(task_id, todo.task_index);
84        assert!(matches!(
85            result,
86            Err(TrackError::InvalidStatusTransition { .. })
87        ));
88    }
89}