memvid_cli/commands/
plan.rs

1//! Plan management commands (show, sync)
2//!
3//! Commands for viewing and syncing plan/subscription information.
4
5use anyhow::Result;
6use chrono::DateTime;
7use clap::{Args, Subcommand};
8use serde_json::json;
9
10use crate::config::CliConfig;
11use crate::org_ticket_cache;
12use crate::utils::{format_bytes, FREE_TIER_MAX_FILE_SIZE};
13
14/// Plan management commands
15#[derive(Subcommand)]
16pub enum PlanCommand {
17    /// Show current plan and capacity
18    Show(PlanShowArgs),
19    /// Sync plan ticket from the dashboard
20    Sync(PlanSyncArgs),
21    /// Clear cached plan ticket
22    Clear(PlanClearArgs),
23}
24
25/// Arguments for the `plan` command
26#[derive(Args)]
27pub struct PlanArgs {
28    #[command(subcommand)]
29    pub command: PlanCommand,
30}
31
32/// Arguments for the `plan show` subcommand
33#[derive(Args)]
34pub struct PlanShowArgs {
35    /// Output as JSON
36    #[arg(long)]
37    pub json: bool,
38}
39
40/// Arguments for the `plan sync` subcommand
41#[derive(Args)]
42pub struct PlanSyncArgs {
43    /// Output as JSON
44    #[arg(long)]
45    pub json: bool,
46}
47
48/// Arguments for the `plan clear` subcommand
49#[derive(Args)]
50pub struct PlanClearArgs {
51    /// Output as JSON
52    #[arg(long)]
53    pub json: bool,
54}
55
56pub fn handle_plan(config: &CliConfig, args: PlanArgs) -> Result<()> {
57    match args.command {
58        PlanCommand::Show(show) => handle_plan_show(config, show),
59        PlanCommand::Sync(sync) => handle_plan_sync(config, sync),
60        PlanCommand::Clear(clear) => handle_plan_clear(config, clear),
61    }
62}
63
64fn handle_plan_show(config: &CliConfig, args: PlanShowArgs) -> Result<()> {
65    if config.api_key.is_none() {
66        if args.json {
67            let output = json!({
68                "plan": "free",
69                "capacity_bytes": FREE_TIER_MAX_FILE_SIZE,
70                "capacity": format_bytes(FREE_TIER_MAX_FILE_SIZE),
71                "features": ["core", "temporal_track", "clip", "whisper", "temporal_enrich"],
72                "authenticated": false,
73            });
74            println!("{}", serde_json::to_string_pretty(&output)?);
75        } else {
76            println!("Plan: Free (not authenticated)");
77            println!("Capacity: {}", format_bytes(FREE_TIER_MAX_FILE_SIZE));
78            println!();
79            println!("To unlock more capacity, set your API key:");
80            println!("  export MEMVID_API_KEY=your_api_key");
81            println!();
82            println!("Get your API key at: https://memvid.com/dashboard/api-keys");
83        }
84        return Ok(());
85    }
86
87    // Try to get cached ticket or show info to sync
88    match org_ticket_cache::get_or_refresh(config) {
89        Ok(cached) => {
90            let in_grace_period = cached.is_in_grace_period();
91            let grace_days = cached.grace_period_days_remaining();
92
93            if args.json {
94                let mut output = json!({
95                    "plan": cached.plan_id,
96                    "plan_name": cached.plan_name,
97                    "capacity_bytes": cached.capacity_bytes(),
98                    "capacity": format_bytes(cached.capacity_bytes()),
99                    "features": cached.ticket.features,
100                    "org_id": cached.org_id,
101                    "org_name": cached.org_name,
102                    "total_storage_bytes": cached.total_storage_bytes,
103                    "total_storage": format_bytes(cached.total_storage_bytes),
104                    "subscription_status": cached.subscription_status,
105                    "expires_at": cached.ticket.expires_at,
106                    "expires_in_secs": cached.ticket.expires_in_secs(),
107                    "authenticated": true,
108                });
109                // Add plan dates if available
110                if let Some(ref start_date) = cached.plan_start_date {
111                    output["plan_start_date"] = json!(start_date);
112                }
113                if let Some(ref period_end) = cached.current_period_end {
114                    output["current_period_end"] = json!(period_end);
115                }
116                if let Some(ref end_date) = cached.plan_end_date {
117                    output["plan_end_date"] = json!(end_date);
118                }
119                if in_grace_period {
120                    output["in_grace_period"] = json!(true);
121                    output["grace_period_days_remaining"] = json!(grace_days);
122                }
123                println!("{}", serde_json::to_string_pretty(&output)?);
124            } else {
125                println!("Plan: {}", cached.plan_name);
126                println!("Organisation: {}", cached.org_name);
127                println!("Subscription: {}", cached.subscription_status);
128                if let Some(ref start_date) = cached.plan_start_date {
129                    // Format the ISO date to a more readable format
130                    if let Ok(dt) = DateTime::parse_from_rfc3339(start_date) {
131                        println!("Plan Started: {}", dt.format("%B %d, %Y"));
132                    }
133                }
134                // Only show "Renews On" for active subscriptions (not canceled)
135                if cached.subscription_status != "canceled" {
136                    if let Some(ref period_end) = cached.current_period_end {
137                        if let Ok(dt) = DateTime::parse_from_rfc3339(period_end) {
138                            println!("Renews On: {}", dt.format("%B %d, %Y"));
139                        }
140                    }
141                }
142
143                // Show grace period warning if applicable
144                if in_grace_period {
145                    println!();
146                    if let Some(days) = grace_days {
147                        println!("⚠️  Your subscription was canceled but you have {} days remaining.", days);
148                        println!("    After that, you'll be downgraded to Free tier ({} limit).", format_bytes(FREE_TIER_MAX_FILE_SIZE));
149                    }
150                    if let Some(ref end_date) = cached.plan_end_date {
151                        println!("    Plan ends: {}", end_date);
152                    }
153                }
154
155                println!();
156                println!("Capacity: {}", format_bytes(cached.capacity_bytes()));
157                println!("Storage Used: {}", format_bytes(cached.total_storage_bytes));
158                println!();
159                println!("Features: {}", cached.ticket.features.join(", "));
160                println!();
161                let expires_in = cached.ticket.expires_in_secs();
162                if expires_in > 0 {
163                    let hours = expires_in / 3600;
164                    let mins = (expires_in % 3600) / 60;
165                    println!("Ticket expires in: {}h {}m", hours, mins);
166                } else {
167                    println!("Ticket expired (run `memvid plan sync` to refresh)");
168                }
169            }
170        }
171        Err(err) => {
172            if args.json {
173                let output = json!({
174                    "error": err.to_string(),
175                    "plan": "unknown",
176                    "authenticated": true,
177                });
178                println!("{}", serde_json::to_string_pretty(&output)?);
179            } else {
180                println!("Failed to get plan information: {}", err);
181                println!();
182                println!("Try syncing your plan ticket:");
183                println!("  memvid plan sync");
184            }
185        }
186    }
187
188    Ok(())
189}
190
191fn handle_plan_sync(config: &CliConfig, args: PlanSyncArgs) -> Result<()> {
192    if config.api_key.is_none() {
193        anyhow::bail!(
194            "API key required. Set it with:\n  export MEMVID_API_KEY=your_api_key\n\n\
195             Get your API key at: https://memvid.com/dashboard/api-keys"
196        );
197    }
198
199    let cached = org_ticket_cache::refresh(config)?;
200    let in_grace_period = cached.is_in_grace_period();
201    let grace_days = cached.grace_period_days_remaining();
202
203    if args.json {
204        let mut output = json!({
205            "success": true,
206            "plan": cached.plan_id,
207            "plan_name": cached.plan_name,
208            "capacity_bytes": cached.capacity_bytes(),
209            "capacity": format_bytes(cached.capacity_bytes()),
210            "features": cached.ticket.features,
211            "org_id": cached.org_id,
212            "org_name": cached.org_name,
213            "subscription_status": cached.subscription_status,
214            "expires_at": cached.ticket.expires_at,
215        });
216        if let Some(ref end_date) = cached.plan_end_date {
217            output["plan_end_date"] = json!(end_date);
218        }
219        if in_grace_period {
220            output["in_grace_period"] = json!(true);
221            output["grace_period_days_remaining"] = json!(grace_days);
222        }
223        println!("{}", serde_json::to_string_pretty(&output)?);
224    } else {
225        println!("✓ Plan synced successfully");
226        println!();
227        println!("Plan: {}", cached.plan_name);
228        println!("Organisation: {}", cached.org_name);
229        println!("Capacity: {}", format_bytes(cached.capacity_bytes()));
230        println!("Subscription: {}", cached.subscription_status);
231
232        // Show grace period warning if applicable
233        if in_grace_period {
234            println!();
235            if let Some(days) = grace_days {
236                println!("⚠️  Subscription canceled - {} days remaining before downgrade.", days);
237            }
238            if let Some(ref end_date) = cached.plan_end_date {
239                println!("    Plan ends: {}", end_date);
240            }
241        }
242
243        println!();
244        let expires_in = cached.ticket.expires_in_secs();
245        let hours = expires_in / 3600;
246        let mins = (expires_in % 3600) / 60;
247        println!("Ticket valid for: {}h {}m", hours, mins);
248    }
249
250    Ok(())
251}
252
253fn handle_plan_clear(config: &CliConfig, args: PlanClearArgs) -> Result<()> {
254    org_ticket_cache::clear(config)?;
255
256    if args.json {
257        let output = json!({
258            "success": true,
259            "message": "Plan ticket cache cleared",
260        });
261        println!("{}", serde_json::to_string_pretty(&output)?);
262    } else {
263        println!("✓ Plan ticket cache cleared");
264    }
265
266    Ok(())
267}