1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/*!
 * Transaction module
 * 
 * Transaction, or Xact abbreviated, is the main element of the Journal.
 * It contains contains Postings.
 */

use chrono::NaiveDate;

use crate::{
    balance::Balance,
    journal::{Journal, PostIndex, XactIndex},
    parser,
    post::Post,
};

pub struct Xact {
    pub date: Option<NaiveDate>,
    pub aux_date: Option<NaiveDate>,
    pub payee: String,
    pub posts: Vec<PostIndex>,
    pub note: Option<String>,
    // pub balance: Amount,
}

impl Xact {
    pub fn new(date: Option<NaiveDate>, payee: &str, note: Option<String>) -> Self {
        // code: Option<String>

        Self {
            payee: payee.to_owned(),
            note,
            posts: vec![],
            date,
            aux_date: None,
            // balance: Amount::null(),
        }
    }

    /// Creates a new Transaction from the scanned tokens.
    pub fn create(date: &str, aux_date: &str, payee: &str, note: &str) -> Self {
        let _date = if date.is_empty() {
            None
        } else {
            Some(parser::parse_date(date))
        };

        let _aux_date = if aux_date.is_empty() {
            None
        } else {
            Some(parser::parse_date(aux_date))
        };

        let _payee = if payee.is_empty() {
            "Unknown Payee".to_string()
        } else {
            payee.to_string()
        };

        let _note = if note.is_empty() {
            None
        } else {
            Some(note.to_string())
        };

        Self {
            date: _date,
            payee: _payee,
            posts: vec![],
            note: _note,
            aux_date: _aux_date,
        }
    }
}

/// Finalize transaction.
/// Adds the Xact and the Posts to the Journal.
///
/// `bool xact_base_t::finalize()`
///
pub fn finalize(xact_index: XactIndex, journal: &mut Journal) {
    // let mut balance: Option<Amount> = None;
    let mut balance = Balance::new();
    // The pointer to the post that has no amount.
    let mut null_post: Option<PostIndex> = None;
    let xact = journal.xacts.get(xact_index).expect("xact");

    // Balance
    for post_index in &xact.posts {
        // must balance?

        let post = journal.posts.get(*post_index).expect("post");

        // amount = post.cost ? post.amount
        // for now, just use the amount
        if post.amount.is_some() {
            // Add to balance.
            let Some(amt) = &post.amount
                else {panic!("should not happen")};

            balance.add(amt);
        } else if null_post.is_some() {
            todo!()
        } else {
            null_post = Some(*post_index);
        }
    }

    // If there is only one post, balance against the default account if one has
    // been set.
    if xact.posts.len() == 1 {
        todo!("handle")
    }

    if null_post.is_none() && balance.amounts.len() == 2 {
        // When an xact involves two different commodities (regardless of how
        // many posts there are) determine the conversion ratio by dividing the
        // total value of one commodity by the total value of the other.  This
        // establishes the per-unit cost for this post for both commodities.

        let mut top_post: Option<&Post> = None;
        for i in &xact.posts {
            let post = journal.get_post(*i);
            if post.amount.is_some() && top_post.is_none() {
                top_post = Some(post);
            }
        }

        // if !saw_cost && top_post
        if top_post.is_some() {
            // log::debug("there were no costs, and a valid top_post")

            let mut x = balance.amounts.iter().nth(0).unwrap();
            let mut y = balance.amounts.iter().nth(1).unwrap();

            // if x && y
            if !x.is_zero() && !y.is_zero() {
                if x.commodity_index != top_post.unwrap().amount.unwrap().commodity_index {
                    (x, y) = (y, x);
                }

                let comm = x.commodity_index;
                let per_unit_cost = (*y / *x).abs();

                for i in &xact.posts {
                    let post = journal.posts.get_mut(*i).unwrap();
                    let amt = post.amount.unwrap();

                    if amt.commodity_index == comm {
                        balance -= amt;
                        post.cost = Some(per_unit_cost * amt);
                        balance += post.cost.unwrap();
                    }
                }
            }
        }
    }

    // if (has_date())
    {
        for post_index in &xact.posts {
            let p = journal.posts.get_mut(*post_index).unwrap();
            if p.cost.is_none() {
                continue;
            }

            let Some(amt) = &p.amount else {panic!("No amount found on the posting")};
            let Some(cost) = &p.cost else {panic!("No cost found on the posting")};
            if amt.commodity_index == cost.commodity_index {
                panic!("A posting's cost must be of a different commodity than its amount");
            }

            {
                // Cost breakdown
                // TODO: virtual cost does not create a price

                // let today = NaiveDateTime::new(Local::now().date_naive(), NaiveTime::MIN);
                let moment = xact.date.unwrap().and_hms_opt(0, 0, 0).unwrap();
                let breakdown = journal
                    .commodity_pool
                    .exchange(amt, cost, false, true, moment);
                // add price
                if amt.commodity_index != cost.commodity_index {
                    journal
                        .commodity_pool
                        .add_price(amt.commodity_index.unwrap(), moment, *cost);
                }

                p.amount = Some(breakdown.amount);
            }
        }
    }

    // Handle null-amount post.
    if null_post.is_some() {
        // If one post has no value at all, its value will become the inverse of
        // the rest.  If multiple commodities are involved, multiple posts are
        // generated to balance them all.

        log::debug!("There was a null posting");

        let Some(null_post_index) = null_post
            else {panic!("should not happen")};
        let Some(post) = journal.posts.get_mut(null_post_index)
            else {panic!("should not happen")};

        // use inverse amount
        let amt = if balance.amounts.len() == 1 {
            // only one commodity
            let amt_bal = balance.amounts.iter().nth(0).unwrap();

            amt_bal.inverse()
        } else {
            // TODO: handle option when there are multiple currencies and only one blank posting.

            todo!("check this option")
        };

        post.amount = Some(amt);
        null_post = None;
    }

    // TODO: Process Commodities?
    // TODO: Process Account records from Posts.
}