web/pages/commodity/convert/
submit.rs1use 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}