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> { 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 { 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 { 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())) }