tackler_core/kernel/
settings.rs

1/*
2 * Tackler-NG 2023-2025
3 * SPDX-License-Identifier: Apache-2.0
4 */
5use crate::config::overlaps::{InputOverlap, OverlapConfig, StorageOverlap};
6use crate::config::{
7    AccountSelectors, Config, Export, ExportType, Kernel, PriceLookupType, Report, ReportType,
8    StorageType,
9};
10use crate::kernel::hash::Hash;
11use crate::kernel::price_lookup::PriceLookup;
12use crate::model::TxnAccount;
13use crate::model::price_entry::PriceDb;
14use crate::model::{AccountTreeNode, Commodity};
15use crate::{config, parser, tackler};
16use jiff::Zoned;
17use std::collections::HashMap;
18use std::path::PathBuf;
19use std::sync::Arc;
20use tackler_api::txn_header::Tag;
21use tackler_api::txn_ts::GroupBy;
22use tackler_rs::normalize_extension;
23
24#[derive(Debug, Default, Clone)]
25pub struct FileInput {
26    pub path: PathBuf,
27}
28
29#[derive(Debug, Clone)]
30pub struct FsInput {
31    pub path: PathBuf,
32    pub dir: PathBuf,
33    pub ext: String,
34}
35
36#[derive(Debug, Clone)]
37pub enum GitInputSelector {
38    CommitId(String),
39    Reference(String),
40}
41
42#[derive(Debug, Clone)]
43pub struct GitInput {
44    pub repo: PathBuf,
45    pub dir: String,
46    pub git_ref: GitInputSelector,
47    pub ext: String,
48}
49
50#[derive(Debug, Clone)]
51pub enum InputSettings {
52    File(FileInput),
53    Fs(FsInput),
54    Git(GitInput),
55}
56impl Default for InputSettings {
57    fn default() -> Self {
58        Self::File(FileInput::default())
59    }
60}
61
62#[derive(Debug, Default)]
63struct Commodities {
64    names: HashMap<String, Arc<Commodity>>,
65    permit_empty_commodity: bool,
66}
67
68impl Commodities {
69    fn default_empty_ok() -> Self {
70        Commodities {
71            names: HashMap::new(),
72            permit_empty_commodity: true,
73        }
74    }
75
76    fn from(cfg: &Config) -> Result<Commodities, tackler::Error> {
77        let cfg_comm = &cfg.transaction.commodities;
78        let permit_empty_commodity = cfg_comm.permit_empty_commodity.unwrap_or(false);
79
80        let comms =
81            cfg_comm.names.iter().try_fold(
82                HashMap::new(),
83                |mut chm, comm| match Commodity::from(comm.to_string()) {
84                    Ok(c) => {
85                        chm.insert(comm.into(), Arc::new(c));
86                        Ok(chm)
87                    }
88                    Err(e) => {
89                        let msg = format!("Invalid Chart of Commodities: {e}");
90                        Err(msg)
91                    }
92                },
93            )?;
94        Ok(Commodities {
95            names: comms,
96            permit_empty_commodity,
97        })
98    }
99}
100
101#[derive(Debug, Default)]
102struct AccountTrees {
103    defined_accounts: HashMap<String, Arc<AccountTreeNode>>,
104    synthetic_parents: HashMap<String, Arc<AccountTreeNode>>,
105}
106
107impl AccountTrees {
108    fn build_account_tree(
109        target_account_tree: &mut HashMap<String, Arc<AccountTreeNode>>,
110        atn: Arc<AccountTreeNode>,
111        other_account_tree: Option<&HashMap<String, Arc<AccountTreeNode>>>,
112    ) -> Result<(), tackler::Error> {
113        let parent = atn.parent.as_str();
114        let has_parent = other_account_tree.is_some_and(|a| a.contains_key(parent))
115            || target_account_tree.contains_key(parent);
116
117        if has_parent || atn.is_root() {
118            // this breaks recursion
119            Ok(())
120        } else {
121            let parent_atn =
122                Arc::new(AccountTreeNode::from(parent).expect("IE: synthetic parent is invalid"));
123            target_account_tree.insert(parent.to_string(), parent_atn.clone());
124
125            Self::build_account_tree(target_account_tree, parent_atn, other_account_tree)
126        }
127    }
128
129    fn from(account_names: &[String], strict_mode: bool) -> Result<AccountTrees, tackler::Error> {
130        let defined_accounts =
131            account_names
132                .iter()
133                .try_fold(
134                    HashMap::new(),
135                    |mut accs, account| match AccountTreeNode::from(account) {
136                        Ok(atn) => {
137                            accs.insert(account.into(), Arc::new(atn));
138                            Ok(accs)
139                        }
140                        Err(e) => {
141                            let msg = format!("Invalid Chart of Accounts: {e}");
142                            Err(msg)
143                        }
144                    },
145                )?;
146
147        let synthetic_parents = if strict_mode {
148            // Synthetic Account Parents are only needed in strict mode
149            let mut sap = HashMap::new();
150            for atn_entry in defined_accounts.iter() {
151                if !&defined_accounts.contains_key(atn_entry.1.parent.as_str()) {
152                    // Parent is missing -> Let's build synthetic tree
153                    let (_, atn) = atn_entry;
154                    Self::build_account_tree(&mut sap, atn.clone(), Some(&defined_accounts))?;
155                }
156            }
157            sap
158        } else {
159            HashMap::new()
160        };
161        Ok(AccountTrees {
162            defined_accounts,
163            synthetic_parents,
164        })
165    }
166}
167
168#[derive(Debug, Default)]
169pub struct Price {
170    // todo: fix visibility
171    pub price_db: PriceDb,
172    pub lookup_type: PriceLookupType,
173}
174
175#[derive(Debug)]
176pub struct Settings {
177    pub(crate) audit_mode: bool,
178    pub(crate) report: Report,
179    pub(crate) export: Export,
180    strict_mode: bool,
181    input_settings: InputSettings,
182    kernel: Kernel,
183    pub price: Price,
184    price_lookup: PriceLookup,
185    global_acc_sel: Option<AccountSelectors>,
186    accounts: AccountTrees,
187    commodities: Commodities,
188    tags: HashMap<String, Arc<Tag>>,
189}
190
191impl Default for Settings {
192    fn default() -> Self {
193        Settings {
194            strict_mode: false,
195            audit_mode: false,
196            input_settings: InputSettings::default(),
197            report: Report::default(),
198            export: Export::default(),
199            kernel: Kernel::default(),
200            price: Price::default(),
201            price_lookup: PriceLookup::default(),
202            global_acc_sel: None,
203            accounts: AccountTrees::default(),
204            commodities: Commodities::default_empty_ok(),
205            tags: HashMap::new(),
206        }
207    }
208}
209
210impl Settings {
211    pub fn default_audit() -> Self {
212        Settings {
213            audit_mode: true,
214            ..Self::default()
215        }
216    }
217}
218
219impl Settings {
220    pub fn try_from(cfg: Config, overlaps: OverlapConfig) -> Result<Settings, tackler::Error> {
221        let strict_mode = overlaps.strict.mode.unwrap_or(cfg.kernel.strict);
222        let audit_mode = overlaps.audit.mode.unwrap_or(cfg.kernel.audit.mode);
223
224        let input_settings = Self::input_settings(&cfg, &overlaps.storage)?;
225
226        let reports = match overlaps.target.reports {
227            Some(reports) => config::to_report_targets(&reports)?,
228            None => cfg.report.targets.clone(),
229        };
230        let exports = match overlaps.target.exports {
231            Some(exports) => config::to_export_targets(&exports)?,
232            None => cfg.export.targets.clone(),
233        };
234
235        let formats = match overlaps.target.formats {
236            Some(formats) => config::to_report_formats(Some(&formats))?,
237            None => cfg.report.formats.clone(),
238        };
239
240        let lookup_type = overlaps.price.lookup_type.unwrap_or(cfg.price.lookup_type);
241
242        let db_path = overlaps.price.db_path.unwrap_or(cfg.price.db_path.clone());
243
244        let account_trees = AccountTrees::from(&cfg.transaction.accounts.names, strict_mode)?;
245
246        let mut commodities = Commodities::from(&cfg)?;
247
248        let tags = cfg
249            .transaction
250            .tags
251            .names
252            .iter()
253            .fold(HashMap::new(), |mut tags, tag| {
254                let t = Tag::from(tag.to_string());
255                tags.insert(tag.into(), Arc::new(t));
256                tags
257            });
258
259        if strict_mode
260            && exports.contains(&ExportType::Equity)
261            && !account_trees
262                .defined_accounts
263                .contains_key(cfg.export.equity.equity_account.as_str())
264        {
265            let msg = "Unknown `equity.equity-account` and `strict` mode is on".to_string();
266            return Err(msg.into());
267        }
268
269        let cfg_rpt_commodity = cfg
270            .report
271            .commodity
272            .map(|c| {
273                Self::inner_get_or_create_commodity(
274                    &mut commodities,
275                    strict_mode,
276                    Some(c.name.as_str()),
277                )
278            })
279            .transpose()?;
280
281        let report_commodity = match overlaps.report.commodity {
282            Some(c) => Some(Self::inner_get_or_create_commodity(
283                &mut commodities,
284                strict_mode,
285                Some(c.as_str()),
286            )?),
287            None => cfg_rpt_commodity,
288        };
289
290        if report_commodity.is_none() && lookup_type != PriceLookupType::None {
291            let msg =
292                "Price conversion is activated, but there is no `report.commodity`".to_string();
293            return Err(msg.into());
294        }
295
296        let group_by = overlaps
297            .report
298            .group_by
299            .map(|g| GroupBy::from(g.as_str()))
300            .unwrap_or(Ok(cfg.report.balance_group.group_by))?;
301
302        let mut tmp_settings = Settings {
303            strict_mode,
304            audit_mode,
305            kernel: cfg.kernel,
306            input_settings,
307            price: Price::default(), // this is not real, see next one
308            price_lookup: PriceLookup::default(), // this is not real, see next one
309            global_acc_sel: overlaps.report.account_overlap,
310            report: Report {
311                commodity: report_commodity,
312                targets: reports,
313                formats,
314                ..cfg.report
315            },
316            export: Export {
317                targets: exports,
318                ..cfg.export
319            },
320            accounts: account_trees,
321            commodities,
322            tags,
323        };
324        tmp_settings.report.balance_group.group_by = group_by;
325
326        let given_time = overlaps.price.before_time;
327
328        fn check_given_time_usage(
329            gt: &Option<String>,
330            plt: &PriceLookupType,
331        ) -> Result<(), tackler::Error> {
332            if gt.is_some() {
333                let msg = format!(
334                    "Price \"before timestamp\" is not allowed when price lookup type is \"{}\"",
335                    plt
336                );
337                return Err(msg.into());
338            }
339            Ok(())
340        }
341        let price_lookup = match lookup_type {
342            ref plt @ PriceLookupType::LastPrice => {
343                check_given_time_usage(&given_time, plt)?;
344                PriceLookup::LastPriceDbEntry
345            }
346            ref plt @ PriceLookupType::TxnTime => {
347                check_given_time_usage(&given_time, plt)?;
348                PriceLookup::AtTheTimeOfTxn
349            }
350            ref plt @ PriceLookupType::GivenTime => match given_time {
351                Some(ts) => tmp_settings
352                    .parse_timestamp(ts.as_str())
353                    .map(PriceLookup::GivenTime)?,
354                None => {
355                    let msg = format!(
356                        "Price lookup type is \"{}\" and there is no timestamp given",
357                        plt
358                    );
359                    return Err(msg.into());
360                }
361            },
362            ref plt @ PriceLookupType::None => {
363                check_given_time_usage(&given_time, plt)?;
364                PriceLookup::None
365            }
366        };
367
368        let price = match &lookup_type {
369            PriceLookupType::None => Price::default(),
370            _ => Price {
371                // we need half-baked settings here bc commodity and timestamp lookups
372                price_db: parser::pricedb_from_file(&db_path, &mut tmp_settings)?,
373                lookup_type,
374            },
375        };
376
377        Ok(Settings {
378            price,
379            price_lookup,
380            ..tmp_settings
381        })
382    }
383}
384impl Settings {
385    pub(crate) fn get_hash(&self) -> Option<Hash> {
386        if self.audit_mode {
387            Some(self.kernel.audit.hash.clone())
388        } else {
389            None
390        }
391    }
392
393    pub(crate) fn get_txn_account(
394        &self,
395        name: &str,
396        commodity: Arc<Commodity>,
397    ) -> Result<TxnAccount, tackler::Error> {
398        let comm = self.get_commodity(commodity.name.as_str())?;
399
400        match self.accounts.defined_accounts.get(name) {
401            Some(account_tree) => Ok(TxnAccount {
402                atn: account_tree.clone(),
403                comm,
404            }),
405            None => {
406                if let Some(acc_parent) = self.accounts.synthetic_parents.get(name) {
407                    Ok(TxnAccount {
408                        atn: acc_parent.clone(),
409                        comm,
410                    })
411                } else {
412                    let msg = format!("gta: Unknown account: '{}'", name);
413                    Err(msg.into())
414                }
415            }
416        }
417    }
418
419    pub(crate) fn get_or_create_txn_account(
420        &mut self,
421        name: &str,
422        commodity: Arc<Commodity>,
423    ) -> Result<TxnAccount, tackler::Error> {
424        let comm = self.get_or_create_commodity(Some(commodity.name.as_str()))?;
425
426        let strict_mode = self.strict_mode;
427        let atn_opt = self.accounts.defined_accounts.get(name).cloned();
428
429        let atn = match atn_opt {
430            Some(account_tree) => TxnAccount {
431                atn: account_tree.clone(),
432                comm,
433            },
434            None => {
435                if self.strict_mode {
436                    let msg = format!("Unknown account: '{}'", name);
437                    return Err(msg.into());
438                } else {
439                    let atn = Arc::new(AccountTreeNode::from(name)?);
440                    self.accounts
441                        .defined_accounts
442                        .insert(name.into(), atn.clone());
443                    AccountTrees::build_account_tree(
444                        &mut self.accounts.defined_accounts,
445                        atn.clone(),
446                        None,
447                    )?;
448
449                    TxnAccount { atn, comm }
450                }
451            }
452        };
453        if !strict_mode {
454            // Not strict mode, so we build the (missing) parents
455            // directly into main Chart of Accounts
456            AccountTrees::build_account_tree(
457                &mut self.accounts.defined_accounts,
458                atn.atn.clone(),
459                None,
460            )?;
461        }
462
463        Ok(atn)
464    }
465
466    pub fn get_commodity(&self, name: &str) -> Result<Arc<Commodity>, tackler::Error> {
467        match self.commodities.names.get(name) {
468            Some(comm) => Ok(comm.clone()),
469            None => {
470                let msg = format!("Unknown commodity: '{}'", name);
471                Err(msg.into())
472            }
473        }
474    }
475    pub(crate) fn get_or_create_commodity(
476        &mut self,
477        name: Option<&str>,
478    ) -> Result<Arc<Commodity>, tackler::Error> {
479        Self::inner_get_or_create_commodity(&mut self.commodities, self.strict_mode, name)
480    }
481
482    fn inner_get_or_create_commodity(
483        commodities: &mut Commodities,
484        strict_mode: bool,
485        name: Option<&str>,
486    ) -> Result<Arc<Commodity>, tackler::Error> {
487        match name {
488            Some(n) => {
489                if n.is_empty() {
490                    if commodities.permit_empty_commodity {
491                        return match commodities.names.get(n) {
492                            Some(c) => Ok(c.clone()),
493                            None => {
494                                let comm = Arc::new(Commodity::default());
495                                commodities.names.insert(n.into(), comm.clone());
496
497                                Ok(comm.clone())
498                            }
499                        };
500                    } else {
501                        let msg =
502                            "Empty commodity and 'permit-empty-commodity' is not set".to_string();
503                        return Err(msg.into());
504                    }
505                }
506                match commodities.names.get(n) {
507                    Some(comm) => Ok(comm.clone()),
508                    None => {
509                        if strict_mode {
510                            let msg = format!("Unknown commodity: '{}'", n);
511                            Err(msg.into())
512                        } else {
513                            let comm = Arc::new(Commodity::from(n.into())?);
514                            commodities.names.insert(n.into(), comm.clone());
515                            Ok(comm)
516                        }
517                    }
518                }
519            }
520            None => {
521                let comm = Arc::new(Commodity::default());
522                Ok(comm)
523            }
524        }
525    }
526
527    pub(crate) fn get_or_create_tag(&mut self, name: &str) -> Result<Arc<Tag>, tackler::Error> {
528        if name.is_empty() {
529            let msg = "Tag name is empty string".to_string();
530            return Err(msg.into());
531        }
532        match self.tags.get(name) {
533            Some(tag) => Ok(tag.clone()),
534            None => {
535                if self.strict_mode {
536                    let msg = format!("Unknown tag: '{}'", name);
537                    Err(msg.into())
538                } else {
539                    let tag = Arc::new(Tag::from(name));
540                    self.tags.insert(name.into(), tag.clone());
541                    Ok(tag)
542                }
543            }
544        }
545    }
546
547    pub fn get_price_lookup(&self) -> PriceLookup {
548        self.price_lookup.clone()
549    }
550
551    pub fn input(&self) -> InputSettings {
552        self.input_settings.clone()
553    }
554
555    fn input_settings(
556        cfg: &Config,
557        storage_overlap: &StorageOverlap,
558    ) -> Result<InputSettings, tackler::Error> {
559        let cfg_input = &cfg.kernel.input;
560
561        // if input_overlap.git.repo => storage git
562        // if input_overlap.fs.path => storage fs
563
564        let storage_target = match storage_overlap.storage_type {
565            Some(storage) => storage,
566            None => cfg_input.storage,
567        };
568
569        let storage_type = if let Some(io) = &storage_overlap.input {
570            match io {
571                InputOverlap::File(f) => {
572                    // This is file based input => no need to configure storage
573                    return Ok(InputSettings::File(FileInput {
574                        path: f.path.clone(),
575                    }));
576                }
577                InputOverlap::Fs(fs) => {
578                    if fs.path.is_none() && storage_target != StorageType::Fs {
579                        let msg = "Conflicting input and storage system arguments. Targeting 'fs', but it's not activated by configuration or by cli options";
580                        return Err(msg.into());
581                    }
582                    StorageType::Fs
583                }
584                InputOverlap::Git(git) => {
585                    if git.repo.is_none() && storage_target != StorageType::Git {
586                        let msg = "Conflicting input and storage system arguments. Targeting 'git', but it's not activated by configuration or by cli options";
587                        return Err(msg.into());
588                    }
589                    StorageType::Git
590                }
591            }
592        } else {
593            storage_target
594        };
595
596        match storage_type {
597            StorageType::Fs => match &cfg_input.fs {
598                Some(fs_cfg) => {
599                    let i = if let Some(InputOverlap::Fs(fs_soi)) = &storage_overlap.input {
600                        // FS: Overlap + Config
601                        let path = fs_soi.path.as_ref().unwrap_or(&fs_cfg.path);
602                        let dir = fs_soi.dir.as_ref().unwrap_or(&fs_cfg.dir);
603                        let ext = fs_soi.ext.as_ref().unwrap_or(&fs_cfg.ext);
604                        FsInput {
605                            path: tackler_rs::get_abs_path(cfg.path(), path)?,
606                            dir: PathBuf::from(dir),
607                            ext: normalize_extension(ext).to_string(),
608                        }
609                    } else {
610                        // FS: No overlap, all info must come from config
611                        let ext = &fs_cfg.ext;
612                        FsInput {
613                            path: tackler_rs::get_abs_path(cfg.path(), fs_cfg.path.as_str())?,
614                            dir: PathBuf::from(&fs_cfg.dir),
615                            ext: normalize_extension(ext).to_string(),
616                        }
617                    };
618                    Ok(InputSettings::Fs(i))
619                }
620                None => {
621                    // FS: No config, all info must come from overlap
622                    if let Some(InputOverlap::Fs(fs_soi)) = &storage_overlap.input {
623                        match (&fs_soi.path, &fs_soi.dir, &fs_soi.ext) {
624                            (Some(path), Some(dir), Some(ext)) => Ok(InputSettings::Fs(FsInput {
625                                path: tackler_rs::get_abs_path(cfg.path(), path)?,
626                                dir: PathBuf::from(dir),
627                                ext: normalize_extension(ext).to_string(),
628                            })),
629                            _ => {
630                                let msg = format!(
631                                    "Not enough information to configure 'fs' storage: path = '{:?}', dir = '{:?}', ext = '{:?}'",
632                                    fs_soi.path, fs_soi.dir, fs_soi.ext
633                                );
634                                Err(msg.into())
635                            }
636                        }
637                    } else {
638                        Err("Storage type 'fs' is not configured".into())
639                    }
640                }
641            },
642            StorageType::Git => match &cfg_input.git {
643                Some(git_cfg) => {
644                    let i = if let Some(InputOverlap::Git(git_soi)) = &storage_overlap.input {
645                        // GIT: Overlap + Config
646                        let repo = git_soi.repo.as_ref().unwrap_or(&git_cfg.repo);
647                        let dir = git_soi.dir.as_ref().unwrap_or(&git_cfg.dir);
648                        let ext = git_soi.ext.as_ref().unwrap_or(&git_cfg.ext);
649                        // reference is only option via cfg
650                        let cfg_ref = GitInputSelector::Reference(git_cfg.git_ref.clone());
651                        let git_ref = git_soi.git_ref.as_ref().unwrap_or(&cfg_ref);
652
653                        GitInput {
654                            repo: tackler_rs::get_abs_path(cfg.path(), repo)?,
655                            git_ref: git_ref.clone(),
656                            dir: dir.clone(),
657                            ext: normalize_extension(ext).to_string(),
658                        }
659                    } else {
660                        // GIT: No overlap, all info must come from config
661                        let repo = git_cfg.repo.as_str();
662                        let ext = &git_cfg.ext;
663                        GitInput {
664                            repo: tackler_rs::get_abs_path(cfg.path(), repo)?,
665                            git_ref: GitInputSelector::Reference(git_cfg.git_ref.clone()),
666                            dir: git_cfg.dir.clone(),
667                            ext: normalize_extension(ext).to_string(),
668                        }
669                    };
670                    Ok(InputSettings::Git(i))
671                }
672                None => {
673                    // GIT: No config, all info must come from overlap
674                    if let Some(InputOverlap::Git(git_soi)) = &storage_overlap.input {
675                        match (&git_soi.repo, &git_soi.dir, &git_soi.ext, &git_soi.git_ref) {
676                            (Some(repo), Some(dir), Some(ext), Some(git_ref)) => {
677                                Ok(InputSettings::Git(GitInput {
678                                    repo: tackler_rs::get_abs_path(cfg.path(), repo)?,
679                                    git_ref: git_ref.clone(),
680                                    dir: dir.clone(),
681                                    ext: normalize_extension(ext).to_string(),
682                                }))
683                            }
684                            _ => {
685                                let msg = format!(
686                                    "Not enough information to configure 'git' storage: repo = '{:?}', dir = '{:?}', ext = '{:?}', ref = '{:?}'",
687                                    git_soi.repo, git_soi.dir, git_soi.ext, git_soi.git_ref
688                                );
689                                Err(msg.into())
690                            }
691                        }
692                    } else {
693                        Err("Storage type 'git' is not configured".into())
694                    }
695                }
696            },
697        }
698    }
699}
700
701impl Settings {
702    pub fn parse_timestamp(&mut self, ts: &str) -> Result<Zoned, tackler::Error> {
703        Ok(winnow::Parser::parse(
704            &mut crate::parser::parts::timestamp::parse_timestamp,
705            winnow::Stateful {
706                input: ts,
707                state: self,
708            },
709        )
710        .map_err(|e| e.to_string())?)
711    }
712
713    pub fn get_offset_datetime(
714        &self,
715        dt: jiff::civil::DateTime,
716    ) -> Result<jiff::Zoned, tackler::Error> {
717        match dt.to_zoned(self.kernel.timestamp.timezone.clone()) {
718            Ok(ts) => Ok(ts),
719            Err(err) => {
720                let msg = format!("time is invalid '{:?}'", err);
721                Err(msg.into())
722            }
723        }
724    }
725    pub fn get_offset_date(&self, date: jiff::civil::Date) -> Result<jiff::Zoned, tackler::Error> {
726        let ts = date.to_datetime(self.kernel.timestamp.default_time);
727        match ts.to_zoned(self.kernel.timestamp.timezone.clone()) {
728            Ok(ts) => Ok(ts),
729            Err(err) => {
730                let msg = format!("time is invalid '{:?}'", err);
731                Err(msg.into())
732            }
733        }
734    }
735
736    pub fn get_report_commodity(&self) -> Option<Arc<Commodity>> {
737        self.report.commodity.as_ref().map(|c| c.clone())
738    }
739
740    pub fn get_report_targets(&self) -> Vec<ReportType> {
741        self.report.targets.clone()
742    }
743
744    pub fn get_export_targets(&self) -> Vec<ExportType> {
745        self.export.targets.clone()
746    }
747
748    fn get_account_selector(&self, acc_sel: &AccountSelectors) -> AccountSelectors {
749        match &self.global_acc_sel {
750            Some(global_acc_sel) => global_acc_sel.clone(),
751            None => acc_sel.clone(),
752        }
753    }
754
755    pub fn get_balance_ras(&self) -> AccountSelectors {
756        self.get_account_selector(&self.report.balance.acc_sel)
757    }
758
759    pub fn get_balance_group_ras(&self) -> AccountSelectors {
760        self.get_account_selector(&self.report.balance_group.acc_sel)
761    }
762
763    pub fn get_register_ras(&self) -> AccountSelectors {
764        self.get_account_selector(&self.report.register.acc_sel)
765    }
766
767    pub fn get_equity_ras(&self) -> AccountSelectors {
768        self.get_account_selector(&self.export.equity.acc_sel)
769    }
770}
771
772#[cfg(test)]
773#[allow(non_snake_case)]
774mod tests {
775    use super::*;
776
777    #[test]
778    fn accounts_strict_false() {
779        let comm = Arc::new(Commodity::default());
780        let mut settings = Settings::default();
781
782        let txntn_1 = settings.get_or_create_txn_account("a:b:c", comm.clone()).unwrap(/*:test:*/);
783        assert_eq!(settings.accounts.defined_accounts.len(), 3);
784
785        assert_eq!(txntn_1.atn.depth, 3);
786        assert_eq!(txntn_1.atn.get_root(), "a");
787        assert_eq!(txntn_1.atn.parent, "a:b");
788        assert_eq!(txntn_1.atn.account, "a:b:c");
789        assert_eq!(txntn_1.atn.get_name(), "c");
790
791        let txntn_2 = settings.get_txn_account("a:b:c", comm.clone()).unwrap(/*:test:*/);
792        assert_eq!(settings.accounts.defined_accounts.len(), 3);
793
794        assert_eq!(txntn_2.atn.depth, 3);
795        assert_eq!(txntn_2.atn.get_root(), "a");
796        assert_eq!(txntn_2.atn.parent, "a:b");
797        assert_eq!(txntn_2.atn.account, "a:b:c");
798        assert_eq!(txntn_2.atn.get_name(), "c");
799
800        let txntn_3 =
801            settings.get_or_create_txn_account("a:b:b-leaf", comm.clone()).unwrap(/*:test:*/);
802        assert_eq!(settings.accounts.defined_accounts.len(), 4);
803
804        assert_eq!(txntn_3.atn.depth, 3);
805        assert_eq!(txntn_3.atn.get_root(), "a");
806        assert_eq!(txntn_3.atn.parent, "a:b");
807        assert_eq!(txntn_3.atn.account, "a:b:b-leaf");
808        assert_eq!(txntn_3.atn.get_name(), "b-leaf");
809    }
810
811    #[test]
812    fn accounts_strict_true() {
813        let comm = Arc::new(Commodity::default());
814        let mut settings = Settings::default();
815        let accounts = vec!["a:b:c".to_string()];
816
817        let acc_trees = AccountTrees::from(&accounts, true).unwrap(/*:test:*/);
818        settings.accounts = acc_trees;
819        settings.strict_mode = true;
820
821        assert_eq!(settings.accounts.defined_accounts.len(), 1);
822        assert_eq!(settings.accounts.synthetic_parents.len(), 2);
823
824        let txntn_1 = settings.get_or_create_txn_account("a:b:c", comm.clone()).unwrap(/*:test:*/);
825        assert_eq!(settings.accounts.defined_accounts.len(), 1);
826        assert_eq!(settings.accounts.synthetic_parents.len(), 2);
827
828        assert_eq!(txntn_1.atn.depth, 3);
829        assert_eq!(txntn_1.atn.get_root(), "a");
830        assert_eq!(txntn_1.atn.parent, "a:b");
831        assert_eq!(txntn_1.atn.account, "a:b:c");
832        assert_eq!(txntn_1.atn.get_name(), "c");
833
834        let txntn_2 = settings.get_txn_account("a:b:c", comm.clone()).unwrap(/*:test:*/);
835        assert_eq!(settings.accounts.defined_accounts.len(), 1);
836        assert_eq!(settings.accounts.synthetic_parents.len(), 2);
837
838        assert_eq!(txntn_2.atn.depth, 3);
839        assert_eq!(txntn_2.atn.get_root(), "a");
840        assert_eq!(txntn_2.atn.parent, "a:b");
841        assert_eq!(txntn_2.atn.account, "a:b:c");
842        assert_eq!(txntn_2.atn.get_name(), "c");
843
844        // Check that it won't create a synthetic account as real one
845        assert!(
846            settings
847                .get_or_create_txn_account("a:b", comm.clone())
848                .is_err()
849        );
850        assert_eq!(settings.accounts.defined_accounts.len(), 1);
851        assert_eq!(settings.accounts.synthetic_parents.len(), 2);
852
853        // Check synthetic account
854        let txntn_3 = settings.get_txn_account("a:b", comm.clone()).unwrap(/*:test:*/);
855        assert_eq!(settings.accounts.defined_accounts.len(), 1);
856        assert_eq!(settings.accounts.synthetic_parents.len(), 2);
857
858        assert_eq!(txntn_3.atn.depth, 2);
859        assert_eq!(txntn_3.atn.get_root(), "a");
860        assert_eq!(txntn_3.atn.parent, "a");
861        assert_eq!(txntn_3.atn.account, "a:b");
862        assert_eq!(txntn_3.atn.get_name(), "b");
863
864        // Check synthetic account
865        let txntn_4 = settings.get_txn_account("a", comm.clone()).unwrap(/*:test:*/);
866        assert_eq!(settings.accounts.defined_accounts.len(), 1);
867        assert_eq!(settings.accounts.synthetic_parents.len(), 2);
868
869        assert_eq!(txntn_4.atn.depth, 1);
870        assert_eq!(txntn_4.atn.get_root(), "a");
871        assert_eq!(txntn_4.atn.parent, "");
872        assert_eq!(txntn_4.atn.account, "a");
873        assert_eq!(txntn_4.atn.get_name(), "a");
874    }
875
876    #[test]
877    fn accounts_strict_true_child_first() {
878        let comm = Arc::new(Commodity::default());
879        let mut settings = Settings::default();
880        let accounts = vec!["a:b:c".to_string(), "a:b".to_string(), "a".to_string()];
881
882        let acc_trees = AccountTrees::from(&accounts, true).unwrap(/*:test:*/);
883        settings.accounts = acc_trees;
884        settings.strict_mode = true;
885
886        assert_eq!(settings.accounts.defined_accounts.len(), 3);
887        assert_eq!(settings.accounts.synthetic_parents.len(), 0);
888
889        let txntn_1 = settings.get_or_create_txn_account("a:b:c", comm.clone()).unwrap(/*:test:*/);
890        assert_eq!(settings.accounts.defined_accounts.len(), 3);
891        assert_eq!(settings.accounts.synthetic_parents.len(), 0);
892        assert_eq!(txntn_1.atn.account, "a:b:c");
893
894        let txntn_2 = settings.get_or_create_txn_account("a:b", comm.clone()).unwrap(/*:test:*/);
895        assert_eq!(settings.accounts.defined_accounts.len(), 3);
896        assert_eq!(settings.accounts.synthetic_parents.len(), 0);
897        assert_eq!(txntn_2.atn.account, "a:b");
898
899        let txntn_2 = settings.get_or_create_txn_account("a", comm.clone()).unwrap(/*:test:*/);
900        assert_eq!(settings.accounts.defined_accounts.len(), 3);
901        assert_eq!(settings.accounts.synthetic_parents.len(), 0);
902        assert_eq!(txntn_2.atn.account, "a");
903    }
904
905    #[test]
906    fn accounts_strict_true_gap() {
907        let comm = Arc::new(Commodity::default());
908        let mut settings = Settings::default();
909        let accounts = vec!["a:b:c:d".to_string(), "a:b".to_string(), "a".to_string()];
910
911        let acc_trees = AccountTrees::from(&accounts, true).unwrap(/*:test:*/);
912        settings.accounts = acc_trees;
913        settings.strict_mode = true;
914
915        assert_eq!(settings.accounts.defined_accounts.len(), 3);
916        assert_eq!(settings.accounts.synthetic_parents.len(), 1);
917
918        // Check that it won't create a synthetic account as real one
919        assert!(
920            settings
921                .get_or_create_txn_account("a:b:c", comm.clone())
922                .is_err()
923        );
924
925        let txntn_synth = settings.get_txn_account("a:b:c", comm.clone()).unwrap(/*:test:*/);
926        assert_eq!(settings.accounts.defined_accounts.len(), 3);
927        assert_eq!(settings.accounts.synthetic_parents.len(), 1);
928        assert_eq!(txntn_synth.atn.account, "a:b:c");
929
930        let txntn_2 = settings.get_or_create_txn_account("a:b", comm.clone()).unwrap(/*:test:*/);
931        assert_eq!(settings.accounts.defined_accounts.len(), 3);
932        assert_eq!(settings.accounts.synthetic_parents.len(), 1);
933        assert_eq!(txntn_2.atn.account, "a:b");
934
935        let txntn_2 = settings.get_or_create_txn_account("a", comm.clone()).unwrap(/*:test:*/);
936        assert_eq!(settings.accounts.defined_accounts.len(), 3);
937        assert_eq!(settings.accounts.synthetic_parents.len(), 1);
938        assert_eq!(txntn_2.atn.account, "a");
939    }
940}