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 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}