tackler_core/kernel/
settings.rs

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