Skip to main content

web/pages/commodity/convert/
submit.rs

1use std::sync::Arc;
2
3use askama::Template;
4use axum::{Extension, Form, extract::State, http::StatusCode, response::IntoResponse};
5use serde::Deserialize;
6use server::command::{
7    CmdResult, FinanceEntity, commodity::ConvertCommodity, commodity::ListCommodities,
8};
9use sqlx::types::Uuid;
10
11use crate::pages::transaction::util::parse_amount_to_rational;
12use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
13
14#[derive(Clone)]
15pub struct CommodityItem {
16    pub id: String,
17    pub symbol: String,
18    pub name: String,
19}
20
21#[derive(Template)]
22#[template(path = "pages/commodity/convert.html")]
23struct ConvertPage {
24    commodities: Vec<CommodityItem>,
25}
26
27#[derive(Template)]
28#[template(path = "components/commodity/convert_result.html")]
29struct ConvertResultTemplate {
30    result: String,
31    target_symbol: String,
32}
33
34#[derive(Template)]
35#[template(path = "components/commodity/convert_error.html")]
36struct ConvertErrorTemplate {
37    message: String,
38}
39
40#[derive(Deserialize)]
41pub struct ConvertForm {
42    pub amount: String,
43    pub source_commodity_id: String,
44    pub target_commodity_id: String,
45}
46
47async fn load_commodities(user_id: Uuid) -> Result<Vec<CommodityItem>, StatusCode> {
48    let result = ListCommodities::new()
49        .user_id(user_id)
50        .run()
51        .await
52        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
53
54    let mut commodities = Vec::new();
55    if let Some(CmdResult::TaggedEntities { entities, .. }) = result {
56        for (entity, tags) in entities {
57            if let FinanceEntity::Commodity(commodity) = entity {
58                let (symbol, name) = if let (FinanceEntity::Tag(s), FinanceEntity::Tag(n)) =
59                    (&tags["symbol"], &tags["name"])
60                {
61                    (s.tag_value.clone(), n.tag_value.clone())
62                } else {
63                    return Err(StatusCode::INTERNAL_SERVER_ERROR);
64                };
65                commodities.push(CommodityItem {
66                    id: commodity.id.to_string(),
67                    symbol,
68                    name,
69                });
70            }
71        }
72    }
73    Ok(commodities)
74}
75
76pub async fn commodity_convert_page(
77    State(_data): State<Arc<AppState>>,
78    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
79) -> Result<impl IntoResponse, StatusCode> {
80    let commodities = load_commodities(jwt_auth.user.id).await?;
81    Ok(HtmlTemplate(ConvertPage { commodities }))
82}
83
84pub async fn commodity_convert_submit(
85    State(_data): State<Arc<AppState>>,
86    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
87    Form(form): Form<ConvertForm>,
88) -> impl IntoResponse {
89    let (amount_num, amount_denom) = match parse_amount_to_rational(&form.amount) {
90        Ok(r) => r,
91        Err(_) => {
92            return HtmlTemplate(ConvertErrorTemplate {
93                message: t!("Invalid amount").to_string(),
94            })
95            .into_response();
96        }
97    };
98
99    let source_id = match Uuid::parse_str(&form.source_commodity_id) {
100        Ok(id) => id,
101        Err(_) => {
102            return HtmlTemplate(ConvertErrorTemplate {
103                message: t!("Invalid source commodity").to_string(),
104            })
105            .into_response();
106        }
107    };
108
109    let target_id = match Uuid::parse_str(&form.target_commodity_id) {
110        Ok(id) => id,
111        Err(_) => {
112            return HtmlTemplate(ConvertErrorTemplate {
113                message: t!("Invalid target commodity").to_string(),
114            })
115            .into_response();
116        }
117    };
118
119    let run_result = ConvertCommodity::new()
120        .user_id(jwt_auth.user.id)
121        .amount_num(amount_num)
122        .amount_denom(amount_denom)
123        .source_commodity_id(source_id)
124        .target_commodity_id(target_id)
125        .run()
126        .await;
127
128    match run_result {
129        Ok(Some(CmdResult::Rational(r))) => {
130            let target_symbol = load_commodities(jwt_auth.user.id)
131                .await
132                .ok()
133                .and_then(|cs| cs.into_iter().find(|c| c.id == form.target_commodity_id))
134                .map(|c| c.symbol)
135                .unwrap_or_default();
136
137            let result = format!("{:.6}", *r.numer() as f64 / *r.denom() as f64);
138            HtmlTemplate(ConvertResultTemplate {
139                result,
140                target_symbol,
141            })
142            .into_response()
143        }
144        Ok(_) => HtmlTemplate(ConvertErrorTemplate {
145            message: t!("Unexpected result from conversion").to_string(),
146        })
147        .into_response(),
148        Err(e) => {
149            let msg = e.to_string();
150            HtmlTemplate(ConvertErrorTemplate {
151                message: if msg.contains("no Price row") {
152                    t!("No exchange rate found between these commodities").to_string()
153                } else {
154                    msg
155                },
156            })
157            .into_response()
158        }
159    }
160}