Lines
0 %
Functions
Branches
100 %
//web/src/pages/transaction/edit/submit.rs
use askama::Template;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use chrono::Local;
use serde::Deserialize;
use server::command::{CmdResult, FinanceEntity, transaction::GetTransaction};
use sqlx::types::Uuid;
use std::sync::Arc;
use crate::pages::transaction::util::{
SplitData, get_account_name, get_commodity_name, parse_transaction_date, process_split_data,
validate_splits_not_empty,
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
#[derive(Template)]
#[template(path = "pages/transaction/edit.html")]
struct TransactionEditPage {
transaction_id: String,
splits: Vec<SplitDisplayData>,
note: String,
date: String,
tags: Vec<finance::tag::Tag>,
}
#[derive(Debug)]
struct SplitDisplayData {
id: String,
amount: String,
amount_converted: String,
from_account: String,
from_account_name: String,
to_account: String,
to_account_name: String,
from_commodity: String,
from_commodity_name: String,
to_commodity: String,
to_commodity_name: String,
pub async fn transaction_edit_page(
Path(transaction_id): Path<String>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let user = &jwt_auth.user;
let tx_uuid = Uuid::parse_str(&transaction_id).map_err(|_| {
let error_response = serde_json::json!({
"status": "fail",
"message": "Invalid transaction ID format",
});
(StatusCode::BAD_REQUEST, Json(error_response))
})?;
let transaction_result = GetTransaction::new()
.user_id(user.id)
.transaction_id(tx_uuid)
.run()
.await
.map_err(|e| {
"message": format!("Failed to get transaction: {:?}", e),
(StatusCode::INTERNAL_SERVER_ERROR, Json(error_response))
let (transaction, tags) =
if let Some(CmdResult::TaggedEntities { mut entities, .. }) = transaction_result {
if let Some((FinanceEntity::Transaction(tx), tags)) = entities.pop() {
(tx, tags)
} else {
"message": "Transaction not found",
return Err((StatusCode::NOT_FOUND, Json(error_response)));
let splits_result = server::command::split::ListSplits::new()
.transaction(tx_uuid)
"message": format!("Failed to get splits: {:?}", e),
let mut splits_display = Vec::new();
if let Some(CmdResult::TaggedEntities {
entities: split_entities,
..
}) = splits_result
{
let mut logical_splits = Vec::new();
let mut processed_splits = std::collections::HashSet::new();
for (entity, tags) in &split_entities {
if let FinanceEntity::Split(split) = entity {
if processed_splits.contains(&split.id) {
continue;
let mut pair_split = None;
let mut pair_tags = None;
for (other_entity, other_tags) in &split_entities {
if let FinanceEntity::Split(other_split) = other_entity
&& other_split.id != split.id
&& !processed_splits.contains(&other_split.id)
&& (split.value_num > 0) != (other_split.value_num > 0)
pair_split = Some(other_split);
pair_tags = Some(other_tags);
break;
if let Some(pair) = pair_split {
processed_splits.insert(split.id);
processed_splits.insert(pair.id);
let (from_split, to_split, from_tags) = if split.value_num < 0 {
(split, pair, tags)
(pair, split, pair_tags.unwrap_or(tags))
let split_tags: Vec<finance::tag::Tag> = from_tags
.iter()
.filter_map(|(_, entity)| {
if let FinanceEntity::Tag(tag) = entity {
Some(finance::tag::Tag {
id: tag.id,
tag_name: tag.tag_name.clone(),
tag_value: tag.tag_value.clone(),
description: tag.description.clone(),
})
None
.collect();
logical_splits.push((from_split, to_split, split_tags));
for (from_split, to_split, split_tags) in logical_splits {
let from_account_name = get_account_name(user.id, from_split.account_id)
.unwrap_or("Unknown Account".to_string());
let to_account_name = get_account_name(user.id, to_split.account_id)
let from_commodity_name = get_commodity_name(user.id, from_split.commodity_id)
.unwrap_or("Unknown Currency".to_string());
let to_commodity_name = get_commodity_name(user.id, to_split.commodity_id)
let amount = (-from_split.value_num as f64) / (from_split.value_denom as f64);
let amount_converted = if from_split.commodity_id == to_split.commodity_id {
0.0
(to_split.value_num as f64) / (to_split.value_denom as f64)
splits_display.push(SplitDisplayData {
id: from_split.id.to_string(),
amount: format!("{amount:.2}"),
amount_converted: if amount_converted > 0.0 {
format!("{amount_converted:.2}")
String::new()
},
from_account: from_split.account_id.to_string(),
from_account_name,
to_account: to_split.account_id.to_string(),
to_account_name,
from_commodity: from_split.commodity_id.to_string(),
from_commodity_name,
to_commodity: to_split.commodity_id.to_string(),
to_commodity_name,
tags: split_tags,
let note = tags
.get("note")
.and_then(|entity| {
Some(tag.tag_value.clone())
.unwrap_or_default();
let date = transaction.post_date.format("%Y-%m-%dT%H:%M").to_string();
let server_user = server::user::User { id: user.id };
let transaction_tags = server_user
.get_transaction_tags(tx_uuid)
let template = TransactionEditPage {
transaction_id: transaction_id.clone(),
splits: splits_display,
note,
date,
tags: transaction_tags,
Ok(HtmlTemplate(template))
#[derive(Deserialize, Debug)]
pub struct TransactionEditForm {
splits: Vec<SplitData>,
note: Option<String>,
date: Option<String>,
pub async fn transaction_edit_submit(
State(_data): State<Arc<AppState>>,
Json(form): Json<TransactionEditForm>,
let tx_uuid = Uuid::parse_str(&form.transaction_id).map_err(|_| {
validate_splits_not_empty(&form.splits)?;
let post_date = parse_transaction_date(form.date.as_deref());
let post_date_utc = post_date.and_utc();
let enter_date_utc = Local::now().naive_utc().and_utc();
let mut split_entities = Vec::new();
let mut prices = Vec::new();
let mut split_tags_to_create = Vec::new();
for split_data in form.splits {
let processed = process_split_data(user.id, tx_uuid, split_data).await?;
let from_split_id = processed.from_split.id;
let to_split_id = processed.to_split.id;
split_entities.push(FinanceEntity::Split(processed.from_split));
split_entities.push(FinanceEntity::Split(processed.to_split));
if let Some(price) = processed.price {
prices.push(FinanceEntity::Price(price));
if let Some(tags) = processed.from_split_tags {
for tag in tags {
split_tags_to_create.push((from_split_id, tag));
if let Some(tags) = processed.to_split_tags {
split_tags_to_create.push((to_split_id, tag));
let mut cmd = server::command::transaction::UpdateTransaction::new()
.splits(split_entities)
.post_date(post_date_utc)
.enter_date(enter_date_utc);
if !prices.is_empty() {
cmd = cmd.prices(prices);
if let Some(note) = form.note.as_deref() {
cmd = cmd.note(note.to_string());
match cmd.run().await {
Ok(result) => {
// Create split tags after transaction is successfully updated
for (split_id, tag_data) in split_tags_to_create {
server_user
.create_split_tag(
split_id,
tag_data.name,
tag_data.value,
tag_data.description,
)
"message": format!("Failed to create split tag: {:?}", e),
log::error!("Failed to create split tag for split {split_id}: {e:?}");
match result {
Some(CmdResult::Entity(FinanceEntity::Transaction(tx))) => {
Ok(format!("{}: {}", t!("Transaction updated with ID"), tx.id))
_ => Ok(t!("Transaction updated successfully").to_string()),
Err(e) => {
"message": format!("Failed to update transaction: {:?}", e),
log::error!("Failed to update transaction: {e:?}");
Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)))