use clap::{Parser, Subcommand}; use std::{io::stdout, path::PathBuf}; use ynabmunger::csv::{output_csv, read_transactions_from}; use ynabmunger::ynab::Client; #[derive(Subcommand)] enum Command { /// List available plans in YNAB Plans { /// Your YNAB token, available from developer settings in YNAB #[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)] token: String, }, /// List available accounts in a given plan in YNAB Accounts { /// Your YNAB token, available from developer settings in YNAB #[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)] token: String, /// The name of the YNAB plan to list #[arg(short, long, group = "planref")] plan: Option, /// Alternatively, give the YNAB plan ID directly #[arg(short = 'P', long, group = "planref")] plan_id: Option, }, /// List transactions /// /// You have to give a plan to list transactions from. You can optionally /// also give an account to show only transactions from that account. You can specify the /// number of days, the default is 30. Transactions { /// Your YNAB token, available from developer settings in YNAB #[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)] token: String, /// The name of the YNAB plan to list transactions from #[arg(short, long, group = "planref")] plan: Option, /// Alternatively, give the YNAB plan ID directly #[arg(short = 'P', long, group = "planref")] plan_id: Option, /// The name of the YNAB account to import to #[arg(short, long, group = "accountref")] account: Option, /// Alternatively, give the YNAB account ID directly #[arg(short = 'A', long, group = "accountref")] account_id: Option, /// The number of days to look back for transactions #[arg(short, long, default_value_t = 30)] days: i64, /// Filter transactions by a search term in the payee field. This will match any transaction /// whose payee contains the search term, case-insensitive. #[arg(short, long)] filter: Option, }, /// Convert from a bank export to a CSV you can import manually to YNAB Convert { #[arg(short, long, help = "Bank export format", default_value_t = String::from("bulder"))] format: String, inputs: Vec, }, /// Read a bank export and import it directly to an account in ynab Import { /// Your YNAB token, available from developer settings in YNAB #[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)] token: String, /// The name of the YNAB plan to import to #[arg(short, long, group = "planref")] plan: Option, /// Alternatively, give the YNAB plan ID directly #[arg(short = 'P', long, group = "planref")] plan_id: Option, /// The name of the YNAB account to import to #[arg(short, long, group = "accountref")] account: Option, /// Alternatively, give the YNAB account ID directly #[arg(short = 'A', long, group = "accountref")] account_id: Option, /// The format of the bank export you are importing /// /// This is different for every bank, and sometimes even the same /// bank can have different formats for different account types. #[arg(short, long, default_value_t = String::from("bulder"))] format: String, inputs: Vec, }, } #[derive(Parser)] struct Cli { #[command(subcommand)] command: Command, } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.command { Command::Plans { token } => { let plans = Client::new(&token).list_plans().await?; println!("Available plans:"); for plan in plans { println!(" - {}", plan); } Ok(()) } Command::Accounts { token, plan, plan_id, } => { let client = Client::new(&token); let plan = match (plan, plan_id) { (_, Some(id)) => Ok(client.plan_from_id(&id)), (Some(name), _) => Ok(client.plan_from_name(&name).await?), _ => Err(ynabmunger::Error::Text("no plan given".to_string())), }?; let accounts = plan.list_accounts().await?; println!("Available accounts in plan:"); for account in accounts { println!(" - {}", account); } Ok(()) } Command::Transactions { token, plan, plan_id, account, account_id, days, filter, } => { let client = Client::new(&token); let plan = match (plan, plan_id) { (_, Some(id)) => Ok(client.plan_from_id(&id)), (Some(name), _) => Ok(client.plan_from_name(&name).await?), _ => Err(ynabmunger::Error::Text("no plan given".to_string())), }?; // Account is optional here let (account, accountstr) = match (account, account_id) { (_, Some(id)) => (Some(plan.account_from_id(&id)), " for account"), (Some(name), _) => (Some(plan.account_from_name(&name).await?), " for account"), _ => (None, ""), }; let transactions = match account { Some(account) => account.list_transactions(days, filter.as_deref()).await?, None => { plan.list_transactions(None, days, filter.as_deref()) .await? } }; println!("Transactions{} in the last 30 days:", accountstr); for t in transactions { println!("{},{},{}", t.date, t.payee, t.format_amount()); } Ok(()) } Command::Import { token, plan, plan_id, account_id, account, format, inputs, } => { let client = Client::new(&token); let plan = match (plan, plan_id) { (_, Some(id)) => Ok(client.plan_from_id(&id)), (Some(name), _) => Ok(client.plan_from_name(&name).await?), _ => Err(ynabmunger::Error::Text("no plan given".to_string())), }?; let account = match (account, account_id) { (_, Some(id)) => Ok(plan.account_from_id(&id)), (Some(name), _) => Ok(plan.account_from_name(&name).await?), _ => Err(ynabmunger::Error::Text("no account given".to_string())), }?; let transactions = read_transactions_from(&inputs, &format)?; account.upload(&transactions).await?; Ok(()) } Command::Convert { format, inputs } => { let transactions = read_transactions_from(&inputs, &format)?; output_csv(stdout(), &transactions)?; Ok(()) } } }