1
use std::sync::Arc;
2

            
3
use askama::Template;
4
use axum::{Extension, Form, extract::State, http::StatusCode, response::IntoResponse};
5
use serde::Deserialize;
6
use server::command::{
7
    CmdResult, FinanceEntity, commodity::ConvertCommodity, commodity::ListCommodities,
8
};
9
use sqlx::types::Uuid;
10

            
11
use crate::pages::transaction::util::parse_amount_to_rational;
12
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
13

            
14
#[derive(Clone)]
15
pub 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")]
23
struct ConvertPage {
24
    commodities: Vec<CommodityItem>,
25
}
26

            
27
#[derive(Template)]
28
#[template(path = "components/commodity/convert_result.html")]
29
struct ConvertResultTemplate {
30
    result: String,
31
    target_symbol: String,
32
}
33

            
34
#[derive(Template)]
35
#[template(path = "components/commodity/convert_error.html")]
36
struct ConvertErrorTemplate {
37
    message: String,
38
}
39

            
40
#[derive(Deserialize)]
41
pub struct ConvertForm {
42
    pub amount: String,
43
    pub source_commodity_id: String,
44
    pub target_commodity_id: String,
45
}
46

            
47
1
async fn load_commodities(user_id: Uuid) -> Result<Vec<CommodityItem>, StatusCode> {
48
1
    let result = ListCommodities::new()
49
1
        .user_id(user_id)
50
1
        .run()
51
1
        .await
52
1
        .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
1
}
75

            
76
1
pub async fn commodity_convert_page(
77
1
    State(_data): State<Arc<AppState>>,
78
1
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
79
1
) -> Result<impl IntoResponse, StatusCode> {
80
1
    let commodities = load_commodities(jwt_auth.user.id).await?;
81
    Ok(HtmlTemplate(ConvertPage { commodities }))
82
1
}
83

            
84
3
pub async fn commodity_convert_submit(
85
3
    State(_data): State<Arc<AppState>>,
86
3
    Extension(jwt_auth): Extension<JWTAuthMiddleware>,
87
3
    Form(form): Form<ConvertForm>,
88
3
) -> impl IntoResponse {
89
3
    let (amount_num, amount_denom) = match parse_amount_to_rational(&form.amount) {
90
2
        Ok(r) => r,
91
        Err(_) => {
92
            return HtmlTemplate(ConvertErrorTemplate {
93
1
                message: t!("Invalid amount").to_string(),
94
            })
95
1
            .into_response();
96
        }
97
    };
98

            
99
2
    let source_id = match Uuid::parse_str(&form.source_commodity_id) {
100
1
        Ok(id) => id,
101
        Err(_) => {
102
            return HtmlTemplate(ConvertErrorTemplate {
103
1
                message: t!("Invalid source commodity").to_string(),
104
            })
105
1
            .into_response();
106
        }
107
    };
108

            
109
1
    let target_id = match Uuid::parse_str(&form.target_commodity_id) {
110
1
        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
1
    let run_result = ConvertCommodity::new()
120
1
        .user_id(jwt_auth.user.id)
121
1
        .amount_num(amount_num)
122
1
        .amount_denom(amount_denom)
123
1
        .source_commodity_id(source_id)
124
1
        .target_commodity_id(target_id)
125
1
        .run()
126
1
        .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
1
        Err(e) => {
149
1
            let msg = e.to_string();
150
            HtmlTemplate(ConvertErrorTemplate {
151
1
                message: if msg.contains("no Price row") {
152
                    t!("No exchange rate found between these commodities").to_string()
153
                } else {
154
1
                    msg
155
                },
156
            })
157
1
            .into_response()
158
        }
159
    }
160
3
}