1
use askama::Template;
2
use axum::{
3
    Extension, Json,
4
    extract::{Path, State},
5
    http::StatusCode,
6
    response::IntoResponse,
7
};
8
use finance::account::Account;
9
use serde::Deserialize;
10
use server::command::{CmdResult, FinanceEntity, account::GetAccount};
11
use sqlx::types::Uuid;
12
use std::sync::Arc;
13

            
14
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
15

            
16
struct ScriptView {
17
    id: Uuid,
18
    name: Option<String>,
19
}
20

            
21
#[derive(Template)]
22
#[template(path = "pages/account/edit.html")]
23
struct AccountEditPage {
24
    account_id: Uuid,
25
    account_name: String,
26
    tags: Vec<finance::tag::Tag>,
27
    scripting_enabled: bool,
28
    scripts: Vec<ScriptView>,
29
}
30

            
31
pub async fn account_edit_page(
32
    Path(id): Path<Uuid>,
33
    State(_data): State<Arc<AppState>>,
34
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
35
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
36
    let user = &jwt_auth.user;
37

            
38
    let account_result = GetAccount::new()
39
        .user_id(user.id)
40
        .account_id(id)
41
        .run()
42
        .await
43
        .map_err(|e| {
44
            let error_response = serde_json::json!({
45
                "status": "fail",
46
                "message": format!("Failed to get account: {e:?}"),
47
            });
48
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
49
        })?;
50

            
51
    let (account, name) = if let Some(CmdResult::TaggedEntities { entities, .. }) = account_result
52
        && let Some((FinanceEntity::Account(account), tags)) = entities.into_iter().next()
53
    {
54
        let name = if let Some(FinanceEntity::Tag(name_tag)) = tags.get("name") {
55
            name_tag.tag_value.clone()
56
        } else {
57
            String::new()
58
        };
59
        (account, name)
60
    } else {
61
        let error_response = serde_json::json!({
62
            "status": "fail",
63
            "message": "Account not found",
64
        });
65
        return Err((StatusCode::NOT_FOUND, Json(error_response)));
66
    };
67

            
68
    let server_user = server::user::User { id: user.id };
69
    let tags: Vec<finance::tag::Tag> = server_user
70
        .get_account_tags(&account)
71
        .await
72
        .unwrap_or_default()
73
        .into_iter()
74
        .filter(|t| t.tag_name != "name")
75
        .collect();
76

            
77
    #[cfg(feature = "scripting")]
78
    let scripts: Vec<ScriptView> = server_user
79
        .list_scripts()
80
        .await
81
        .unwrap_or_default()
82
        .into_iter()
83
        .map(|s| ScriptView {
84
            id: s.id,
85
            name: s.name,
86
        })
87
        .collect();
88

            
89
    #[cfg(not(feature = "scripting"))]
90
    let scripts: Vec<ScriptView> = Vec::new();
91

            
92
    let template = AccountEditPage {
93
        account_id: account.id,
94
        account_name: name,
95
        tags,
96
        scripting_enabled: cfg!(feature = "scripting"),
97
        scripts,
98
    };
99

            
100
    Ok(HtmlTemplate(template))
101
}
102

            
103
#[derive(Deserialize)]
104
pub struct RenameForm {
105
    account_id: Uuid,
106
    name: String,
107
}
108

            
109
#[derive(Deserialize)]
110
struct AccountTagData {
111
    name: String,
112
    value: String,
113
    description: Option<String>,
114
}
115

            
116
#[derive(Deserialize)]
117
pub struct AccountTagsForm {
118
    tags: Vec<AccountTagData>,
119
}
120

            
121
pub async fn rename_account(
122
    State(_data): State<Arc<AppState>>,
123
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
124
    Json(form): Json<RenameForm>,
125
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
126
    let user = &jwt_auth.user;
127
    let server_user = server::user::User { id: user.id };
128

            
129
    let account = Account {
130
        id: form.account_id,
131
        parent: None,
132
    };
133

            
134
    let name_tag = finance::tag::Tag {
135
        id: Uuid::new_v4(),
136
        tag_name: "name".to_string(),
137
        tag_value: form.name,
138
        description: None,
139
    };
140

            
141
    server_user
142
        .set_account_tag(&account, &name_tag)
143
        .await
144
        .map_err(|e| {
145
            let error_response = serde_json::json!({
146
                "status": "fail",
147
                "message": t!("Failed to rename account"),
148
            });
149
            log::error!("Failed to rename account: {e:?}");
150
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
151
        })?;
152

            
153
    Ok(t!("Account renamed").to_string())
154
}
155

            
156
pub async fn account_tags_submit(
157
    Path(id): Path<Uuid>,
158
    State(_data): State<Arc<AppState>>,
159
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
160
    Json(form): Json<AccountTagsForm>,
161
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
162
    let user = &jwt_auth.user;
163
    let server_user = server::user::User { id: user.id };
164

            
165
    let account = Account { id, parent: None };
166

            
167
    let existing_tags = server_user
168
        .get_account_tags(&account)
169
        .await
170
        .unwrap_or_default();
171

            
172
    for tag in &existing_tags {
173
        if tag.tag_name == "name" {
174
            continue;
175
        }
176
        let _ = server_user.detach_account_tag(id, tag.id).await;
177
        let _ = server_user.cleanup_orphan_tag(tag.id).await;
178
    }
179

            
180
    for tag_data in form.tags {
181
        if tag_data.name == "name" {
182
            continue;
183
        }
184
        server_user
185
            .create_account_tag(id, tag_data.name, tag_data.value, tag_data.description)
186
            .await
187
            .map_err(|e| {
188
                let error_response = serde_json::json!({
189
                    "status": "fail",
190
                    "message": format!("Failed to create account tag: {:?}", e),
191
                });
192
                log::error!("Failed to create account tag: {e:?}");
193
                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
194
            })?;
195
    }
196

            
197
    Ok(t!("Account tags saved").to_string())
198
}
199

            
200
#[cfg(feature = "scripting")]
201
pub async fn run_account_script(
202
    Path((account_id, script_id)): Path<(Uuid, Uuid)>,
203
    State(_data): State<Arc<AppState>>,
204
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
205
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
206
    use scripting::ScriptExecutor;
207
    use server::script::TransactionState;
208

            
209
    let user = &jwt_auth.user;
210
    let server_user = server::user::User { id: user.id };
211

            
212
    let script = server_user.get_script(script_id).await.map_err(|e| {
213
        let error_response = serde_json::json!({
214
            "status": "fail",
215
            "message": format!("Failed to get script: {e:?}"),
216
        });
217
        (StatusCode::NOT_FOUND, Json(error_response))
218
    })?;
219

            
220
    let transaction_ids = server_user
221
        .list_transaction_ids_by_account(account_id)
222
        .await
223
        .map_err(|e| {
224
            let error_response = serde_json::json!({
225
                "status": "fail",
226
                "message": format!("Failed to fetch transactions: {e:?}"),
227
            });
228
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
229
        })?;
230

            
231
    let executor = ScriptExecutor::new();
232
    let mut processed = 0u64;
233

            
234
    for tx_id in &transaction_ids {
235
        let tx_result = server::command::transaction::GetTransaction::new()
236
            .user_id(user.id)
237
            .transaction_id(*tx_id)
238
            .run()
239
            .await
240
            .map_err(|e| {
241
                let error_response = serde_json::json!({
242
                    "status": "fail",
243
                    "message": format!("Failed to get transaction {tx_id}: {e:?}"),
244
                });
245
                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
246
            })?;
247

            
248
        let (transaction, note) = if let Some(CmdResult::TaggedEntities { mut entities, .. }) =
249
            tx_result
250
            && let Some((FinanceEntity::Transaction(tx), tags)) = entities.pop()
251
        {
252
            let note = tags.get("note").and_then(|entity| {
253
                if let FinanceEntity::Tag(tag) = entity {
254
                    Some(tag.tag_value.clone())
255
                } else {
256
                    None
257
                }
258
            });
259
            (tx, note)
260
        } else {
261
            continue;
262
        };
263

            
264
        let splits_result = server::command::split::ListSplits::new()
265
            .user_id(user.id)
266
            .transaction(*tx_id)
267
            .run()
268
            .await
269
            .map_err(|e| {
270
                let error_response = serde_json::json!({
271
                    "status": "fail",
272
                    "message": format!("Failed to get splits for {tx_id}: {e:?}"),
273
                });
274
                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
275
            })?;
276

            
277
        let mut split_entities = Vec::new();
278
        if let Some(CmdResult::TaggedEntities {
279
            entities: split_data,
280
            ..
281
        }) = splits_result
282
        {
283
            for (entity, _tags) in split_data {
284
                split_entities.push(entity);
285
            }
286
        }
287

            
288
        let state = TransactionState::new(transaction)
289
            .with(split_entities)
290
            .with_note(note);
291

            
292
        let state = state
293
            .run_scripts(&executor, &[(script.id, script.bytecode.clone())])
294
            .map_err(|e| {
295
                let error_response = serde_json::json!({
296
                    "status": "fail",
297
                    "message": format!("Script execution failed on {tx_id}: {e:?}"),
298
                });
299
                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
300
            })?;
301

            
302
        for tag in &state.transaction_tags {
303
            let _ = server_user
304
                .create_transaction_tag(*tx_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
305
                .await;
306
        }
307

            
308
        for (split_id, tag) in &state.split_tags {
309
            let _ = server_user
310
                .create_split_tag(*split_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
311
                .await;
312
        }
313

            
314
        processed += 1;
315
    }
316

            
317
    Ok(format!("{}: {processed}", t!("Processed transactions")))
318
}