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

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

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

            
21
#[derive(Deserialize)]
22
pub struct TransactionCreateParams {
23
    from_account: Option<Uuid>,
24
}
25

            
26
#[derive(Template)]
27
#[template(path = "pages/transaction/create.html")]
28
struct TransactionCreatePage {
29
    from_account: Option<Uuid>,
30
}
31

            
32
pub async fn transaction_create_page(
33
    Query(params): Query<TransactionCreateParams>,
34
) -> impl IntoResponse {
35
    let template = TransactionCreatePage {
36
        from_account: params.from_account,
37
    };
38
    HtmlTemplate(template)
39
}
40

            
41
#[derive(Template)]
42
#[template(path = "components/transaction/create.html")]
43
struct TransactionFormTemplate {}
44

            
45
pub async fn transaction_form() -> impl IntoResponse {
46
    let template = TransactionFormTemplate {};
47
    HtmlTemplate(template)
48
}
49

            
50
#[derive(Deserialize, Debug)]
51
pub struct TransactionForm {
52
    splits: Vec<SplitData>,
53
    note: Option<String>,
54
    date: Option<String>,
55
    tags: Option<Vec<TagData>>,
56
}
57

            
58
8
pub async fn transaction_submit(
59
8
    State(_data): State<Arc<AppState>>,
60
8
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
61
8
    Json(form): Json<TransactionForm>,
62
8
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
63
8
    let user = &jwt_auth.user;
64

            
65
    // Validate splits
66
8
    validate_splits_not_empty(&form.splits)?;
67

            
68
    // Parse date
69
7
    let post_date = parse_transaction_date(form.date.as_deref());
70
7
    let post_date_utc = post_date.and_utc();
71
7
    let enter_date_utc = Local::now().naive_utc().and_utc();
72

            
73
    // Create transaction ID
74
7
    let tx_id = Uuid::new_v4();
75

            
76
    // Process splits using shared utility
77
7
    let mut split_entities = Vec::new();
78
7
    let mut prices = Vec::new();
79
7
    let mut split_tags_to_create = Vec::new();
80

            
81
7
    for split_data in form.splits {
82
7
        let processed = process_split_data(tx_id, split_data).await?;
83

            
84
3
        let from_split_id = processed.from_split.id;
85
3
        let to_split_id = processed.to_split.id;
86

            
87
3
        split_entities.push(FinanceEntity::Split(processed.from_split));
88
3
        split_entities.push(FinanceEntity::Split(processed.to_split));
89

            
90
3
        if let Some(price) = processed.price {
91
            prices.push(FinanceEntity::Price(price));
92
3
        }
93

            
94
3
        if let Some(tags) = processed.from_split_tags {
95
            for tag in tags {
96
                split_tags_to_create.push((from_split_id, tag));
97
            }
98
3
        }
99

            
100
3
        if let Some(tags) = processed.to_split_tags {
101
            for tag in tags {
102
                split_tags_to_create.push((to_split_id, tag));
103
            }
104
3
        }
105
    }
106

            
107
    // Execute command
108
3
    let mut cmd = server::command::transaction::CreateTransaction::new()
109
3
        .user_id(user.id)
110
3
        .splits(split_entities)
111
3
        .id(tx_id)
112
3
        .post_date(post_date_utc)
113
3
        .enter_date(enter_date_utc);
114

            
115
3
    if !prices.is_empty() {
116
        cmd = cmd.prices(prices);
117
3
    }
118

            
119
3
    if let Some(note) = form.note.as_deref()
120
3
        && !note.trim().is_empty()
121
3
    {
122
3
        cmd = cmd.note(note.to_string());
123
3
    }
124

            
125
3
    match cmd.run().await {
126
        Ok(result) => {
127
            let server_user = server::user::User { id: user.id };
128

            
129
            for (split_id, tag_data) in split_tags_to_create {
130
                server_user
131
                    .create_split_tag(
132
                        split_id,
133
                        tag_data.name,
134
                        tag_data.value,
135
                        tag_data.description,
136
                    )
137
                    .await
138
                    .map_err(|e| {
139
                        let error_response = serde_json::json!({
140
                            "status": "fail",
141
                            "message": format!("Failed to create split tag: {:?}", e),
142
                        });
143
                        log::error!("Failed to create split tag for split {split_id}: {e:?}");
144
                        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
145
                    })?;
146
            }
147

            
148
            if let Some(tags) = form.tags {
149
                for tag_data in tags {
150
                    server_user
151
                        .create_transaction_tag(
152
                            tx_id,
153
                            tag_data.name,
154
                            tag_data.value,
155
                            tag_data.description,
156
                        )
157
                        .await
158
                        .map_err(|e| {
159
                            let error_response = serde_json::json!({
160
                                "status": "fail",
161
                                "message": format!("Failed to create transaction tag: {:?}", e),
162
                            });
163
                            log::error!("Failed to create transaction tag: {e:?}");
164
                            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
165
                        })?;
166
                }
167
            }
168

            
169
            match result {
170
                Some(CmdResult::Entity(FinanceEntity::Transaction(tx))) => Ok(format!(
171
                    "{}: {}",
172
                    t!("New transaction created with ID"),
173
                    tx.id
174
                )),
175
                _ => Ok(t!("New transaction created successfully").to_string()),
176
            }
177
        }
178
3
        Err(e) => {
179
3
            let error_response = serde_json::json!({
180
3
                "status": "fail",
181
3
                "message": format!("Failed to create transaction: {:?}", e),
182
            });
183

            
184
3
            log::error!("Failed to create transaction: {e:?}");
185
3
            Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)))
186
        }
187
    }
188
8
}