Fix API interaction and add transactions command
This commit is contained in:
+55
-1
@@ -32,6 +32,31 @@ enum Command {
|
||||
#[arg(short = 'P', long, group = "planref")]
|
||||
plan_id: Option<String>,
|
||||
},
|
||||
// List transactions in the last 30 days
|
||||
//
|
||||
// You have to give a plan to list transactions from. You can optionally
|
||||
// also give an account to show only transactions from that account.
|
||||
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>,
|
||||
},
|
||||
/// 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"))]
|
||||
@@ -116,13 +141,42 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
plan_id,
|
||||
} => {
|
||||
let plan = Lookup::from_options(plan, plan_id)?;
|
||||
let accounts = Ynab::new(&token).list_accounts(plan)?;
|
||||
let accounts = Ynab::list_accounts(&Ynab::new(&token), plan)?;
|
||||
println!("Available accounts in plan:");
|
||||
for account in accounts {
|
||||
println!(" - {}", account);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Command::Transactions {
|
||||
token,
|
||||
plan,
|
||||
plan_id,
|
||||
account,
|
||||
account_id,
|
||||
} => {
|
||||
let plan = Lookup::from_options(plan, plan_id)?;
|
||||
|
||||
// Account is optional here
|
||||
let accountstr: &str;
|
||||
let account = match (account, account_id) {
|
||||
(None, None) => {
|
||||
accountstr = "";
|
||||
None
|
||||
}
|
||||
(name, id) => {
|
||||
accountstr = " for account";
|
||||
Some(Lookup::from_options(name, id)?)
|
||||
}
|
||||
};
|
||||
let transactions = Ynab::new(&token).list_transactions(plan, account)?;
|
||||
println!("Transactions{} in the last 30 days:", accountstr);
|
||||
for t in transactions {
|
||||
println!("{},{},{}", t.date, t.payee, t.amount);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Command::Import {
|
||||
token,
|
||||
plan,
|
||||
|
||||
+29
-2
@@ -80,6 +80,12 @@ impl Ynab {
|
||||
.send()
|
||||
.map_err(|e| format!("post error: {}", e))?;
|
||||
if res.status().is_success() {
|
||||
println!(
|
||||
"successful api response {}: {}",
|
||||
res.status(),
|
||||
res.text()
|
||||
.map_err(|e| format!("error retrieving api error: {}", e))?
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
@@ -114,6 +120,28 @@ impl Ynab {
|
||||
Ok(res.data.accounts.into_iter().map(|p| p.name).collect())
|
||||
}
|
||||
|
||||
pub fn list_transactions(
|
||||
&self,
|
||||
plan: Lookup,
|
||||
account: Option<Lookup>,
|
||||
) -> Result<Vec<Transaction>, String> {
|
||||
let plan_id = self.resolve_plan(plan)?;
|
||||
let account_id = match account {
|
||||
Some(account) => Some(self.resolve_account(&plan_id, account)?),
|
||||
None => None,
|
||||
};
|
||||
let since_date = chrono::Local::now().date_naive() - chrono::Duration::days(30);
|
||||
let since_date = since_date.format("%Y-%m-%d");
|
||||
let url = format!("plans/{}/transactions?since_date={}", plan_id, since_date);
|
||||
let raw = self.get(&url).map_err(|e| format!("request error {}", e))?;
|
||||
let res: types::YnabResponse<types::YnabTransactionList> =
|
||||
serde_json::from_str(&raw).map_err(|e| format!("parse error: {}", e))?;
|
||||
Ok(transform::from_ynab_transactions(
|
||||
&res.data.transactions,
|
||||
account_id.as_deref(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn upload(
|
||||
&self,
|
||||
transactions: &[Transaction],
|
||||
@@ -124,10 +152,9 @@ impl Ynab {
|
||||
let account_id = self.resolve_account(&plan_id, account)?;
|
||||
let request = format!("plans/{}/transactions", plan_id);
|
||||
let body = serde_json::to_string(&types::YnabTransactionList {
|
||||
transactions: transform::ynab_transactions(transactions, &account_id),
|
||||
transactions: transform::to_ynab_transactions(transactions, &account_id),
|
||||
})
|
||||
.map_err(|e| format!("transaction format error: {}", e))?;
|
||||
println!("{}", body);
|
||||
self.post(&request, &body)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+25
-3
@@ -2,7 +2,7 @@ use super::types;
|
||||
use crate::Transaction;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn ynab_transactions(
|
||||
pub(crate) fn to_ynab_transactions(
|
||||
transactions: &[Transaction],
|
||||
account_id: &str,
|
||||
) -> Vec<types::YnabTransaction> {
|
||||
@@ -16,13 +16,35 @@ pub(crate) fn ynab_transactions(
|
||||
*n
|
||||
};
|
||||
let y = types::YnabTransaction {
|
||||
import_id: format!("YNAB:{}:{}:{}", t.amount, t.date, id),
|
||||
import_id: Some(format!("YNAB:{}:{}:{}", t.amount, t.date, id)),
|
||||
date: t.date.clone(),
|
||||
amount: t.amount,
|
||||
payee_name: t.payee.clone(),
|
||||
payee_name: Some(t.payee.clone()),
|
||||
account_id: account_id.to_string(),
|
||||
cleared: "cleared".to_string(),
|
||||
};
|
||||
result.push(y);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn from_ynab_transactions(
|
||||
transactions: &[types::YnabTransaction],
|
||||
account_id: Option<&str>,
|
||||
) -> Vec<Transaction> {
|
||||
transactions
|
||||
.iter()
|
||||
.filter(|t| match account_id {
|
||||
Some(id) => t.account_id == id,
|
||||
None => true,
|
||||
})
|
||||
.map(|t| Transaction {
|
||||
date: t.date.to_owned(),
|
||||
payee: match &t.payee_name {
|
||||
Some(name) => name.to_owned(),
|
||||
None => String::new(),
|
||||
},
|
||||
amount: t.amount,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,12 +1,13 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Deserialize, Debug, Serialize)]
|
||||
pub(crate) struct YnabTransaction {
|
||||
pub(crate) import_id: String,
|
||||
pub(crate) import_id: Option<String>,
|
||||
pub(crate) date: String,
|
||||
pub(crate) amount: i64,
|
||||
pub(crate) payee_name: String,
|
||||
pub(crate) payee_name: Option<String>,
|
||||
pub(crate) account_id: String,
|
||||
pub(crate) cleared: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -36,7 +37,7 @@ pub(crate) struct YnabAccount {
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub(crate) struct YnabTransactionList {
|
||||
pub(crate) transactions: Vec<YnabTransaction>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user