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
65impl 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
74impl 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
83impl 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)]
93pub struct CommodityAmount {
94 pub commodity_id: Uuid,
95 pub commodity_symbol: String,
96 pub amount: Rational64,
97}
98
99#[derive(Debug, Clone, Serialize)]
100pub 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)]
111pub 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)]
118pub struct PeriodData {
119 pub label: Option<String>,
120 pub roots: Vec<ReportNode>,
121}
122
123#[derive(Debug, Clone, Serialize)]
124pub struct ReportData {
125 pub meta: ReportMeta,
126 pub periods: Vec<PeriodData>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
134pub 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)]
142pub struct ActivityGroupResult {
143 pub label: String,
144 pub flip_sign: bool,
145 pub roots: Vec<ReportNode>,
146}
147
148#[derive(Debug, Clone, Serialize)]
149pub struct ActivityPeriod {
150 pub label: Option<String>,
151 pub groups: Vec<ActivityGroupResult>,
152}
153
154#[derive(Debug, Clone, Serialize)]
155pub struct ActivityData {
156 pub meta: ReportMeta,
157 pub periods: Vec<ActivityPeriod>,
158}
159
160pub const UNCATEGORIZED_KEY: &str = "__uncategorized__";
163
164#[derive(Debug, Clone, Serialize)]
165pub struct BreakdownRow {
166 pub tag_value: String,
167 pub is_uncategorized: bool,
168 pub amounts: Vec<CommodityAmount>,
169}
170
171#[derive(Debug, Clone, Serialize)]
172pub struct BreakdownPeriod {
173 pub label: Option<String>,
174 pub rows: Vec<BreakdownRow>,
175}
176
177#[derive(Debug, Clone, Serialize)]
178pub 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")]
186pub enum FilterEntity {
187 Account,
188 Transaction,
189 Split,
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "lowercase")]
194pub 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")]
202pub 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")]
212pub 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)]
239pub 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#[derive(Debug)]
256pub 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
277impl From<String> for CmdResult {
278 fn from(s: String) -> Self {
279 CmdResult::String(s)
280 }
281}
282
283pub struct LinesView<'view>(&'view CmdResult);
284pub struct LinesViewMut<'view>(&'view mut CmdResult);
285
286impl 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
297impl 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
308impl 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
317impl 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
328impl fmt::Display for CmdResult {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 match self {
331 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 let max_width = l.iter().map(std::string::String::len).max().unwrap_or(0);
337
338 writeln!(f, "CmdResult<Lines>: {} items", l.len())?;
340
341 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 }
394}