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 server::command::{CmdResult, FinanceEntity, account::GetAccount};
11use sqlx::types::Uuid;
12use std::sync::Arc;
13
14use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
15
16struct ScriptView {
17 id: Uuid,
18 name: Option<String>,
19}
20
21#[derive(Template)]
22#[template(path = "pages/account/edit.html")]
23struct 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
31pub 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)]
104pub struct RenameForm {
105 account_id: Uuid,
106 name: String,
107}
108
109#[derive(Deserialize)]
110struct AccountTagData {
111 name: String,
112 value: String,
113 description: Option<String>,
114}
115
116#[derive(Deserialize)]
117pub struct AccountTagsForm {
118 tags: Vec<AccountTagData>,
119}
120
121pub 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
156pub 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")]
201pub 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}