use std::path::Path;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use crate::client::archive::ArchiveClient;
use crate::config::Config;
use crate::error::YukiError;
use crate::output::{OutputFormat, format_json, format_table, is_tty};
pub struct UploadOptions<'a> {
pub folder: &'a str,
pub amount: Option<f64>,
pub category: Option<&'a str>,
pub payment_method: Option<&'a str>,
pub project: Option<&'a str>,
pub remarks: Option<&'a str>,
pub currency: &'a str,
}
fn folder_id(name: &str) -> Result<i32, YukiError> {
match name {
"inkoop" | "purchase" => Ok(1),
"verkoop" | "sales" => Ok(2),
"bank" => Ok(3),
"personeel" | "personnel" => Ok(4),
"belasting" | "tax" => Ok(5),
"uitzoeken" => Ok(7),
"overig-financieel" | "other" => Ok(8),
_ => Err(YukiError::Config(format!("unknown folder: {name}"))),
}
}
pub async fn run(
config: &Config,
admin: Option<&str>,
file: &str,
options: UploadOptions<'_>,
format: Option<&str>,
quiet: bool,
) -> Result<(), YukiError> {
let fid = folder_id(options.folder)?;
let bytes = std::fs::read(file).map_err(|e| YukiError::Config(format!("{file}: {e}")))?;
let data_base64 = BASE64.encode(&bytes);
let filename = Path::new(file)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(file);
let entry = config.resolve_admin(admin)?;
let mut client = ArchiveClient::new();
client.authenticate(&config.api_key).await?;
let doc_id = match options.amount {
Some(amt) => {
client
.upload_document_with_data(
&entry.admin_id,
filename,
&data_base64,
fid,
options.currency,
amt,
options.category,
options.payment_method,
options.project,
options.remarks,
)
.await?
}
None => {
client
.upload_document(&entry.admin_id, filename, &data_base64, fid)
.await?
}
};
if quiet {
return Ok(());
}
let headers = vec!["Document ID".to_string()];
let rows = vec![vec![doc_id]];
let fmt = OutputFormat::from_flag(format, is_tty());
match fmt {
OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
}
Ok(())
}
pub async fn categories(config: &Config, format: Option<&str>) -> Result<(), YukiError> {
let mut client = ArchiveClient::new();
client.authenticate(&config.api_key).await?;
let cats = client.cost_categories().await?;
let headers = vec!["ID".to_string(), "Description".to_string()];
let rows: Vec<Vec<String>> = cats
.into_iter()
.map(|c| vec![c.id, c.description])
.collect();
let fmt = OutputFormat::from_flag(format, is_tty());
match fmt {
OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
}
Ok(())
}
pub async fn payment_methods(config: &Config, format: Option<&str>) -> Result<(), YukiError> {
let mut client = ArchiveClient::new();
client.authenticate(&config.api_key).await?;
let methods = client.payment_methods().await?;
let headers = vec!["ID".to_string(), "Description".to_string()];
let rows: Vec<Vec<String>> = methods
.into_iter()
.map(|m| vec![m.id, m.description])
.collect();
let fmt = OutputFormat::from_flag(format, is_tty());
match fmt {
OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn folder_id_inkoop() {
assert_eq!(folder_id("inkoop").unwrap(), 1);
assert_eq!(folder_id("purchase").unwrap(), 1);
}
#[test]
fn folder_id_verkoop() {
assert_eq!(folder_id("verkoop").unwrap(), 2);
assert_eq!(folder_id("sales").unwrap(), 2);
}
#[test]
fn folder_id_uitzoeken_default() {
assert_eq!(folder_id("uitzoeken").unwrap(), 7);
}
#[test]
fn folder_id_all_known() {
assert_eq!(folder_id("bank").unwrap(), 3);
assert_eq!(folder_id("personeel").unwrap(), 4);
assert_eq!(folder_id("personnel").unwrap(), 4);
assert_eq!(folder_id("belasting").unwrap(), 5);
assert_eq!(folder_id("tax").unwrap(), 5);
assert_eq!(folder_id("overig-financieel").unwrap(), 8);
assert_eq!(folder_id("other").unwrap(), 8);
}
#[test]
fn folder_id_unknown_errors() {
assert!(folder_id("nonexistent").is_err());
}
}