1
//web/src/pages/transaction/edit/submit.rs
2

            
3
use askama::Template;
4
use axum::{
5
    Extension, Json,
6
    extract::{Path, State},
7
    http::StatusCode,
8
    response::IntoResponse,
9
};
10
use chrono::Local;
11
use serde::Deserialize;
12
use server::command::{CmdResult, FinanceEntity, transaction::GetTransaction};
13
use sqlx::types::Uuid;
14
use std::sync::Arc;
15

            
16
use crate::pages::transaction::util::{
17
    SplitData, get_account_name, get_commodity_name, parse_transaction_date, process_split_data,
18
    validate_splits_not_empty,
19
};
20
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
21

            
22
#[derive(Template)]
23
#[template(path = "pages/transaction/edit.html")]
24
struct TransactionEditPage {
25
    transaction_id: String,
26
    splits: Vec<SplitDisplayData>,
27
    note: String,
28
    date: String,
29
    tags: Vec<finance::tag::Tag>,
30
}
31

            
32
#[derive(Debug)]
33
struct SplitDisplayData {
34
    id: String,
35
    amount: String,
36
    amount_converted: String,
37
    from_account: String,
38
    from_account_name: String,
39
    to_account: String,
40
    to_account_name: String,
41
    from_commodity: String,
42
    from_commodity_name: String,
43
    to_commodity: String,
44
    to_commodity_name: String,
45
    tags: Vec<finance::tag::Tag>,
46
}
47

            
48
pub async fn transaction_edit_page(
49
    Path(transaction_id): Path<String>,
50
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
51
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
52
    let user = &jwt_auth.user;
53

            
54
    let tx_uuid = Uuid::parse_str(&transaction_id).map_err(|_| {
55
        let error_response = serde_json::json!({
56
            "status": "fail",
57
            "message": "Invalid transaction ID format",
58
        });
59
        (StatusCode::BAD_REQUEST, Json(error_response))
60
    })?;
61

            
62
    let transaction_result = GetTransaction::new()
63
        .user_id(user.id)
64
        .transaction_id(tx_uuid)
65
        .run()
66
        .await
67
        .map_err(|e| {
68
            let error_response = serde_json::json!({
69
                "status": "fail",
70
                "message": format!("Failed to get transaction: {:?}", e),
71
            });
72
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
73
        })?;
74

            
75
    let (transaction, tags) =
76
        if let Some(CmdResult::TaggedEntities { mut entities, .. }) = transaction_result {
77
            if let Some((FinanceEntity::Transaction(tx), tags)) = entities.pop() {
78
                (tx, tags)
79
            } else {
80
                let error_response = serde_json::json!({
81
                    "status": "fail",
82
                    "message": "Transaction not found",
83
                });
84
                return Err((StatusCode::NOT_FOUND, Json(error_response)));
85
            }
86
        } else {
87
            let error_response = serde_json::json!({
88
                "status": "fail",
89
                "message": "Transaction not found",
90
            });
91
            return Err((StatusCode::NOT_FOUND, Json(error_response)));
92
        };
93

            
94
    let splits_result = server::command::split::ListSplits::new()
95
        .user_id(user.id)
96
        .transaction(tx_uuid)
97
        .run()
98
        .await
99
        .map_err(|e| {
100
            let error_response = serde_json::json!({
101
                "status": "fail",
102
                "message": format!("Failed to get splits: {:?}", e),
103
            });
104
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
105
        })?;
106

            
107
    let mut splits_display = Vec::new();
108

            
109
    if let Some(CmdResult::TaggedEntities {
110
        entities: split_entities,
111
        ..
112
    }) = splits_result
113
    {
114
        let mut logical_splits = Vec::new();
115
        let mut processed_splits = std::collections::HashSet::new();
116

            
117
        for (entity, tags) in &split_entities {
118
            if let FinanceEntity::Split(split) = entity {
119
                if processed_splits.contains(&split.id) {
120
                    continue;
121
                }
122

            
123
                let mut pair_split = None;
124
                let mut pair_tags = None;
125

            
126
                for (other_entity, other_tags) in &split_entities {
127
                    if let FinanceEntity::Split(other_split) = other_entity
128
                        && other_split.id != split.id
129
                        && !processed_splits.contains(&other_split.id)
130
                        && (split.value_num > 0) != (other_split.value_num > 0)
131
                    {
132
                        pair_split = Some(other_split);
133
                        pair_tags = Some(other_tags);
134
                        break;
135
                    }
136
                }
137

            
138
                if let Some(pair) = pair_split {
139
                    processed_splits.insert(split.id);
140
                    processed_splits.insert(pair.id);
141

            
142
                    let (from_split, to_split, from_tags) = if split.value_num < 0 {
143
                        (split, pair, tags)
144
                    } else {
145
                        (pair, split, pair_tags.unwrap_or(tags))
146
                    };
147

            
148
                    let split_tags: Vec<finance::tag::Tag> = from_tags
149
                        .iter()
150
                        .filter_map(|(_, entity)| {
151
                            if let FinanceEntity::Tag(tag) = entity {
152
                                Some(finance::tag::Tag {
153
                                    id: tag.id,
154
                                    tag_name: tag.tag_name.clone(),
155
                                    tag_value: tag.tag_value.clone(),
156
                                    description: tag.description.clone(),
157
                                })
158
                            } else {
159
                                None
160
                            }
161
                        })
162
                        .collect();
163

            
164
                    logical_splits.push((from_split, to_split, split_tags));
165
                }
166
            }
167
        }
168

            
169
        for (from_split, to_split, split_tags) in logical_splits {
170
            let from_account_name = get_account_name(user.id, from_split.account_id)
171
                .await
172
                .unwrap_or("Unknown Account".to_string());
173
            let to_account_name = get_account_name(user.id, to_split.account_id)
174
                .await
175
                .unwrap_or("Unknown Account".to_string());
176
            let from_commodity_name = get_commodity_name(user.id, from_split.commodity_id)
177
                .await
178
                .unwrap_or("Unknown Currency".to_string());
179
            let to_commodity_name = get_commodity_name(user.id, to_split.commodity_id)
180
                .await
181
                .unwrap_or("Unknown Currency".to_string());
182

            
183
            let amount = (-from_split.value_num as f64) / (from_split.value_denom as f64);
184
            let amount_converted = if from_split.commodity_id == to_split.commodity_id {
185
                0.0
186
            } else {
187
                (to_split.value_num as f64) / (to_split.value_denom as f64)
188
            };
189

            
190
            splits_display.push(SplitDisplayData {
191
                id: from_split.id.to_string(),
192
                amount: format!("{amount:.2}"),
193
                amount_converted: if amount_converted > 0.0 {
194
                    format!("{amount_converted:.2}")
195
                } else {
196
                    String::new()
197
                },
198
                from_account: from_split.account_id.to_string(),
199
                from_account_name,
200
                to_account: to_split.account_id.to_string(),
201
                to_account_name,
202
                from_commodity: from_split.commodity_id.to_string(),
203
                from_commodity_name,
204
                to_commodity: to_split.commodity_id.to_string(),
205
                to_commodity_name,
206
                tags: split_tags,
207
            });
208
        }
209
    }
210

            
211
    let note = tags
212
        .get("note")
213
        .and_then(|entity| {
214
            if let FinanceEntity::Tag(tag) = entity {
215
                Some(tag.tag_value.clone())
216
            } else {
217
                None
218
            }
219
        })
220
        .unwrap_or_default();
221

            
222
    let date = transaction.post_date.format("%Y-%m-%dT%H:%M").to_string();
223

            
224
    let server_user = server::user::User { id: user.id };
225
    let transaction_tags = server_user
226
        .get_transaction_tags(tx_uuid)
227
        .await
228
        .unwrap_or_default();
229

            
230
    let template = TransactionEditPage {
231
        transaction_id: transaction_id.clone(),
232
        splits: splits_display,
233
        note,
234
        date,
235
        tags: transaction_tags,
236
    };
237

            
238
    Ok(HtmlTemplate(template))
239
}
240

            
241
#[derive(Deserialize, Debug)]
242
pub struct TransactionEditForm {
243
    transaction_id: String,
244
    splits: Vec<SplitData>,
245
    note: Option<String>,
246
    date: Option<String>,
247
}
248

            
249
pub async fn transaction_edit_submit(
250
    State(_data): State<Arc<AppState>>,
251
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
252
    Json(form): Json<TransactionEditForm>,
253
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
254
    let user = &jwt_auth.user;
255

            
256
    let tx_uuid = Uuid::parse_str(&form.transaction_id).map_err(|_| {
257
        let error_response = serde_json::json!({
258
            "status": "fail",
259
            "message": "Invalid transaction ID format",
260
        });
261
        (StatusCode::BAD_REQUEST, Json(error_response))
262
    })?;
263

            
264
    validate_splits_not_empty(&form.splits)?;
265

            
266
    let post_date = parse_transaction_date(form.date.as_deref());
267
    let post_date_utc = post_date.and_utc();
268
    let enter_date_utc = Local::now().naive_utc().and_utc();
269

            
270
    let mut split_entities = Vec::new();
271
    let mut prices = Vec::new();
272
    let mut split_tags_to_create = Vec::new();
273

            
274
    for split_data in form.splits {
275
        let processed = process_split_data(user.id, tx_uuid, split_data).await?;
276

            
277
        let from_split_id = processed.from_split.id;
278
        let to_split_id = processed.to_split.id;
279

            
280
        split_entities.push(FinanceEntity::Split(processed.from_split));
281
        split_entities.push(FinanceEntity::Split(processed.to_split));
282

            
283
        if let Some(price) = processed.price {
284
            prices.push(FinanceEntity::Price(price));
285
        }
286

            
287
        if let Some(tags) = processed.from_split_tags {
288
            for tag in tags {
289
                split_tags_to_create.push((from_split_id, tag));
290
            }
291
        }
292

            
293
        if let Some(tags) = processed.to_split_tags {
294
            for tag in tags {
295
                split_tags_to_create.push((to_split_id, tag));
296
            }
297
        }
298
    }
299

            
300
    let mut cmd = server::command::transaction::UpdateTransaction::new()
301
        .user_id(user.id)
302
        .transaction_id(tx_uuid)
303
        .splits(split_entities)
304
        .post_date(post_date_utc)
305
        .enter_date(enter_date_utc);
306

            
307
    if !prices.is_empty() {
308
        cmd = cmd.prices(prices);
309
    }
310

            
311
    if let Some(note) = form.note.as_deref() {
312
        cmd = cmd.note(note.to_string());
313
    }
314

            
315
    match cmd.run().await {
316
        Ok(result) => {
317
            // Create split tags after transaction is successfully updated
318
            let server_user = server::user::User { id: user.id };
319
            for (split_id, tag_data) in split_tags_to_create {
320
                server_user
321
                    .create_split_tag(
322
                        split_id,
323
                        tag_data.name,
324
                        tag_data.value,
325
                        tag_data.description,
326
                    )
327
                    .await
328
                    .map_err(|e| {
329
                        let error_response = serde_json::json!({
330
                            "status": "fail",
331
                            "message": format!("Failed to create split tag: {:?}", e),
332
                        });
333
                        log::error!("Failed to create split tag for split {split_id}: {e:?}");
334
                        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
335
                    })?;
336
            }
337

            
338
            match result {
339
                Some(CmdResult::Entity(FinanceEntity::Transaction(tx))) => {
340
                    Ok(format!("{}: {}", t!("Transaction updated with ID"), tx.id))
341
                }
342
                _ => Ok(t!("Transaction updated successfully").to_string()),
343
            }
344
        }
345
        Err(e) => {
346
            let error_response = serde_json::json!({
347
                "status": "fail",
348
                "message": format!("Failed to update transaction: {:?}", e),
349
            });
350

            
351
            log::error!("Failed to update transaction: {e:?}");
352
            Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)))
353
        }
354
    }
355
}