106 lines
3.3 KiB
Rust
106 lines
3.3 KiB
Rust
use crate::error::ApiResult;
|
|
use crate::models::Currency;
|
|
use crate::money::convert_minor;
|
|
use crate::state::AppState;
|
|
|
|
pub async fn load_currencies(state: &AppState, enabled_only: bool) -> ApiResult<Vec<Currency>> {
|
|
let rows = if enabled_only {
|
|
sqlx::query_as::<_, Currency>(
|
|
"SELECT code, name, symbol, exponent, is_base, rate_to_base, enabled
|
|
FROM currencies WHERE enabled = TRUE ORDER BY code",
|
|
)
|
|
.fetch_all(&state.db)
|
|
.await?
|
|
} else {
|
|
sqlx::query_as::<_, Currency>(
|
|
"SELECT code, name, symbol, exponent, is_base, rate_to_base, enabled
|
|
FROM currencies ORDER BY code",
|
|
)
|
|
.fetch_all(&state.db)
|
|
.await?
|
|
};
|
|
Ok(rows)
|
|
}
|
|
|
|
pub async fn convert(
|
|
state: &AppState,
|
|
amount_minor: i64,
|
|
from: &str,
|
|
to: &str,
|
|
) -> ApiResult<(i64, String)> {
|
|
let currencies = load_currencies(state, true).await?;
|
|
let from_c = currencies
|
|
.iter()
|
|
.find(|c| c.code == from.to_uppercase())
|
|
.ok_or_else(|| {
|
|
crate::error::ApiError::BadRequest(format!("unknown or disabled currency: {from}"))
|
|
})?;
|
|
let to_c = currencies
|
|
.iter()
|
|
.find(|c| c.code == to.to_uppercase())
|
|
.ok_or_else(|| {
|
|
crate::error::ApiError::BadRequest(format!("unknown or disabled currency: {to}"))
|
|
})?;
|
|
let converted = convert_minor(amount_minor, from_c, to_c)?;
|
|
Ok((converted, to_c.code.clone()))
|
|
}
|
|
|
|
pub async fn upsert(
|
|
state: &AppState,
|
|
code: String,
|
|
name: serde_json::Value,
|
|
symbol: String,
|
|
exponent: i16,
|
|
rate_to_base: rust_decimal::Decimal,
|
|
enabled: bool,
|
|
) -> ApiResult<Currency> {
|
|
use crate::error::ApiError;
|
|
let code = code.to_uppercase();
|
|
if code.len() != 3 || !code.chars().all(|c| c.is_ascii_uppercase()) {
|
|
return Err(ApiError::BadRequest(
|
|
"code must be a 3-letter ISO code".into(),
|
|
));
|
|
}
|
|
if rate_to_base <= rust_decimal::Decimal::ZERO {
|
|
return Err(ApiError::BadRequest("rate_to_base must be > 0".into()));
|
|
}
|
|
if !(0..=6).contains(&exponent) {
|
|
return Err(ApiError::BadRequest("exponent must be 0..=6".into()));
|
|
}
|
|
Ok(sqlx::query_as::<_, Currency>(
|
|
"INSERT INTO currencies (code, name, symbol, exponent, rate_to_base, enabled)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (code) DO UPDATE
|
|
SET name = $2, symbol = $3, exponent = $4, rate_to_base = $5, enabled = $6
|
|
RETURNING code, name, symbol, exponent, is_base, rate_to_base, enabled",
|
|
)
|
|
.bind(&code)
|
|
.bind(&name)
|
|
.bind(&symbol)
|
|
.bind(exponent)
|
|
.bind(rate_to_base)
|
|
.bind(enabled)
|
|
.fetch_one(&state.db)
|
|
.await?)
|
|
}
|
|
|
|
pub async fn set_rate(
|
|
state: &AppState,
|
|
code: &str,
|
|
rate: rust_decimal::Decimal,
|
|
) -> ApiResult<Currency> {
|
|
use crate::error::ApiError;
|
|
if rate <= rust_decimal::Decimal::ZERO {
|
|
return Err(ApiError::BadRequest("rate_to_base must be > 0".into()));
|
|
}
|
|
sqlx::query_as::<_, Currency>(
|
|
"UPDATE currencies SET rate_to_base = $2 WHERE code = $1
|
|
RETURNING code, name, symbol, exponent, is_base, rate_to_base, enabled",
|
|
)
|
|
.bind(code.to_uppercase())
|
|
.bind(rate)
|
|
.fetch_optional(&state.db)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound("currency".into()))
|
|
}
|