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 server::command::{CmdResult, FinanceEntity, account::GetAccount};
use sqlx::types::Uuid;
use std::sync::Arc;
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
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>,
pub async fn account_edit_page(
Path(id): Path<Uuid>,
State(_data): State<Arc<AppState>>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user = &jwt_auth.user;
let account_result = GetAccount::new()
.user_id(user.id)
.account_id(id)
.run()
.await
.map_err(|e| {
let error_response = serde_json::json!({
"status": "fail",
"message": format!("Failed to get account: {e:?}"),
});
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
})?;
let (account, name) = if let Some(CmdResult::TaggedEntities { entities, .. }) = account_result
&& let Some((FinanceEntity::Account(account), tags)) = entities.into_iter().next()
{
let name = if let Some(FinanceEntity::Tag(name_tag)) = tags.get("name") {
name_tag.tag_value.clone()
} else {
String::new()
(account, name)
"message": "Account not found",
return Err((StatusCode::NOT_FOUND, Json(error_response)));
let server_user = server::user::User { id: user.id };
let tags: Vec<finance::tag::Tag> = server_user
.get_account_tags(&account)
.unwrap_or_default()
.into_iter()
.filter(|t| t.tag_name != "name")
.collect();
#[cfg(feature = "scripting")]
let scripts: Vec<ScriptView> = server_user
.list_scripts()
.map(|s| ScriptView {
id: s.id,
name: s.name,
})
#[cfg(not(feature = "scripting"))]
let scripts: Vec<ScriptView> = Vec::new();
let template = AccountEditPage {
account_id: account.id,
account_name: name,
tags,
scripting_enabled: cfg!(feature = "scripting"),
scripts,
Ok(HtmlTemplate(template))
#[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(
Json(form): Json<RenameForm>,
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)
"message": t!("Failed to rename account"),
log::error!("Failed to rename account: {e:?}");
Ok(t!("Account renamed").to_string())
pub async fn account_tags_submit(
Json(form): Json<AccountTagsForm>,
let account = Account { id, parent: None };
let existing_tags = server_user
.unwrap_or_default();
for tag in &existing_tags {
if tag.tag_name != "name" {
let _ = server_user.delete_tag(tag.id).await;
for tag_data in form.tags {
if tag_data.name == "name" {
continue;
.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;
use server::script::TransactionState;
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::new();
let mut processed = 0u64;
for tx_id in &transaction_ids {
let tx_result = server::command::transaction::GetTransaction::new()
.transaction_id(*tx_id)
"message": format!("Failed to get transaction {tx_id}: {e:?}"),
let (transaction, note) = if let Some(CmdResult::TaggedEntities { mut entities, .. }) =
tx_result
&& let Some((FinanceEntity::Transaction(tx), tags)) = entities.pop()
let note = tags.get("note").and_then(|entity| {
if let FinanceEntity::Tag(tag) = entity {
Some(tag.tag_value.clone())
None
(tx, note)
let splits_result = server::command::split::ListSplits::new()
.transaction(*tx_id)
"message": format!("Failed to get splits for {tx_id}: {e:?}"),
let mut split_entities = Vec::new();
if let Some(CmdResult::TaggedEntities {
entities: split_data,
..
}) = splits_result
for (entity, _tags) in split_data {
split_entities.push(entity);
let state = TransactionState::new(transaction)
.with(split_entities)
.with_note(note);
let state = state
.run_scripts(&executor, &[script.bytecode.clone()])
"message": format!("Script execution failed on {tx_id}: {e:?}"),
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")))