Files
ynabmunger/src/main.rs
T
james f7b645895d Format transaction amounts on output
This might want to use the same YNAB CSV format as `convert`.
2026-06-03 09:20:12 +02:00

212 lines
7.3 KiB
Rust

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<String>,
/// Alternatively, give the YNAB plan ID directly
#[arg(short = 'P', long, group = "planref")]
plan_id: Option<String>,
},
/// 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<String>,
/// Alternatively, give the YNAB plan ID directly
#[arg(short = 'P', long, group = "planref")]
plan_id: Option<String>,
/// The name of the YNAB account to import to
#[arg(short, long, group = "accountref")]
account: Option<String>,
/// Alternatively, give the YNAB account ID directly
#[arg(short = 'A', long, group = "accountref")]
account_id: Option<String>,
/// 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<String>,
},
/// 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<PathBuf>,
},
/// 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<String>,
/// Alternatively, give the YNAB plan ID directly
#[arg(short = 'P', long, group = "planref")]
plan_id: Option<String>,
/// The name of the YNAB account to import to
#[arg(short, long, group = "accountref")]
account: Option<String>,
/// Alternatively, give the YNAB account ID directly
#[arg(short = 'A', long, group = "accountref")]
account_id: Option<String>,
/// 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<PathBuf>,
},
}
#[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(())
}
}
}