envelope_cli/storage/
init.rs1use 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
12pub fn initialize_storage(paths: &EnvelopePaths) -> Result<(), EnvelopeError> {
16 paths.ensure_directories()?;
18
19 if !paths.budget_file().exists() {
21 create_default_categories(paths)?;
22 }
23
24 Ok(())
25}
26
27fn create_default_categories(paths: &EnvelopePaths) -> Result<(), EnvelopeError> {
29 let mut groups = Vec::new();
30 let mut categories = Vec::new();
31
32 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 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
69pub 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 let content = std::fs::read_to_string(paths.budget_file()).unwrap();
104 let data: CategoryData = serde_json::from_str(&content).unwrap();
105
106 assert_eq!(data.groups.len(), 4);
108
109 assert!(!data.categories.is_empty());
111
112 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 initialize_storage(&paths).unwrap();
127
128 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 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 assert_eq!(data.groups.len(), 1);
143 assert_eq!(data.groups[0].name, "Custom Group");
144 }
145}