Lines
0 %
Functions
Branches
100 %
use askama::Template;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use finance::account::Account;
use serde::Deserialize;
use sqlx::types::Uuid;
use std::sync::Arc;
use crate::{AppState, jwt_auth::JWTAuthMiddleware};
struct ScriptView {
id: Uuid,
name: Option<String>,
}
#[derive(Template)]
#[template(path = "pages/account/edit.html")]
struct AccountEditPage {
account_id: Uuid,
account_name: String,
tags: Vec<finance::tag::Tag>,
scripting_enabled: bool,
scripts: Vec<ScriptView>,
#[derive(Deserialize)]
pub struct RenameForm {
name: String,
struct AccountTagData {
value: String,
description: Option<String>,
pub struct AccountTagsForm {
tags: Vec<AccountTagData>,
pub async fn rename_account(
State(_data): State<Arc<AppState>>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
Json(form): Json<RenameForm>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user = &jwt_auth.user;
let server_user = server::user::User { id: user.id };
let account = Account {
id: form.account_id,
parent: None,
let name_tag = finance::tag::Tag {
id: Uuid::new_v4(),
tag_name: "name".to_string(),
tag_value: form.name,
description: None,
server_user
.set_account_tag(&account, &name_tag)
.await
.map_err(|e| {
let error_response = serde_json::json!({
"status": "fail",
"message": t!("Failed to rename account"),
});
log::error!("Failed to rename account: {e:?}");
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
})?;
Ok(t!("Account renamed").to_string())
pub async fn account_tags_submit(
Path(id): Path<Uuid>,
Json(form): Json<AccountTagsForm>,
let account = Account { id, parent: None };
let existing_tags = server_user
.get_account_tags(&account)
.unwrap_or_default();
for tag in &existing_tags {
if tag.tag_name == "name" {
continue;
let _ = server_user.detach_account_tag(id, tag.id).await;
let _ = server_user.cleanup_orphan_tag(tag.id).await;
for tag_data in form.tags {
if tag_data.name == "name" {
.create_account_tag(id, tag_data.name, tag_data.value, tag_data.description)
"message": format!("Failed to create account tag: {:?}", e),
log::error!("Failed to create account tag: {e:?}");
Ok(t!("Account tags saved").to_string())
pub async fn run_account_script(
Path((account_id, script_id)): Path<(Uuid, Uuid)>,
use scripting::ScriptExecutor;
let script = server_user.get_script(script_id).await.map_err(|e| {
"message": format!("Failed to get script: {e:?}"),
(StatusCode::NOT_FOUND, Json(error_response))
let transaction_ids = server_user
.list_transaction_ids_by_account(account_id)
"message": format!("Failed to fetch transactions: {e:?}"),
let executor = ScriptExecutor::try_new().map_err(|e| {
"message": format!("Failed to build script executor: {e:?}"),
let mut processed = 0u64;
// Per-tx I/O moved into `server::script::load_transaction_state`;
// this loop is thin glue around the typestate read path + the
// script-output write path. Lets CLI/TUI use the same helper
// when their migration lands.
for tx_id in &transaction_ids {
let state = match server::script::load_transaction_state(user.id, *tx_id).await {
Ok(Some(s)) => s,
Ok(None) => continue,
Err(e) => {
"message": format!("Failed to load transaction {tx_id}: {e:?}"),
return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
let report = state
.run_scripts(&executor, &[(script.id, script.bytecode.clone())])
"message": format!("Script execution failed on {tx_id}: {e:?}"),
for failure in &report.failures {
log::error!(
"Script {sid} failed on tx {tx_id}: {code}: {message}",
sid = failure.script_id,
code = failure.code,
message = failure.message
);
let state = report.state;
for tag in &state.transaction_tags {
let _ = server_user
.create_transaction_tag(*tx_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
.await;
for (split_id, tag) in &state.split_tags {
.create_split_tag(*split_id, tag.tag_name.clone(), tag.tag_value.clone(), None)
processed += 1;
Ok(format!("{}: {processed}", t!("Processed transactions")))