Skip to main content

server/command/
mod.rs

1use derive_more::From;
2use finance::{
3    account::Account, commodity::Commodity, error::FinanceError, price::Price, split::Split,
4    tag::Tag, transaction::Transaction,
5};
6use num_rational::Rational64;
7use serde::{Deserialize, Serialize};
8use sqlx::{
9    types::Uuid,
10    types::chrono::{DateTime, Utc},
11};
12use std::{
13    collections::HashMap,
14    fmt::{self, Debug},
15};
16use thiserror::Error;
17
18use crate::{config::ConfigError, error::ServerError};
19
20pub mod account;
21pub mod commodity;
22pub mod config;
23pub mod report;
24pub mod split;
25pub mod ssh_key;
26pub mod transaction;
27pub mod user;
28
29#[derive(Debug, Clone)]
30pub struct CommodityInfo {
31    pub commodity_id: Uuid,
32    pub symbol: String,
33    pub name: String,
34}
35
36#[derive(Debug, Clone)]
37pub struct PaginationInfo {
38    pub total_count: i64,
39    pub limit: i64,
40    pub offset: i64,
41    pub has_more: bool,
42}
43
44#[derive(Debug, From)]
45pub enum FinanceEntity {
46    Commodity(Commodity),
47    Tag(Tag),
48    Split(Split),
49    Transaction(Transaction),
50    Price(Price),
51    Account(Account),
52}
53
54#[derive(Debug, From)]
55pub enum Argument {
56    String(String),
57    Rational(Rational64),
58    Uuid(Uuid),
59    Data(Vec<u8>),
60    FinanceEntity(FinanceEntity),
61    FinanceEntities(Vec<FinanceEntity>),
62    DateTime(DateTime<Utc>),
63}
64
65#[derive(Debug, Clone, Serialize)]
66pub struct CommodityAmount {
67    pub commodity_id: Uuid,
68    pub commodity_symbol: String,
69    pub amount: Rational64,
70}
71
72#[derive(Debug, Clone, Serialize)]
73pub struct ReportNode {
74    pub account_id: Uuid,
75    pub account_name: String,
76    pub account_path: String,
77    pub depth: usize,
78    pub account_type: Option<String>,
79    pub amounts: Vec<CommodityAmount>,
80    pub children: Vec<ReportNode>,
81}
82
83#[derive(Debug, Clone, Serialize)]
84pub struct ReportMeta {
85    pub date_from: Option<DateTime<Utc>>,
86    pub date_to: Option<DateTime<Utc>>,
87    pub target_commodity_id: Option<Uuid>,
88}
89
90#[derive(Debug, Clone, Serialize)]
91pub struct PeriodData {
92    pub label: Option<String>,
93    pub roots: Vec<ReportNode>,
94}
95
96#[derive(Debug, Clone, Serialize)]
97pub struct ReportData {
98    pub meta: ReportMeta,
99    pub periods: Vec<PeriodData>,
100}
101
102/// One pane of an `ActivityReport`. The caller supplies a label, a filter
103/// that selects which splits belong in the pane, and a display-time
104/// sign-flip hint. The command returns raw accountant values; `flip_sign`
105/// just travels with the data so UIs know how to render it.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ActivityGroup {
108    pub label: String,
109    pub filter: ReportFilter,
110    #[serde(default)]
111    pub flip_sign: bool,
112}
113
114#[derive(Debug, Clone, Serialize)]
115pub struct ActivityGroupResult {
116    pub label: String,
117    pub flip_sign: bool,
118    pub roots: Vec<ReportNode>,
119}
120
121#[derive(Debug, Clone, Serialize)]
122pub struct ActivityPeriod {
123    pub label: Option<String>,
124    pub groups: Vec<ActivityGroupResult>,
125}
126
127#[derive(Debug, Clone, Serialize)]
128pub struct ActivityData {
129    pub meta: ReportMeta,
130    pub periods: Vec<ActivityPeriod>,
131}
132
133/// Sentinel value used in `BreakdownRow::tag_value` for splits that have no
134/// value for the pivot tag.
135pub const UNCATEGORIZED_KEY: &str = "__uncategorized__";
136
137#[derive(Debug, Clone, Serialize)]
138pub struct BreakdownRow {
139    pub tag_value: String,
140    pub is_uncategorized: bool,
141    pub amounts: Vec<CommodityAmount>,
142}
143
144#[derive(Debug, Clone, Serialize)]
145pub struct BreakdownPeriod {
146    pub label: Option<String>,
147    pub rows: Vec<BreakdownRow>,
148}
149
150#[derive(Debug, Clone, Serialize)]
151pub struct BreakdownData {
152    pub meta: ReportMeta,
153    pub tag_name: String,
154    pub periods: Vec<BreakdownPeriod>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158#[serde(rename_all = "lowercase")]
159pub enum FilterEntity {
160    Account,
161    Transaction,
162    Split,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "lowercase")]
167pub enum PeriodGrouping {
168    Month,
169    Quarter,
170    Year,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
174#[serde(rename_all = "snake_case")]
175pub enum BreakdownSort {
176    #[default]
177    AmountDesc,
178    AmountAsc,
179    NameAsc,
180    NameDesc,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184#[serde(tag = "op", content = "args", rename_all = "snake_case")]
185pub enum ReportFilter {
186    AccountEq(Uuid),
187    AccountIn(Vec<Uuid>),
188    AccountSubtree(Uuid),
189    CounterpartyEq(Uuid),
190    CounterpartyIn(Vec<Uuid>),
191    CommodityEq(Uuid),
192    CommodityIn(Vec<Uuid>),
193    AmountGt(Rational64),
194    AmountLt(Rational64),
195    AmountEq(Rational64),
196    Tag {
197        entity: FilterEntity,
198        name: String,
199        value: String,
200    },
201    TagIn {
202        entity: FilterEntity,
203        name: String,
204        values: Vec<String>,
205    },
206    And(Vec<ReportFilter>),
207    Or(Vec<ReportFilter>),
208    Not(Box<ReportFilter>),
209}
210
211#[derive(Debug, Error)]
212pub enum CmdError {
213    #[error("Wrong arguments: {0}")]
214    Args(String),
215    #[error("Config: {0}")]
216    Config(#[from] ConfigError),
217    #[error("Database: {0}")]
218    DB(#[from] sqlx::Error),
219    #[error("Server: {0}")]
220    Server(#[from] ServerError),
221    #[error("Finance: {0}")]
222    Finance(#[from] FinanceError),
223    #[error("Script execution failed: {0}")]
224    Script(String),
225    #[error("This account is disabled")]
226    Disabled,
227}
228
229// Implementing CmdResult as an enum with String and Rational returning options
230#[derive(Debug)]
231pub enum CmdResult {
232    String(String),
233    Rational(Rational64),
234    Uuid(Uuid),
235    Bool(bool),
236    Data(Vec<u8>),
237    Lines(Vec<String>),
238    Entity(FinanceEntity),
239    Entities(Vec<FinanceEntity>),
240    TaggedEntities {
241        entities: Vec<(FinanceEntity, HashMap<String, FinanceEntity>)>,
242        pagination: Option<PaginationInfo>,
243    },
244    /// Transaction listings carry a per-row computed `amount` summary in a
245    /// dedicated slot so it never collides with a user tag named `amount`.
246    TaggedTransactions {
247        entities: Vec<(
248            FinanceEntity,
249            HashMap<String, FinanceEntity>,
250            Option<String>,
251        )>,
252        pagination: Option<PaginationInfo>,
253    },
254    CommodityInfoList(Vec<CommodityInfo>),
255    MultiCurrencyBalance(Vec<(Commodity, Rational64)>),
256    Report(ReportData),
257    Breakdown(BreakdownData),
258    Activity(ActivityData),
259    SshKeys(Vec<ssh_key::SshKeyRecord>),
260}
261
262impl From<String> for CmdResult {
263    fn from(s: String) -> Self {
264        CmdResult::String(s)
265    }
266}
267
268impl fmt::Display for CmdResult {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        match self {
271            CmdResult::String(s) => write!(f, "{s}"),
272            CmdResult::Rational(r) => write!(f, "{r}"),
273            CmdResult::Data(d) => write!(f, "CmdResult<Data>: \"{}\" bytes", d.len()),
274            CmdResult::Lines(l) => {
275                // Find the maximum width for alignment
276                let max_width = l.iter().map(std::string::String::len).max().unwrap_or(0);
277
278                // Write header
279                writeln!(f, "CmdResult<Lines>: {} items", l.len())?;
280
281                // Write each item in a column format
282                for (i, item) in l.iter().enumerate() {
283                    writeln!(f, "{:>4}. {:<width$}", i + 1, item, width = max_width)?;
284                }
285                Ok(())
286            }
287            CmdResult::Entity(e) => write!(f, "CmdResult<FinanceEntity>: \"{e:?}\""),
288            CmdResult::Entities(e) => write!(f, "CmdResult<FinanceEntities>: \"{}\"", e.len()),
289            CmdResult::TaggedEntities {
290                entities,
291                pagination,
292            } => match pagination {
293                Some(p) => write!(
294                    f,
295                    "CmdResult<TaggedEntities>: {} of {} (offset: {})",
296                    entities.len(),
297                    p.total_count,
298                    p.offset
299                ),
300                None => write!(f, "CmdResult<TaggedEntities>: \"{}\"", entities.len()),
301            },
302            CmdResult::TaggedTransactions {
303                entities,
304                pagination,
305            } => match pagination {
306                Some(p) => write!(
307                    f,
308                    "CmdResult<TaggedTransactions>: {} of {} (offset: {})",
309                    entities.len(),
310                    p.total_count,
311                    p.offset
312                ),
313                None => write!(f, "CmdResult<TaggedTransactions>: \"{}\"", entities.len()),
314            },
315            CmdResult::CommodityInfoList(e) => {
316                write!(f, "CmdResult<CommodityInfoList>: \"{}\"", e.len())
317            }
318            CmdResult::MultiCurrencyBalance(e) => {
319                write!(f, "CmdResult<MultiCurrencyBalance>: \"{}\"", e.len())
320            }
321            CmdResult::Report(r) => {
322                write!(f, "CmdResult<Report>: {} periods", r.periods.len())
323            }
324            CmdResult::Breakdown(b) => {
325                write!(
326                    f,
327                    "CmdResult<Breakdown>: tag={} periods={}",
328                    b.tag_name,
329                    b.periods.len()
330                )
331            }
332            CmdResult::Activity(a) => {
333                write!(
334                    f,
335                    "CmdResult<Activity>: {} periods, {} groups",
336                    a.periods.len(),
337                    a.periods.first().map_or(0, |p| p.groups.len()),
338                )
339            }
340            CmdResult::Uuid(id) => write!(f, "{id}"),
341            CmdResult::Bool(b) => write!(f, "{b}"),
342            CmdResult::SshKeys(keys) => {
343                write!(f, "CmdResult<SshKeys>: {} keys", keys.len())
344            }
345        }
346    }
347}