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#[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
133pub 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#[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 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 let max_width = l.iter().map(std::string::String::len).max().unwrap_or(0);
277
278 writeln!(f, "CmdResult<Lines>: {} items", l.len())?;
280
281 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}