Skip to main content

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!(
148                            "⚠️  Your subscription was canceled but you have {} days remaining.",
149                            days
150                        );
151                        println!(
152                            "    After that, you'll be downgraded to Free tier ({} limit).",
153                            format_bytes(FREE_TIER_MAX_FILE_SIZE)
154                        );
155                    }
156                    if let Some(ref end_date) = cached.plan_end_date {
157                        println!("    Plan ends: {}", end_date);
158                    }
159                }
160
161                println!();
162                println!("Capacity: {}", format_bytes(cached.capacity_bytes()));
163                println!("Storage Used: {}", format_bytes(cached.total_storage_bytes));
164                println!();
165                println!("Features: {}", cached.ticket.features.join(", "));
166                println!();
167                let expires_in = cached.ticket.expires_in_secs();
168                if expires_in > 0 {
169                    let hours = expires_in / 3600;
170                    let mins = (expires_in % 3600) / 60;
171                    println!("Ticket expires in: {}h {}m", hours, mins);
172                } else {
173                    println!("Ticket expired (run `memvid plan sync` to refresh)");
174                }
175            }
176        }
177        Err(err) => {
178            if args.json {
179                let output = json!({
180                    "error": err.to_string(),
181                    "plan": "unknown",
182                    "authenticated": true,
183                });
184                println!("{}", serde_json::to_string_pretty(&output)?);
185            } else {
186                println!("Failed to get plan information: {}", err);
187                println!();
188                println!("Try syncing your plan ticket:");
189                println!("  memvid plan sync");
190            }
191        }
192    }
193
194    Ok(())
195}
196
197fn handle_plan_sync(config: &CliConfig, args: PlanSyncArgs) -> Result<()> {
198    if config.api_key.is_none() {
199        anyhow::bail!(
200            "API key required. Set it with:\n  export MEMVID_API_KEY=your_api_key\n\n\
201             Get your API key at: https://memvid.com/dashboard/api-keys"
202        );
203    }
204
205    let cached = org_ticket_cache::refresh(config)?;
206    let in_grace_period = cached.is_in_grace_period();
207    let grace_days = cached.grace_period_days_remaining();
208
209    if args.json {
210        let mut output = json!({
211            "success": true,
212            "plan": cached.plan_id,
213            "plan_name": cached.plan_name,
214            "capacity_bytes": cached.capacity_bytes(),
215            "capacity": format_bytes(cached.capacity_bytes()),
216            "features": cached.ticket.features,
217            "org_id": cached.org_id,
218            "org_name": cached.org_name,
219            "subscription_status": cached.subscription_status,
220            "expires_at": cached.ticket.expires_at,
221        });
222        if let Some(ref end_date) = cached.plan_end_date {
223            output["plan_end_date"] = json!(end_date);
224        }
225        if in_grace_period {
226            output["in_grace_period"] = json!(true);
227            output["grace_period_days_remaining"] = json!(grace_days);
228        }
229        println!("{}", serde_json::to_string_pretty(&output)?);
230    } else {
231        println!("✓ Plan synced successfully");
232        println!();
233        println!("Plan: {}", cached.plan_name);
234        println!("Organisation: {}", cached.org_name);
235        println!("Capacity: {}", format_bytes(cached.capacity_bytes()));
236        println!("Subscription: {}", cached.subscription_status);
237
238        // Show grace period warning if applicable
239        if in_grace_period {
240            println!();
241            if let Some(days) = grace_days {
242                println!(
243                    "⚠️  Subscription canceled - {} days remaining before downgrade.",
244                    days
245                );
246            }
247            if let Some(ref end_date) = cached.plan_end_date {
248                println!("    Plan ends: {}", end_date);
249            }
250        }
251
252        println!();
253        let expires_in = cached.ticket.expires_in_secs();
254        let hours = expires_in / 3600;
255        let mins = (expires_in % 3600) / 60;
256        println!("Ticket valid for: {}h {}m", hours, mins);
257    }
258
259    Ok(())
260}
261
262fn handle_plan_clear(config: &CliConfig, args: PlanClearArgs) -> Result<()> {
263    org_ticket_cache::clear(config)?;
264
265    if args.json {
266        let output = json!({
267            "success": true,
268            "message": "Plan ticket cache cleared",
269        });
270        println!("{}", serde_json::to_string_pretty(&output)?);
271    } else {
272        println!("✓ Plan ticket cache cleared");
273    }
274
275    Ok(())
276}