Skip to main content

yuki_cli/cli/
accounts.rs

1use crate::cli::setup_domain;
2use crate::client::accounting::AccountingClient;
3use crate::client::accounting_info::AccountingInfoClient;
4use crate::client::soap_client::SoapClient;
5use crate::config::Config;
6use crate::error::YukiError;
7use crate::output::{OutputFormat, format_json, format_table, is_tty};
8use crate::period::parse_period;
9
10pub async fn balance(
11    config: &Config,
12    admin: Option<&str>,
13    account: Option<&str>,
14    period: Option<&str>,
15    format: Option<&str>,
16) -> Result<(), YukiError> {
17    let (start, _end) = resolve_period(period)?;
18    let gl_code = account.unwrap_or("");
19    let (client, entry) = setup_domain(config, admin).await?;
20    let xml = client
21        .gl_account_balance(&entry.admin_id, gl_code, &start)
22        .await?;
23    let balance = SoapClient::parse_single_result(&xml, "GLAccountBalanceResult").unwrap_or(xml);
24
25    let headers = vec!["Account".into(), "Date".into(), "Balance".into()];
26    let rows = vec![vec![gl_code.to_string(), start, balance]];
27
28    let fmt = OutputFormat::from_flag(format, is_tty());
29    match fmt {
30        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
31        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
32    }
33    Ok(())
34}
35
36pub async fn transactions(
37    config: &Config,
38    admin: Option<&str>,
39    account: Option<&str>,
40    period: Option<&str>,
41    format: Option<&str>,
42) -> Result<(), YukiError> {
43    let (start, end) = resolve_period(period)?;
44    let gl_code = account.unwrap_or("");
45    let (client, entry) = setup_domain(config, admin).await?;
46    let xml = client
47        .gl_account_transactions(&entry.admin_id, gl_code, &start, &end)
48        .await?;
49    let transactions = AccountingClient::parse_gl_transactions(&xml)?;
50
51    let headers = vec![
52        "ID".into(),
53        "Date".into(),
54        "Amount".into(),
55        "Description".into(),
56    ];
57    let rows: Vec<Vec<String>> = transactions
58        .iter()
59        .map(|t| {
60            vec![
61                t.id.clone(),
62                t.date.clone(),
63                t.amount.clone(),
64                t.description.clone(),
65            ]
66        })
67        .collect();
68
69    let fmt = OutputFormat::from_flag(format, is_tty());
70    match fmt {
71        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
72        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
73    }
74    Ok(())
75}
76
77pub async fn scheme(
78    config: &Config,
79    admin: Option<&str>,
80    format: Option<&str>,
81) -> Result<(), YukiError> {
82    let entry = config.resolve_admin(admin)?;
83    let mut client = AccountingInfoClient::new();
84    client.authenticate(&config.api_key).await?;
85    let accounts = client.get_gl_account_scheme(&entry.admin_id).await?;
86
87    let headers = vec!["Code".into(), "Description".into(), "Type".into()];
88    let rows: Vec<Vec<String>> = accounts
89        .into_iter()
90        .map(|a| vec![a.code, a.description, a.account_type])
91        .collect();
92
93    let fmt = OutputFormat::from_flag(format, is_tty());
94    match fmt {
95        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
96        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
97    }
98    Ok(())
99}
100
101pub async fn start_balance(
102    config: &Config,
103    admin: Option<&str>,
104    year: Option<&str>,
105    format: Option<&str>,
106) -> Result<(), YukiError> {
107    let bookyear = year.unwrap_or(&current_year().to_string()).to_string();
108    let entry = config.resolve_admin(admin)?;
109    let mut client = AccountingInfoClient::new();
110    client.authenticate(&config.api_key).await?;
111    let balances = client
112        .get_start_balance_by_gl_account(&entry.admin_id, &bookyear)
113        .await?;
114
115    let headers = vec!["GL Account".into(), "Description".into(), "Balance".into()];
116    let rows: Vec<Vec<String>> = balances
117        .into_iter()
118        .map(|b| vec![b.gl_account_code, b.description, b.balance])
119        .collect();
120
121    let fmt = OutputFormat::from_flag(format, is_tty());
122    match fmt {
123        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
124        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
125    }
126    Ok(())
127}
128
129pub async fn revenue(
130    config: &Config,
131    admin: Option<&str>,
132    period: Option<&str>,
133    format: Option<&str>,
134) -> Result<(), YukiError> {
135    let (start, end) = resolve_period(period)?;
136    let (client, entry) = setup_domain(config, admin).await?;
137    let amount = client.net_revenue(&entry.admin_id, &start, &end).await?;
138
139    let headers = vec!["Period".into(), "Net Revenue".into()];
140    let rows = vec![vec![format!("{start} — {end}"), amount]];
141
142    let fmt = OutputFormat::from_flag(format, is_tty());
143    match fmt {
144        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
145        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
146    }
147    Ok(())
148}
149
150/// Resolve an optional period string to (start_date, end_date).
151///
152/// When no period is given, defaults to the current calendar year.
153fn resolve_period(period: Option<&str>) -> Result<(String, String), YukiError> {
154    match period {
155        Some(p) => parse_period(p),
156        None => {
157            let year = current_year();
158            Ok((format!("{year}-01-01"), format!("{year}-12-31")))
159        }
160    }
161}
162
163fn current_year() -> u32 {
164    use std::time::{SystemTime, UNIX_EPOCH};
165    let secs = SystemTime::now()
166        .duration_since(UNIX_EPOCH)
167        .unwrap_or_default()
168        .as_secs();
169    // Approximate: seconds since epoch divided by seconds per year
170    1970 + (secs / 31_557_600) as u32
171}