1
use finance::{split::Split, tag::Tag};
2
use sqlx::types::Uuid;
3
use std::{collections::HashMap, fmt::Debug};
4
use supp_macro::command;
5

            
6
use super::{CmdError, CmdResult};
7
use crate::{command::FinanceEntity, config::ConfigError, user::User};
8

            
9
command! {
10
    ListSplits {
11
        #[required]
12
        user_id: Uuid,
13
        #[optional]
14
        account: Uuid,
15
        #[optional]
16
        transaction: Uuid,
17
    } => {
18
        let user = User { id: user_id };
19
        let mut conn = user.get_connection().await.map_err(|err| {
20
            log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
21
            ConfigError::DB
22
        })?;
23

            
24
        // Ensure at least one filter is provided
25
        if account.is_none() && transaction.is_none() {
26
            return Err(CmdError::Args(
27
                "At least one filter (account or transaction) is required".to_string(),
28
            ));
29
        }
30

            
31
        // Execute the appropriate query based on provided filters
32
        let splits = match (account, transaction) {
33
            (Some(account_id), Some(tx_id)) => {
34
                // Filter by both account and transaction
35
                sqlx::query_file_as!(
36
                    Split,
37
                    "sql/select/splits/by_account_and_transaction.sql",
38
                    account_id,
39
                    tx_id
40
                )
41
                .fetch_all(&mut *conn)
42
                .await?
43
            }
44
            (Some(account_id), None) => {
45
                // Filter by account only
46
                sqlx::query_file_as!(
47
                    Split,
48
                    "sql/select/splits/by_account.sql",
49
                    account_id
50
                )
51
                .fetch_all(&mut *conn)
52
                .await?
53
            }
54
            (None, Some(tx_id)) => {
55
                // Filter by transaction only
56
                sqlx::query_file_as!(
57
                    Split,
58
                    "sql/select/splits/by_transaction.sql",
59
                    tx_id
60
                )
61
                .fetch_all(&mut *conn)
62
                .await?
63
            }
64
            _ => unreachable!(), // We already checked this case above
65
        };
66

            
67
        // Convert Split objects to tagged entities
68
        let mut split_entities = Vec::new();
69
        for split in splits {
70
            // Get tags for this split
71
            let tags: HashMap<String, FinanceEntity> = sqlx::query_file!(
72
                "sql/select/tags/by_split.sql",
73
                split.id
74
            )
75
            .fetch_all(&mut *conn)
76
            .await?
77
            .into_iter()
78
            .map(|row| {
79
                (
80
                    row.tag_name.clone(),
81
                    FinanceEntity::Tag(Tag {
82
                        id: row.id,
83
                        tag_name: row.tag_name,
84
                        tag_value: row.tag_value,
85
                        description: row.description,
86
                    }),
87
                )
88
            })
89
            .collect();
90

            
91
            split_entities.push((FinanceEntity::Split(split), tags));
92
        }
93

            
94
        Ok(Some(CmdResult::TaggedEntities(split_entities)))
95
    }
96
24
}
97

            
98
#[cfg(test)]
99
mod command_tests {
100
    use super::*;
101
    use crate::{
102
        command::{
103
            account::CreateAccount, commodity::CreateCommodity, transaction::CreateTransaction,
104
        },
105
        db::DB_POOL,
106
    };
107
    use sqlx::{PgPool, types::chrono::Utc};
108
    use supp_macro::local_db_sqlx_test;
109
    use tokio::sync::OnceCell;
110

            
111
    /// Context for keeping environment intact
112
    static CONTEXT: OnceCell<()> = OnceCell::const_new();
113
    static USER: OnceCell<User> = OnceCell::const_new();
114

            
115
3
    async fn setup() {
116
2
        CONTEXT
117
2
            .get_or_init(|| async {
118
                #[cfg(feature = "testlog")]
119
2
                let _ = env_logger::builder()
120
2
                    .is_test(true)
121
2
                    .filter_level(log::LevelFilter::Trace)
122
2
                    .try_init();
123
4
            })
124
2
            .await;
125
4
        USER.get_or_init(|| async { User { id: Uuid::new_v4() } })
126
2
            .await;
127
2
    }
128

            
129
    #[local_db_sqlx_test]
130
    async fn test_list_splits(pool: PgPool) -> anyhow::Result<()> {
131
        let user = USER.get().unwrap();
132
        user.commit()
133
            .await
134
            .expect("Failed to commit user to database");
135

            
136
        // First create a commodity
137
        let commodity_result = CreateCommodity::new()
138
            .fraction(1.into())
139
            .symbol("TST".to_string())
140
            .name("Test Commodity".to_string())
141
            .user_id(user.id)
142
            .run()
143
            .await?;
144

            
145
        // Get the commodity ID
146
        let commodity_id = if let Some(CmdResult::String(id)) = commodity_result {
147
            uuid::Uuid::parse_str(&id)?
148
        } else {
149
            panic!("Expected commodity ID string result");
150
        };
151

            
152
        // Create two accounts
153
        let account1 = if let Some(CmdResult::Entity(FinanceEntity::Account(account))) =
154
            CreateAccount::new()
155
                .name("Account 1".to_string())
156
                .user_id(user.id)
157
                .run()
158
                .await?
159
        {
160
            account
161
        } else {
162
            panic!("Expected account entity result");
163
        };
164

            
165
        let account2 = if let Some(CmdResult::Entity(FinanceEntity::Account(account))) =
166
            CreateAccount::new()
167
                .name("Account 2".to_string())
168
                .user_id(user.id)
169
                .run()
170
                .await?
171
        {
172
            account
173
        } else {
174
            panic!("Expected account entity result");
175
        };
176

            
177
        // Create a transaction between the accounts
178
        let tx_id = Uuid::new_v4();
179
        let now = Utc::now();
180

            
181
        let split1_id = Uuid::new_v4();
182
        let split1 = Split {
183
            id: split1_id,
184
            tx_id,
185
            account_id: account1.id,
186
            commodity_id,
187
            value_num: -100,
188
            value_denom: 1,
189
            reconcile_state: None,
190
            reconcile_date: None,
191
            lot_id: None,
192
        };
193

            
194
        let split2_id = Uuid::new_v4();
195
        let split2 = Split {
196
            id: split2_id,
197
            tx_id,
198
            account_id: account2.id,
199
            commodity_id,
200
            value_num: 100,
201
            value_denom: 1,
202
            reconcile_state: None,
203
            reconcile_date: None,
204
            lot_id: None,
205
        };
206

            
207
        let splits = vec![FinanceEntity::Split(split1), FinanceEntity::Split(split2)];
208
        CreateTransaction::new()
209
            .user_id(user.id)
210
            .splits(splits)
211
            .id(tx_id)
212
            .post_date(now)
213
            .enter_date(now)
214
            .note("Test transaction".to_string())
215
            .run()
216
            .await?;
217

            
218
        // List splits for account1
219
        if let Some(CmdResult::TaggedEntities(entities)) = ListSplits::new()
220
            .user_id(user.id)
221
            .account(account1.id)
222
            .run()
223
            .await?
224
        {
225
            assert_eq!(entities.len(), 1, "Expected one split for account1");
226

            
227
            let (entity, _tags) = &entities[0];
228
            if let FinanceEntity::Split(split) = entity {
229
                assert_eq!(split.id, split1_id);
230
                assert_eq!(split.account_id, account1.id);
231
                assert_eq!(split.value_num, -100);
232
                assert_eq!(split.value_denom, 1);
233
            } else {
234
                panic!("Expected Split entity");
235
            }
236
        } else {
237
            panic!("Expected TaggedEntities result");
238
        }
239

            
240
        // List splits for account2
241
        if let Some(CmdResult::TaggedEntities(entities)) = ListSplits::new()
242
            .user_id(user.id)
243
            .account(account2.id)
244
            .run()
245
            .await?
246
        {
247
            assert_eq!(entities.len(), 1, "Expected one split for account2");
248

            
249
            let (entity, _tags) = &entities[0];
250
            if let FinanceEntity::Split(split) = entity {
251
                assert_eq!(split.id, split2_id);
252
                assert_eq!(split.account_id, account2.id);
253
                assert_eq!(split.value_num, 100);
254
                assert_eq!(split.value_denom, 1);
255
            } else {
256
                panic!("Expected Split entity");
257
            }
258
        } else {
259
            panic!("Expected TaggedEntities result");
260
        }
261

            
262
        // List splits for non-existent account
263
        if let Some(CmdResult::TaggedEntities(entities)) = ListSplits::new()
264
            .user_id(user.id)
265
            .account(Uuid::new_v4())
266
            .run()
267
            .await?
268
        {
269
            assert_eq!(
270
                entities.len(),
271
                0,
272
                "Expected no splits for non-existent account"
273
            );
274
        } else {
275
            panic!("Expected TaggedEntities result");
276
        }
277
    }
278
}