1
use derive_more::From;
2
use finance::{
3
    account::Account, commodity::Commodity, error::FinanceError, price::Price, split::Split,
4
    tag::Tag, transaction::Transaction,
5
};
6
use num_rational::Rational64;
7
use serde::{Deserialize, Serialize};
8
use sqlx::{
9
    types::Uuid,
10
    types::chrono::{DateTime, Utc},
11
};
12
use std::{
13
    collections::HashMap,
14
    fmt::{self, Debug},
15
};
16
use thiserror::Error;
17

            
18
use crate::{config::ConfigError, error::ServerError};
19

            
20
pub mod account;
21
pub mod commodity;
22
pub mod config;
23
pub mod report;
24
pub mod split;
25
pub mod ssh_key;
26
pub mod transaction;
27
pub mod user;
28

            
29
#[derive(Debug, Clone)]
30
pub struct CommodityInfo {
31
    pub commodity_id: Uuid,
32
    pub symbol: String,
33
    pub name: String,
34
}
35

            
36
#[derive(Debug, Clone)]
37
pub 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)]
45
pub 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)]
55
pub 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
impl From<Argument> for String {
66
    fn from(arg: Argument) -> Self {
67
        match arg {
68
            Argument::String(s) => s,
69
            _ => panic!("Cannot convert {arg:?} to String"),
70
        }
71
    }
72
}
73

            
74
impl From<Argument> for Rational64 {
75
    fn from(arg: Argument) -> Self {
76
        match arg {
77
            Argument::Rational(r) => r,
78
            _ => panic!("Cannot convert {arg:?} to Rational64"),
79
        }
80
    }
81
}
82

            
83
impl From<Argument> for Vec<u8> {
84
    fn from(arg: Argument) -> Self {
85
        match arg {
86
            Argument::Data(d) => d,
87
            _ => panic!("Cannot convert {arg:?} to Vec<u8>"),
88
        }
89
    }
90
}
91

            
92
#[derive(Debug, Clone, Serialize)]
93
pub struct CommodityAmount {
94
    pub commodity_id: Uuid,
95
    pub commodity_symbol: String,
96
    pub amount: Rational64,
97
}
98

            
99
#[derive(Debug, Clone, Serialize)]
100
pub struct ReportNode {
101
    pub account_id: Uuid,
102
    pub account_name: String,
103
    pub account_path: String,
104
    pub depth: usize,
105
    pub account_type: Option<String>,
106
    pub amounts: Vec<CommodityAmount>,
107
    pub children: Vec<ReportNode>,
108
}
109

            
110
#[derive(Debug, Clone, Serialize)]
111
pub struct ReportMeta {
112
    pub date_from: Option<DateTime<Utc>>,
113
    pub date_to: Option<DateTime<Utc>>,
114
    pub target_commodity_id: Option<Uuid>,
115
}
116

            
117
#[derive(Debug, Clone, Serialize)]
118
pub struct PeriodData {
119
    pub label: Option<String>,
120
    pub roots: Vec<ReportNode>,
121
}
122

            
123
#[derive(Debug, Clone, Serialize)]
124
pub struct ReportData {
125
    pub meta: ReportMeta,
126
    pub periods: Vec<PeriodData>,
127
}
128

            
129
/// One pane of an `ActivityReport`. The caller supplies a label, a filter
130
/// that selects which splits belong in the pane, and a display-time
131
/// sign-flip hint. The command returns raw accountant values; `flip_sign`
132
/// just travels with the data so UIs know how to render it.
133
#[derive(Debug, Clone, Serialize, Deserialize)]
134
pub struct ActivityGroup {
135
    pub label: String,
136
    pub filter: ReportFilter,
137
    #[serde(default)]
138
    pub flip_sign: bool,
139
}
140

            
141
#[derive(Debug, Clone, Serialize)]
142
pub struct ActivityGroupResult {
143
    pub label: String,
144
    pub flip_sign: bool,
145
    pub roots: Vec<ReportNode>,
146
}
147

            
148
#[derive(Debug, Clone, Serialize)]
149
pub struct ActivityPeriod {
150
    pub label: Option<String>,
151
    pub groups: Vec<ActivityGroupResult>,
152
}
153

            
154
#[derive(Debug, Clone, Serialize)]
155
pub struct ActivityData {
156
    pub meta: ReportMeta,
157
    pub periods: Vec<ActivityPeriod>,
158
}
159

            
160
/// Sentinel value used in `BreakdownRow::tag_value` for splits that have no
161
/// value for the pivot tag.
162
pub const UNCATEGORIZED_KEY: &str = "__uncategorized__";
163

            
164
#[derive(Debug, Clone, Serialize)]
165
pub struct BreakdownRow {
166
    pub tag_value: String,
167
    pub is_uncategorized: bool,
168
    pub amounts: Vec<CommodityAmount>,
169
}
170

            
171
#[derive(Debug, Clone, Serialize)]
172
pub struct BreakdownPeriod {
173
    pub label: Option<String>,
174
    pub rows: Vec<BreakdownRow>,
175
}
176

            
177
#[derive(Debug, Clone, Serialize)]
178
pub struct BreakdownData {
179
    pub meta: ReportMeta,
180
    pub tag_name: String,
181
    pub periods: Vec<BreakdownPeriod>,
182
}
183

            
184
#[derive(Debug, Clone, Serialize, Deserialize)]
185
#[serde(rename_all = "lowercase")]
186
pub enum FilterEntity {
187
    Account,
188
    Transaction,
189
    Split,
190
}
191

            
192
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193
#[serde(rename_all = "lowercase")]
194
pub enum PeriodGrouping {
195
    Month,
196
    Quarter,
197
    Year,
198
}
199

            
200
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
201
#[serde(rename_all = "snake_case")]
202
pub enum BreakdownSort {
203
    #[default]
204
    AmountDesc,
205
    AmountAsc,
206
    NameAsc,
207
    NameDesc,
208
}
209

            
210
#[derive(Debug, Clone, Serialize, Deserialize)]
211
#[serde(tag = "op", content = "args", rename_all = "snake_case")]
212
pub enum ReportFilter {
213
    AccountEq(Uuid),
214
    AccountIn(Vec<Uuid>),
215
    AccountSubtree(Uuid),
216
    CounterpartyEq(Uuid),
217
    CounterpartyIn(Vec<Uuid>),
218
    CommodityEq(Uuid),
219
    CommodityIn(Vec<Uuid>),
220
    AmountGt(Rational64),
221
    AmountLt(Rational64),
222
    AmountEq(Rational64),
223
    Tag {
224
        entity: FilterEntity,
225
        name: String,
226
        value: String,
227
    },
228
    TagIn {
229
        entity: FilterEntity,
230
        name: String,
231
        values: Vec<String>,
232
    },
233
    And(Vec<ReportFilter>),
234
    Or(Vec<ReportFilter>),
235
    Not(Box<ReportFilter>),
236
}
237

            
238
#[derive(Debug, Error)]
239
pub enum CmdError {
240
    #[error("Wrong arguments: {0}")]
241
    Args(String),
242
    #[error("Config: {0}")]
243
    Config(#[from] ConfigError),
244
    #[error("Database: {0}")]
245
    DB(#[from] sqlx::Error),
246
    #[error("Server: {0}")]
247
    Server(#[from] ServerError),
248
    #[error("Finance: {0}")]
249
    Finance(#[from] FinanceError),
250
    #[error("Script execution failed: {0}")]
251
    Script(String),
252
}
253

            
254
// Implementing CmdResult as an enum with String and Rational returning options
255
#[derive(Debug)]
256
pub enum CmdResult {
257
    String(String),
258
    Rational(Rational64),
259
    Uuid(Uuid),
260
    Bool(bool),
261
    Data(Vec<u8>),
262
    Lines(Vec<String>),
263
    Entity(FinanceEntity),
264
    Entities(Vec<FinanceEntity>),
265
    TaggedEntities {
266
        entities: Vec<(FinanceEntity, HashMap<String, FinanceEntity>)>,
267
        pagination: Option<PaginationInfo>,
268
    },
269
    CommodityInfoList(Vec<CommodityInfo>),
270
    MultiCurrencyBalance(Vec<(Commodity, Rational64)>),
271
    Report(ReportData),
272
    Breakdown(BreakdownData),
273
    Activity(ActivityData),
274
    SshKeys(Vec<ssh_key::SshKeyRecord>),
275
}
276

            
277
impl From<String> for CmdResult {
278
69
    fn from(s: String) -> Self {
279
69
        CmdResult::String(s)
280
69
    }
281
}
282

            
283
pub struct LinesView<'view>(&'view CmdResult);
284
pub struct LinesViewMut<'view>(&'view mut CmdResult);
285

            
286
impl std::ops::Deref for LinesView<'_> {
287
    type Target = Vec<String>;
288

            
289
    fn deref(&self) -> &Self::Target {
290
        match self.0 {
291
            CmdResult::Lines(lines) => lines,
292
            _ => panic!("Attempted to use Lines view on non-Lines variant"),
293
        }
294
    }
295
}
296

            
297
impl std::ops::Deref for LinesViewMut<'_> {
298
    type Target = Vec<String>;
299

            
300
    fn deref(&self) -> &Self::Target {
301
        match self.0 {
302
            &mut CmdResult::Lines(ref lines) => lines,
303
            _ => panic!("Attempted to use Lines view on non-Lines variant"),
304
        }
305
    }
306
}
307

            
308
impl std::ops::DerefMut for LinesViewMut<'_> {
309
    fn deref_mut(&mut self) -> &mut Self::Target {
310
        match self.0 {
311
            &mut CmdResult::Lines(ref mut lines) => lines,
312
            _ => panic!("Attempted to use Lines view on non-Lines variant"),
313
        }
314
    }
315
}
316

            
317
impl CmdResult {
318
    #[must_use]
319
    pub fn as_lines(&self) -> LinesView<'_> {
320
        LinesView(self)
321
    }
322

            
323
    pub fn as_lines_mut(&mut self) -> LinesViewMut<'_> {
324
        LinesViewMut(self)
325
    }
326
}
327

            
328
impl fmt::Display for CmdResult {
329
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330
1
        match self {
331
1
            CmdResult::String(s) => write!(f, "{s}"),
332
            CmdResult::Rational(r) => write!(f, "{r}"),
333
            CmdResult::Data(d) => write!(f, "CmdResult<Data>: \"{}\" bytes", d.len()),
334
            CmdResult::Lines(l) => {
335
                // Find the maximum width for alignment
336
                let max_width = l.iter().map(std::string::String::len).max().unwrap_or(0);
337

            
338
                // Write header
339
                writeln!(f, "CmdResult<Lines>: {} items", l.len())?;
340

            
341
                // Write each item in a column format
342
                for (i, item) in l.iter().enumerate() {
343
                    writeln!(f, "{:>4}. {:<width$}", i + 1, item, width = max_width)?;
344
                }
345
                Ok(())
346
            }
347
            CmdResult::Entity(e) => write!(f, "CmdResult<FinanceEntity>: \"{e:?}\""),
348
            CmdResult::Entities(e) => write!(f, "CmdResult<FinanceEntities>: \"{}\"", e.len()),
349
            CmdResult::TaggedEntities {
350
                entities,
351
                pagination,
352
            } => match pagination {
353
                Some(p) => write!(
354
                    f,
355
                    "CmdResult<TaggedEntities>: {} of {} (offset: {})",
356
                    entities.len(),
357
                    p.total_count,
358
                    p.offset
359
                ),
360
                None => write!(f, "CmdResult<TaggedEntities>: \"{}\"", entities.len()),
361
            },
362
            CmdResult::CommodityInfoList(e) => {
363
                write!(f, "CmdResult<CommodityInfoList>: \"{}\"", e.len())
364
            }
365
            CmdResult::MultiCurrencyBalance(e) => {
366
                write!(f, "CmdResult<MultiCurrencyBalance>: \"{}\"", e.len())
367
            }
368
            CmdResult::Report(r) => {
369
                write!(f, "CmdResult<Report>: {} periods", r.periods.len())
370
            }
371
            CmdResult::Breakdown(b) => {
372
                write!(
373
                    f,
374
                    "CmdResult<Breakdown>: tag={} periods={}",
375
                    b.tag_name,
376
                    b.periods.len()
377
                )
378
            }
379
            CmdResult::Activity(a) => {
380
                write!(
381
                    f,
382
                    "CmdResult<Activity>: {} periods, {} groups",
383
                    a.periods.len(),
384
                    a.periods.first().map_or(0, |p| p.groups.len()),
385
                )
386
            }
387
            CmdResult::Uuid(id) => write!(f, "{id}"),
388
            CmdResult::Bool(b) => write!(f, "{b}"),
389
            CmdResult::SshKeys(keys) => {
390
                write!(f, "CmdResult<SshKeys>: {} keys", keys.len())
391
            }
392
        }
393
1
    }
394
}