envelope_cli/setup/
wizard.rs

1//! Setup wizard orchestration
2//!
3//! Coordinates the multi-step setup process for first-time users.
4
5use std::io::{self, Write};
6
7use crate::config::{paths::EnvelopePaths, settings::Settings};
8use crate::error::{EnvelopeError, EnvelopeResult};
9use crate::models::{Account, Money, TransactionStatus};
10use crate::services::account::AccountService;
11use crate::services::transaction::{CreateTransactionInput, TransactionService};
12use crate::storage::Storage;
13
14use super::steps::{
15    account::AccountSetupStep,
16    categories::{CategoriesSetupStep, CategoryChoice},
17    period::PeriodSetupStep,
18};
19
20/// Result of running the setup wizard
21pub struct SetupResult {
22    /// Whether setup was completed successfully
23    pub completed: bool,
24    /// The created account (if any)
25    pub account: Option<Account>,
26    /// Starting balance added to Available to Budget
27    pub starting_balance: Money,
28}
29
30/// The setup wizard state machine
31pub struct SetupWizard {
32    paths: EnvelopePaths,
33}
34
35impl SetupWizard {
36    /// Create a new setup wizard
37    pub fn new(paths: EnvelopePaths) -> Self {
38        Self { paths }
39    }
40
41    /// Check if setup is needed (first run)
42    pub fn needs_setup(&self, settings: &Settings) -> bool {
43        !settings.setup_completed && !self.paths.settings_file().exists()
44    }
45
46    /// Run the interactive setup wizard
47    pub fn run(&self, storage: &Storage, settings: &mut Settings) -> EnvelopeResult<SetupResult> {
48        println!();
49        println!("===========================================");
50        println!("  Welcome to EnvelopeCLI Setup Wizard!");
51        println!("===========================================");
52        println!();
53        println!("This wizard will help you set up your budget.");
54        println!("Press Ctrl+C at any time to cancel.");
55        println!();
56
57        // Confirm start
58        let confirm = prompt_string("Ready to begin? (yes/no) [yes]: ")?;
59        if !confirm.is_empty() && confirm.to_lowercase() != "yes" && confirm.to_lowercase() != "y" {
60            println!("Setup cancelled.");
61            return Ok(SetupResult {
62                completed: false,
63                account: None,
64                starting_balance: Money::zero(),
65            });
66        }
67
68        // Step 1: Create first account
69        let account_result = AccountSetupStep::run()?;
70
71        // Step 2: Category groups
72        let categories_result = CategoriesSetupStep::run()?;
73
74        // Step 3: Budget period
75        let period_result = PeriodSetupStep::run()?;
76
77        // Summary
78        println!();
79        println!("===========================================");
80        println!("  Setup Summary");
81        println!("===========================================");
82        println!();
83        println!(
84            "Account: {} ({})",
85            account_result.account.name, account_result.account.account_type
86        );
87        println!("Starting Balance: {}", account_result.starting_balance);
88        println!(
89            "Categories: {}",
90            match categories_result.choice {
91                CategoryChoice::UseDefaults => "Default categories",
92                CategoryChoice::Empty => "Empty (add your own)",
93                CategoryChoice::Customize => "Custom",
94            }
95        );
96        println!("Budget Period: {:?}", period_result.period_type);
97        println!();
98
99        let confirm = prompt_string("Apply these settings? (yes/no) [yes]: ")?;
100        if !confirm.is_empty() && confirm.to_lowercase() != "yes" && confirm.to_lowercase() != "y" {
101            println!("Setup cancelled.");
102            return Ok(SetupResult {
103                completed: false,
104                account: None,
105                starting_balance: Money::zero(),
106            });
107        }
108
109        // Apply settings
110        println!();
111        println!("Applying settings...");
112
113        // Initialize storage with defaults if using default categories
114        if categories_result.choice == CategoryChoice::UseDefaults {
115            crate::storage::init::initialize_storage(&self.paths)?;
116        }
117
118        // Save the account
119        let account_service = AccountService::new(storage);
120        let saved_account = account_service.create(
121            &account_result.account.name,
122            account_result.account.account_type,
123            account_result.starting_balance,
124            account_result.account.on_budget,
125        )?;
126
127        // Create starting balance transaction if non-zero
128        if !account_result.starting_balance.is_zero() {
129            let txn_service = TransactionService::new(storage);
130            let input = CreateTransactionInput {
131                account_id: saved_account.id,
132                date: chrono::Local::now().naive_local().date(),
133                amount: account_result.starting_balance,
134                payee_name: Some("Starting Balance".to_string()),
135                category_id: None,
136                memo: Some("Initial account balance".to_string()),
137                status: Some(TransactionStatus::Cleared),
138            };
139            txn_service.create(input)?;
140        }
141
142        // Update settings
143        settings.budget_period_type = period_result.period_type;
144        settings.setup_completed = true;
145        settings.save(&self.paths)?;
146
147        println!();
148        println!("Setup complete!");
149        println!();
150        println!("Your budget is ready. Here are some next steps:");
151        println!("  - Run 'envelope tui' to open the interactive interface");
152        println!("  - Run 'envelope budget assign' to allocate funds to categories");
153        println!("  - Run 'envelope transaction add' to record transactions");
154        println!();
155
156        Ok(SetupResult {
157            completed: true,
158            account: Some(saved_account),
159            starting_balance: account_result.starting_balance,
160        })
161    }
162
163    /// Run a minimal CLI setup (non-interactive)
164    pub fn run_minimal(
165        &self,
166        _storage: &Storage,
167        settings: &mut Settings,
168    ) -> EnvelopeResult<SetupResult> {
169        println!("Initializing EnvelopeCLI...");
170
171        // Initialize default categories
172        crate::storage::init::initialize_storage(&self.paths)?;
173
174        // Mark setup as complete
175        settings.setup_completed = true;
176        settings.save(&self.paths)?;
177
178        println!("Initialization complete!");
179
180        Ok(SetupResult {
181            completed: true,
182            account: None,
183            starting_balance: Money::zero(),
184        })
185    }
186}
187
188/// Prompt for a string input
189fn prompt_string(prompt: &str) -> EnvelopeResult<String> {
190    print!("{}", prompt);
191    io::stdout()
192        .flush()
193        .map_err(|e| EnvelopeError::Io(e.to_string()))?;
194
195    let mut input = String::new();
196    io::stdin()
197        .read_line(&mut input)
198        .map_err(|e| EnvelopeError::Io(e.to_string()))?;
199
200    Ok(input.trim().to_string())
201}