1use 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#[derive(Subcommand)]
16pub enum PlanCommand {
17 Show(PlanShowArgs),
19 Sync(PlanSyncArgs),
21 Clear(PlanClearArgs),
23}
24
25#[derive(Args)]
27pub struct PlanArgs {
28 #[command(subcommand)]
29 pub command: PlanCommand,
30}
31
32#[derive(Args)]
34pub struct PlanShowArgs {
35 #[arg(long)]
37 pub json: bool,
38}
39
40#[derive(Args)]
42pub struct PlanSyncArgs {
43 #[arg(long)]
45 pub json: bool,
46}
47
48#[derive(Args)]
50pub struct PlanClearArgs {
51 #[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 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 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 if let Ok(dt) = DateTime::parse_from_rfc3339(start_date) {
131 println!("Plan Started: {}", dt.format("%B %d, %Y"));
132 }
133 }
134 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 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 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}