Lines
53.85 %
Functions
18.75 %
Branches
100 %
use std::sync::Arc;
use askama::Template;
use axum::{Extension, Form, extract::State, http::StatusCode, response::IntoResponse};
use serde::Deserialize;
use server::command::{
CmdResult, FinanceEntity, commodity::ConvertCommodity, commodity::ListCommodities,
};
use sqlx::types::Uuid;
use crate::pages::transaction::util::parse_amount_to_rational;
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
#[derive(Clone)]
pub struct CommodityItem {
pub id: String,
pub symbol: String,
pub name: String,
}
#[derive(Template)]
#[template(path = "pages/commodity/convert.html")]
struct ConvertPage {
commodities: Vec<CommodityItem>,
#[template(path = "components/commodity/convert_result.html")]
struct ConvertResultTemplate {
result: String,
target_symbol: String,
#[template(path = "components/commodity/convert_error.html")]
struct ConvertErrorTemplate {
message: String,
#[derive(Deserialize)]
pub struct ConvertForm {
pub amount: String,
pub source_commodity_id: String,
pub target_commodity_id: String,
async fn load_commodities(user_id: Uuid) -> Result<Vec<CommodityItem>, StatusCode> {
let result = ListCommodities::new()
.user_id(user_id)
.run()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut commodities = Vec::new();
if let Some(CmdResult::TaggedEntities { entities, .. }) = result {
for (entity, tags) in entities {
if let FinanceEntity::Commodity(commodity) = entity {
let (symbol, name) = if let (FinanceEntity::Tag(s), FinanceEntity::Tag(n)) =
(&tags["symbol"], &tags["name"])
{
(s.tag_value.clone(), n.tag_value.clone())
} else {
return Err(StatusCode::INTERNAL_SERVER_ERROR);
commodities.push(CommodityItem {
id: commodity.id.to_string(),
symbol,
name,
});
Ok(commodities)
pub async fn commodity_convert_page(
State(_data): State<Arc<AppState>>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
) -> Result<impl IntoResponse, StatusCode> {
let commodities = load_commodities(jwt_auth.user.id).await?;
Ok(HtmlTemplate(ConvertPage { commodities }))
pub async fn commodity_convert_submit(
Form(form): Form<ConvertForm>,
) -> impl IntoResponse {
let (amount_num, amount_denom) = match parse_amount_to_rational(&form.amount) {
Ok(r) => r,
Err(_) => {
return HtmlTemplate(ConvertErrorTemplate {
message: t!("Invalid amount").to_string(),
})
.into_response();
let source_id = match Uuid::parse_str(&form.source_commodity_id) {
Ok(id) => id,
message: t!("Invalid source commodity").to_string(),
let target_id = match Uuid::parse_str(&form.target_commodity_id) {
message: t!("Invalid target commodity").to_string(),
let run_result = ConvertCommodity::new()
.user_id(jwt_auth.user.id)
.amount_num(amount_num)
.amount_denom(amount_denom)
.source_commodity_id(source_id)
.target_commodity_id(target_id)
.await;
match run_result {
Ok(Some(CmdResult::Rational(r))) => {
let target_symbol = load_commodities(jwt_auth.user.id)
.ok()
.and_then(|cs| cs.into_iter().find(|c| c.id == form.target_commodity_id))
.map(|c| c.symbol)
.unwrap_or_default();
let result = format!("{:.6}", *r.numer() as f64 / *r.denom() as f64);
HtmlTemplate(ConvertResultTemplate {
result,
target_symbol,
.into_response()
Ok(_) => HtmlTemplate(ConvertErrorTemplate {
message: t!("Unexpected result from conversion").to_string(),
.into_response(),
Err(e) => {
let msg = e.to_string();
HtmlTemplate(ConvertErrorTemplate {
message: if msg.contains("no Price row") {
t!("No exchange rate found between these commodities").to_string()
msg
},