envelope_cli/storage/
init.rs

1//! Storage initialization
2//!
3//! Handles first-run setup and default data creation
4
5use crate::config::paths::EnvelopePaths;
6use crate::error::EnvelopeError;
7use crate::models::{Category, DefaultCategoryGroup};
8
9use super::categories::CategoryData;
10use super::file_io::write_json_atomic;
11
12/// Initialize storage for a fresh installation
13///
14/// Creates default category groups and basic structure
15pub fn initialize_storage(paths: &EnvelopePaths) -> Result<(), EnvelopeError> {
16    // Ensure all directories exist
17    paths.ensure_directories()?;
18
19    // Create default categories if budget.json doesn't exist
20    if !paths.budget_file().exists() {
21        create_default_categories(paths)?;
22    }
23
24    Ok(())
25}
26
27/// Create default category groups and some starter categories
28fn create_default_categories(paths: &EnvelopePaths) -> Result<(), EnvelopeError> {
29    let mut groups = Vec::new();
30    let mut categories = Vec::new();
31
32    // Create default groups with predefined categories
33    for (i, default_group) in DefaultCategoryGroup::all().iter().enumerate() {
34        let group = default_group.to_group(i as i32);
35        let group_id = group.id;
36        groups.push(group);
37
38        // Add default categories for each group
39        let default_cats = match default_group {
40            DefaultCategoryGroup::Bills => vec![
41                "Rent/Mortgage",
42                "Electric",
43                "Water",
44                "Internet",
45                "Phone",
46                "Insurance",
47            ],
48            DefaultCategoryGroup::Needs => {
49                vec!["Groceries", "Transportation", "Medical", "Household"]
50            }
51            DefaultCategoryGroup::Wants => {
52                vec!["Dining Out", "Entertainment", "Shopping", "Subscriptions"]
53            }
54            DefaultCategoryGroup::Savings => vec!["Emergency Fund", "Vacation", "Large Purchases"],
55        };
56
57        for (j, cat_name) in default_cats.into_iter().enumerate() {
58            let category = Category::with_sort_order(cat_name, group_id, j as i32);
59            categories.push(category);
60        }
61    }
62
63    let data = CategoryData { groups, categories };
64    write_json_atomic(paths.budget_file(), &data)?;
65
66    Ok(())
67}
68
69/// Check if storage needs initialization
70pub fn needs_initialization(paths: &EnvelopePaths) -> bool {
71    !paths.budget_file().exists()
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::models::CategoryGroup;
78    use tempfile::TempDir;
79
80    #[test]
81    fn test_initialize_storage() {
82        let temp_dir = TempDir::new().unwrap();
83        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
84
85        assert!(needs_initialization(&paths));
86
87        initialize_storage(&paths).unwrap();
88
89        assert!(!needs_initialization(&paths));
90        assert!(paths.budget_file().exists());
91        assert!(paths.data_dir().exists());
92        assert!(paths.backup_dir().exists());
93    }
94
95    #[test]
96    fn test_default_categories_created() {
97        let temp_dir = TempDir::new().unwrap();
98        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
99
100        initialize_storage(&paths).unwrap();
101
102        // Load and verify
103        let content = std::fs::read_to_string(paths.budget_file()).unwrap();
104        let data: CategoryData = serde_json::from_str(&content).unwrap();
105
106        // Should have 4 default groups
107        assert_eq!(data.groups.len(), 4);
108
109        // Should have categories in each group
110        assert!(!data.categories.is_empty());
111
112        // Verify group names
113        let group_names: Vec<_> = data.groups.iter().map(|g| g.name.as_str()).collect();
114        assert!(group_names.contains(&"Bills"));
115        assert!(group_names.contains(&"Needs"));
116        assert!(group_names.contains(&"Wants"));
117        assert!(group_names.contains(&"Savings"));
118    }
119
120    #[test]
121    fn test_doesnt_overwrite_existing() {
122        let temp_dir = TempDir::new().unwrap();
123        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
124
125        // First initialization
126        initialize_storage(&paths).unwrap();
127
128        // Modify the file
129        let custom_data = CategoryData {
130            groups: vec![CategoryGroup::new("Custom Group")],
131            categories: vec![],
132        };
133        write_json_atomic(paths.budget_file(), &custom_data).unwrap();
134
135        // Second initialization should not overwrite
136        initialize_storage(&paths).unwrap();
137
138        let content = std::fs::read_to_string(paths.budget_file()).unwrap();
139        let data: CategoryData = serde_json::from_str(&content).unwrap();
140
141        // Should still have our custom data
142        assert_eq!(data.groups.len(), 1);
143        assert_eq!(data.groups[0].name, "Custom Group");
144    }
145}