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
#[derive(Debug, Clone, Serialize)]
66
pub struct CommodityAmount {
67
    pub commodity_id: Uuid,
68
    pub commodity_symbol: String,
69
    pub amount: Rational64,
70
}
71

            
72
#[derive(Debug, Clone, Serialize)]
73
pub 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)]
84
pub 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)]
91
pub struct PeriodData {
92
    pub label: Option<String>,
93
    pub roots: Vec<ReportNode>,
94
}
95

            
96
#[derive(Debug, Clone, Serialize)]
97
pub 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)]
107
pub 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)]
115
pub struct ActivityGroupResult {
116
    pub label: String,
117
    pub flip_sign: bool,
118
    pub roots: Vec<ReportNode>,
119
}
120

            
121
#[derive(Debug, Clone, Serialize)]
122
pub struct ActivityPeriod {
123
    pub label: Option<String>,
124
    pub groups: Vec<ActivityGroupResult>,
125
}
126

            
127
#[derive(Debug, Clone, Serialize)]
128
pub 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.
135
pub const UNCATEGORIZED_KEY: &str = "__uncategorized__";
136

            
137
#[derive(Debug, Clone, Serialize)]
138
pub struct BreakdownRow {
139
    pub tag_value: String,
140
    pub is_uncategorized: bool,
141
    pub amounts: Vec<CommodityAmount>,
142
}
143

            
144
#[derive(Debug, Clone, Serialize)]
145
pub struct BreakdownPeriod {
146
    pub label: Option<String>,
147
    pub rows: Vec<BreakdownRow>,
148
}
149

            
150
#[derive(Debug, Clone, Serialize)]
151
pub 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")]
159
pub enum FilterEntity {
160
    Account,
161
    Transaction,
162
    Split,
163
}
164

            
165
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166
#[serde(rename_all = "lowercase")]
167
pub 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")]
175
pub 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")]
185
pub 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)]
212
pub 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)]
231
pub 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

            
262
impl From<String> for CmdResult {
263
2878
    fn from(s: String) -> Self {
264
2878
        CmdResult::String(s)
265
2878
    }
266
}
267

            
268
impl fmt::Display for CmdResult {
269
1
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270
1
        match self {
271
1
            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
1
    }
347
}