Skip to main content

track/use_cases/
create_today_task.rs

1use crate::db::Database;
2use crate::models::{Task, TaskStatus};
3use crate::services::link_service::ScrapService;
4use crate::services::task_service::TaskService;
5use crate::services::todo_service::TodoService;
6use crate::utils::Result;
7use chrono::{Local, Utc};
8use rusqlite::{params, OptionalExtension};
9
10/// Creates or reuses the daily "today" task with atomic todo/scrap inheritance.
11pub struct CreateTodayTaskUseCase<'a> {
12    db: &'a Database,
13}
14
15impl<'a> CreateTodayTaskUseCase<'a> {
16    pub fn new(db: &'a Database) -> Self {
17        Self { db }
18    }
19
20    /// Returns today's task, creating it and inheriting pending work when needed.
21    pub fn get_or_create(&self) -> Result<Task> {
22        let today = Local::now().format("%Y-%m-%d").to_string();
23        let task_name = format!("Today: {}", today);
24
25        let conn = self.db.get_connection();
26        let active_query = format!(
27            "SELECT id FROM tasks WHERE is_today_task = 1 AND name = ?1 AND status = '{}'",
28            TaskStatus::ACTIVE
29        );
30        let mut stmt = conn.prepare(&active_query)?;
31
32        if let Some(task_id) = stmt
33            .query_row(params![task_name], |row| row.get::<_, i64>(0))
34            .optional()?
35        {
36            return TaskService::new(self.db).get_task(task_id);
37        }
38
39        self.create_with_inheritance(&task_name)
40    }
41
42    /// Creates a today task and inherits pending todos/scraps inside one transaction.
43    pub fn create_with_inheritance(&self, task_name: &str) -> Result<Task> {
44        self.db.with_transaction(|| {
45            let inherit_from = self.find_today_task_to_inherit_from()?;
46
47            let conn = self.db.get_connection();
48            let now = Utc::now().to_rfc3339();
49
50            conn.execute(
51                "UPDATE tasks SET is_today_task = 0 WHERE is_today_task = 1",
52                [],
53            )?;
54
55            conn.execute(
56                "INSERT INTO tasks (name, description, status, is_today_task, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
57                params![
58                    task_name,
59                    None::<String>,
60                    TaskStatus::Active.as_str(),
61                    1,
62                    now
63                ],
64            )?;
65
66            let task_id = conn.last_insert_rowid();
67
68            if let Some(from_task_id) = inherit_from {
69                let todo_service = TodoService::new(self.db);
70                let scrap_service = ScrapService::new(self.db);
71                let mapping =
72                    todo_service.copy_incomplete_todos_in_tx(from_task_id, task_id)?;
73                scrap_service.copy_linked_scraps_in_tx(from_task_id, task_id, &mapping)?;
74            }
75
76            self.db.set_current_task_id(task_id)?;
77            self.db.increment_rev("task")?;
78            TaskService::new(self.db).get_task(task_id)
79        })
80    }
81
82    fn find_today_task_to_inherit_from(&self) -> Result<Option<i64>> {
83        let conn = self.db.get_connection();
84        let active_query = format!(
85            "SELECT id FROM tasks WHERE is_today_task = 1 AND status = '{}' ORDER BY created_at DESC LIMIT 1",
86            TaskStatus::ACTIVE
87        );
88        let mut stmt = conn.prepare(&active_query)?;
89
90        stmt.query_row([], |row| row.get(0))
91            .optional()
92            .map_err(Into::into)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::services::{ScrapService, TodoService};
100
101    #[test]
102    fn create_today_task_inherits_pending_todos_and_scraps() {
103        let db = Database::new_in_memory().unwrap();
104        let task_service = TaskService::new(&db);
105        let todo_service = TodoService::new(&db);
106        let scrap_service = ScrapService::new(&db);
107        let use_case = CreateTodayTaskUseCase::new(&db);
108
109        let previous = task_service
110            .create_task("Today: 2026-01-01", None, None, None)
111            .unwrap();
112        db.get_connection()
113            .execute(
114                "UPDATE tasks SET is_today_task = 1 WHERE id = ?1",
115                rusqlite::params![previous.id],
116            )
117            .unwrap();
118        db.set_current_task_id(previous.id).unwrap();
119
120        todo_service
121            .add_todo(previous.id, "Carry over", false)
122            .unwrap();
123        scrap_service.add_scrap(previous.id, "linked note").unwrap();
124
125        let today_task = use_case.get_or_create().unwrap();
126        assert_ne!(today_task.id, previous.id);
127        assert!(today_task.is_today_task);
128
129        let inherited = todo_service.list_todos(today_task.id).unwrap();
130        assert_eq!(inherited.len(), 1);
131        assert_eq!(inherited[0].content, "Carry over");
132
133        let inherited_scraps = scrap_service.list_scraps(today_task.id).unwrap();
134        assert_eq!(inherited_scraps.len(), 1);
135        assert_eq!(inherited_scraps[0].content, "linked note");
136    }
137}