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, TagData, get_account_name, get_commodity_name, parse_transaction_date,
18
    process_split_data, 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
    transaction_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: Vec<finance::tag::Tag> = server_user
226
        .get_transaction_tags(tx_uuid)
227
        .await
228
        .unwrap_or_default()
229
        .into_iter()
230
        .filter(|t| t.tag_name != "note")
231
        .collect();
232

            
233
    let template = TransactionEditPage {
234
        transaction_id: transaction_id.clone(),
235
        splits: splits_display,
236
        note,
237
        date,
238
        transaction_tags,
239
    };
240

            
241
    Ok(HtmlTemplate(template))
242
}
243

            
244
#[derive(Deserialize, Debug)]
245
pub struct TransactionEditForm {
246
    transaction_id: String,
247
    splits: Vec<SplitData>,
248
    note: Option<String>,
249
    date: Option<String>,
250
    tags: Option<Vec<TagData>>,
251
}
252

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

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

            
268
    validate_splits_not_empty(&form.splits)?;
269

            
270
    let post_date = parse_transaction_date(form.date.as_deref());
271
    let post_date_utc = post_date.and_utc();
272
    let enter_date_utc = Local::now().naive_utc().and_utc();
273

            
274
    let mut split_entities = Vec::new();
275
    let mut prices = Vec::new();
276
    let mut split_tags_to_create = Vec::new();
277

            
278
    for split_data in form.splits {
279
        let processed = process_split_data(tx_uuid, split_data).await?;
280

            
281
        let from_split_id = processed.from_split.id;
282
        let to_split_id = processed.to_split.id;
283

            
284
        split_entities.push(FinanceEntity::Split(processed.from_split));
285
        split_entities.push(FinanceEntity::Split(processed.to_split));
286

            
287
        if let Some(price) = processed.price {
288
            prices.push(FinanceEntity::Price(price));
289
        }
290

            
291
        if let Some(tags) = processed.from_split_tags {
292
            for tag in tags {
293
                split_tags_to_create.push((from_split_id, tag));
294
            }
295
        }
296

            
297
        if let Some(tags) = processed.to_split_tags {
298
            for tag in tags {
299
                split_tags_to_create.push((to_split_id, tag));
300
            }
301
        }
302
    }
303

            
304
    let mut cmd = server::command::transaction::UpdateTransaction::new()
305
        .user_id(user.id)
306
        .transaction_id(tx_uuid)
307
        .splits(split_entities)
308
        .post_date(post_date_utc)
309
        .enter_date(enter_date_utc);
310

            
311
    if !prices.is_empty() {
312
        cmd = cmd.prices(prices);
313
    }
314

            
315
    if let Some(note) = form.note.as_deref() {
316
        cmd = cmd.note(note.to_string());
317
    }
318

            
319
    match cmd.run().await {
320
        Ok(result) => {
321
            let server_user = server::user::User { id: user.id };
322

            
323
            for (split_id, tag_data) in split_tags_to_create {
324
                server_user
325
                    .create_split_tag(
326
                        split_id,
327
                        tag_data.name,
328
                        tag_data.value,
329
                        tag_data.description,
330
                    )
331
                    .await
332
                    .map_err(|e| {
333
                        let error_response = serde_json::json!({
334
                            "status": "fail",
335
                            "message": format!("Failed to create split tag: {:?}", e),
336
                        });
337
                        log::error!("Failed to create split tag for split {split_id}: {e:?}");
338
                        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
339
                    })?;
340
            }
341

            
342
            // Delete existing non-note transaction tags and create new ones
343
            let existing_tags = server_user
344
                .get_transaction_tags(tx_uuid)
345
                .await
346
                .unwrap_or_default();
347
            for tag in &existing_tags {
348
                if tag.tag_name != "note" {
349
                    let _ = server_user.delete_tag(tag.id).await;
350
                }
351
            }
352

            
353
            if let Some(tags) = form.tags {
354
                for tag_data in tags {
355
                    if tag_data.name == "note" {
356
                        continue;
357
                    }
358
                    server_user
359
                        .create_transaction_tag(
360
                            tx_uuid,
361
                            tag_data.name,
362
                            tag_data.value,
363
                            tag_data.description,
364
                        )
365
                        .await
366
                        .map_err(|e| {
367
                            let error_response = serde_json::json!({
368
                                "status": "fail",
369
                                "message": format!("Failed to create transaction tag: {:?}", e),
370
                            });
371
                            log::error!("Failed to create transaction tag: {e:?}");
372
                            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
373
                        })?;
374
                }
375
            }
376

            
377
            match result {
378
                Some(CmdResult::Entity(FinanceEntity::Transaction(tx))) => {
379
                    Ok(format!("{}: {}", t!("Transaction updated with ID"), tx.id))
380
                }
381
                _ => Ok(t!("Transaction updated successfully").to_string()),
382
            }
383
        }
384
        Err(e) => {
385
            let error_response = serde_json::json!({
386
                "status": "fail",
387
                "message": format!("Failed to update transaction: {:?}", e),
388
            });
389

            
390
            log::error!("Failed to update transaction: {e:?}");
391
            Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)))
392
        }
393
    }
394
}