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 sqlx::types::Uuid;
11
use std::sync::Arc;
12

            
13
use crate::{AppState, jwt_auth::JWTAuthMiddleware};
14

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

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

            
30
#[derive(Deserialize)]
31
pub struct RenameForm {
32
    account_id: Uuid,
33
    name: String,
34
}
35

            
36
#[derive(Deserialize)]
37
struct AccountTagData {
38
    name: String,
39
    value: String,
40
    description: Option<String>,
41
}
42

            
43
#[derive(Deserialize)]
44
pub struct AccountTagsForm {
45
    tags: Vec<AccountTagData>,
46
}
47

            
48
pub async fn rename_account(
49
    State(_data): State<Arc<AppState>>,
50
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
51
    Json(form): Json<RenameForm>,
52
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
53
    let user = &jwt_auth.user;
54
    let server_user = server::user::User { id: user.id };
55

            
56
    let account = Account {
57
        id: form.account_id,
58
        parent: None,
59
    };
60

            
61
    let name_tag = finance::tag::Tag {
62
        id: Uuid::new_v4(),
63
        tag_name: "name".to_string(),
64
        tag_value: form.name,
65
        description: None,
66
    };
67

            
68
    server_user
69
        .set_account_tag(&account, &name_tag)
70
        .await
71
        .map_err(|e| {
72
            let error_response = serde_json::json!({
73
                "status": "fail",
74
                "message": t!("Failed to rename account"),
75
            });
76
            log::error!("Failed to rename account: {e:?}");
77
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
78
        })?;
79

            
80
    Ok(t!("Account renamed").to_string())
81
}
82

            
83
pub async fn account_tags_submit(
84
    Path(id): Path<Uuid>,
85
    State(_data): State<Arc<AppState>>,
86
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
87
    Json(form): Json<AccountTagsForm>,
88
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
89
    let user = &jwt_auth.user;
90
    let server_user = server::user::User { id: user.id };
91

            
92
    let account = Account { id, parent: None };
93

            
94
    let existing_tags = server_user
95
        .get_account_tags(&account)
96
        .await
97
        .unwrap_or_default();
98

            
99
    for tag in &existing_tags {
100
        if tag.tag_name == "name" {
101
            continue;
102
        }
103
        let _ = server_user.detach_account_tag(id, tag.id).await;
104
        let _ = server_user.cleanup_orphan_tag(tag.id).await;
105
    }
106

            
107
    for tag_data in form.tags {
108
        if tag_data.name == "name" {
109
            continue;
110
        }
111
        server_user
112
            .create_account_tag(id, tag_data.name, tag_data.value, tag_data.description)
113
            .await
114
            .map_err(|e| {
115
                let error_response = serde_json::json!({
116
                    "status": "fail",
117
                    "message": format!("Failed to create account tag: {:?}", e),
118
                });
119
                log::error!("Failed to create account tag: {e:?}");
120
                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
121
            })?;
122
    }
123

            
124
    Ok(t!("Account tags saved").to_string())
125
}
126

            
127
pub async fn run_account_script(
128
    Path((account_id, script_id)): Path<(Uuid, Uuid)>,
129
    State(_data): State<Arc<AppState>>,
130
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
131
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
132
    use scripting::ScriptExecutor;
133

            
134
    let user = &jwt_auth.user;
135
    let server_user = server::user::User { id: user.id };
136

            
137
    let script = server_user.get_script(script_id).await.map_err(|e| {
138
        let error_response = serde_json::json!({
139
            "status": "fail",
140
            "message": format!("Failed to get script: {e:?}"),
141
        });
142
        (StatusCode::NOT_FOUND, Json(error_response))
143
    })?;
144

            
145
    let transaction_ids = server_user
146
        .list_transaction_ids_by_account(account_id)
147
        .await
148
        .map_err(|e| {
149
            let error_response = serde_json::json!({
150
                "status": "fail",
151
                "message": format!("Failed to fetch transactions: {e:?}"),
152
            });
153
            (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
154
        })?;
155

            
156
    let executor = ScriptExecutor::try_new().map_err(|e| {
157
        let error_response = serde_json::json!({
158
            "status": "fail",
159
            "message": format!("Failed to build script executor: {e:?}"),
160
        });
161
        (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
162
    })?;
163
    let mut processed = 0u64;
164

            
165
    // Per-tx I/O moved into `server::script::load_transaction_state`;
166
    // this loop is thin glue around the typestate read path + the
167
    // script-output write path. Lets CLI/TUI use the same helper
168
    // when their migration lands.
169
    for tx_id in &transaction_ids {
170
        let state = match server::script::load_transaction_state(user.id, *tx_id).await {
171
            Ok(Some(s)) => s,
172
            Ok(None) => continue,
173
            Err(e) => {
174
                let error_response = serde_json::json!({
175
                    "status": "fail",
176
                    "message": format!("Failed to load transaction {tx_id}: {e:?}"),
177
                });
178
                return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
179
            }
180
        };
181

            
182
        let report = state
183
            .run_scripts(&executor, &[(script.id, script.bytecode.clone())])
184
            .map_err(|e| {
185
                let error_response = serde_json::json!({
186
                    "status": "fail",
187
                    "message": format!("Script execution failed on {tx_id}: {e:?}"),
188
                });
189
                (StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
190
            })?;
191
        for failure in &report.failures {
192
            log::error!(
193
                "Script {sid} failed on tx {tx_id}: {code}: {message}",
194
                sid = failure.script_id,
195
                code = failure.code,
196
                message = failure.message
197
            );
198
        }
199
        let state = report.state;
200

            
201
        for tag in &state.transaction_tags {
202
            let _ = server_user
203
                .create_transaction_tag(*tx_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
204
                .await;
205
        }
206

            
207
        for (split_id, tag) in &state.split_tags {
208
            let _ = server_user
209
                .create_split_tag(*split_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
210
                .await;
211
        }
212

            
213
        processed += 1;
214
    }
215

            
216
    Ok(format!("{}: {processed}", t!("Processed transactions")))
217
}