1
//! Transaction-draft accumulator for render-only template execution.
2
//!
3
//! A template is per-user nomiscript *source* that, when rendered, calls the
4
//! draft natives (`set-draft-note`, `set-draft-date`, `draft-split`,
5
//! `draft-tag`) to describe a not-yet-persisted transaction. Those natives
6
//! mutate a [`TransactionDraft`] held in the session's Store data; the render
7
//! entry point reads it back via `store.into_data()` after the program runs.
8
//! Nothing here touches the database — the draft is pure intent the web/CLI
9
//! layer turns into a pre-filled new-transaction form.
10

            
11
/// One split line a template requested: an account + commodity (both by uuid
12
/// string, resolved by the template via `get-account`/`get-commodity`), a
13
/// rational amount, and any per-split tags the template attached via
14
/// `draft-split-tag`.
15
#[derive(Debug, Clone, PartialEq, Eq)]
16
pub struct DraftSplit {
17
    pub account_id: String,
18
    pub commodity_id: String,
19
    pub value_num: i64,
20
    pub value_denom: i64,
21
    pub tags: Vec<DraftTag>,
22
}
23

            
24
/// A `(name, value)` tag a template asked to attach to the drafted transaction.
25
#[derive(Debug, Clone, PartialEq, Eq)]
26
pub struct DraftTag {
27
    pub name: String,
28
    pub value: String,
29
}
30

            
31
/// The accumulated result of rendering a template: everything needed to
32
/// pre-fill a new-transaction form. All fields are optional/append-only; a
33
/// template sets what it knows and leaves the rest for the user.
34
#[derive(Debug, Clone, Default, PartialEq, Eq)]
35
pub struct TransactionDraft {
36
    pub note: Option<String>,
37
    pub date: Option<String>,
38
    pub splits: Vec<DraftSplit>,
39
    pub tags: Vec<DraftTag>,
40
}
41

            
42
impl TransactionDraft {
43
    #[must_use]
44
104
    pub fn new() -> Self {
45
104
        Self::default()
46
104
    }
47

            
48
18
    pub fn set_note(&mut self, note: String) {
49
18
        self.note = Some(note);
50
18
    }
51

            
52
18
    pub fn set_date(&mut self, date: String) {
53
18
        self.date = Some(date);
54
18
    }
55

            
56
    /// The number of splits drafted so far — the index the next
57
    /// [`add_split`](Self::add_split) will return.
58
    #[must_use]
59
75
    pub fn split_count(&self) -> usize {
60
75
        self.splits.len()
61
75
    }
62

            
63
    /// Appends a split and returns its index — the handle a template passes to
64
    /// [`add_split_tag`](Self::add_split_tag) to tag that specific split.
65
77
    pub fn add_split(&mut self, split: DraftSplit) -> usize {
66
77
        self.splits.push(split);
67
77
        self.splits.len() - 1
68
77
    }
69

            
70
18
    pub fn add_tag(&mut self, tag: DraftTag) {
71
18
        self.tags.push(tag);
72
18
    }
73

            
74
    /// Attaches a tag to the split at `index`. Returns `false` if the index is
75
    /// out of range (a template handed a stale/invalid handle).
76
38
    pub fn add_split_tag(&mut self, index: usize, tag: DraftTag) -> bool {
77
38
        match self.splits.get_mut(index) {
78
19
            Some(split) => {
79
19
                split.tags.push(tag);
80
19
                true
81
            }
82
19
            None => false,
83
        }
84
38
    }
85
}
86

            
87
#[cfg(test)]
88
mod tests {
89
    use super::*;
90

            
91
5
    fn split() -> DraftSplit {
92
5
        DraftSplit {
93
5
            account_id: "a".to_string(),
94
5
            commodity_id: "c".to_string(),
95
5
            value_num: 1,
96
5
            value_denom: 1,
97
5
            tags: Vec::new(),
98
5
        }
99
5
    }
100

            
101
    #[test]
102
1
    fn split_count_predicts_next_add_split_index() {
103
1
        let mut draft = TransactionDraft::new();
104
1
        assert_eq!(draft.split_count(), 0);
105
1
        assert_eq!(draft.add_split(split()), 0);
106
1
        assert_eq!(draft.split_count(), 1);
107
1
        assert_eq!(draft.add_split(split()), 1);
108
1
        assert_eq!(draft.split_count(), 2);
109
1
    }
110

            
111
    #[test]
112
1
    fn add_split_tag_targets_only_the_indexed_split() {
113
1
        let mut draft = TransactionDraft::new();
114
1
        let first = draft.add_split(split());
115
1
        let second = draft.add_split(split());
116

            
117
1
        assert!(draft.add_split_tag(
118
1
            first,
119
1
            DraftTag {
120
1
                name: "memo".to_string(),
121
1
                value: "lunch".to_string(),
122
1
            }
123
        ));
124

            
125
1
        assert_eq!(draft.splits[first].tags.len(), 1);
126
1
        assert_eq!(draft.splits[first].tags[0].name, "memo");
127
1
        assert!(draft.splits[second].tags.is_empty());
128
1
    }
129

            
130
    #[test]
131
1
    fn add_split_tag_rejects_out_of_range_handle() {
132
1
        let mut draft = TransactionDraft::new();
133
1
        draft.add_split(split());
134
1
        assert!(!draft.add_split_tag(
135
1
            7,
136
1
            DraftTag {
137
1
                name: "k".to_string(),
138
1
                value: "v".to_string(),
139
1
            }
140
1
        ));
141
1
    }
142
}