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!(
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 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}