Skip to main content

web/pages/account/
edit.rs

1use askama::Template;
2use axum::{
3    Extension, Json,
4    extract::{Path, State},
5    http::StatusCode,
6    response::IntoResponse,
7};
8use finance::account::Account;
9use serde::Deserialize;
10use sqlx::types::Uuid;
11use std::sync::Arc;
12
13use crate::{AppState, jwt_auth::JWTAuthMiddleware};
14
15struct ScriptView {
16    id: Uuid,
17    name: Option<String>,
18}
19
20#[derive(Template)]
21#[template(path = "pages/account/edit.html")]
22struct 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)]
31pub struct RenameForm {
32    account_id: Uuid,
33    name: String,
34}
35
36#[derive(Deserialize)]
37struct AccountTagData {
38    name: String,
39    value: String,
40    description: Option<String>,
41}
42
43#[derive(Deserialize)]
44pub struct AccountTagsForm {
45    tags: Vec<AccountTagData>,
46}
47
48pub 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
83pub 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
127pub 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}