use rust_decimal::prelude::*; use rust_decimal::{Decimal, RoundingStrategy}; use crate::error::ApiError; use crate::models::Currency; /// Convert integer minor units between currencies via base rates. /// rate_to_base = units of currency per 1 unit of base. Rounds half-up /// to whole minor units of the target currency. pub fn convert_minor(amount_minor: i64, from: &Currency, to: &Currency) -> Result { if amount_minor < 0 { return Err(ApiError::BadRequest("amount_minor must be >= 0".into())); } let amount = Decimal::from(amount_minor); let from_scale = Decimal::from(10i64.pow(from.exponent as u32)); let to_scale = Decimal::from(10i64.pow(to.exponent as u32)); // minor(from) -> major(from) -> major(base) -> minor(to) let base_major = amount / from_scale / from.rate_to_base; let target = (base_major * to.rate_to_base * to_scale) .round_dp_with_strategy(0, RoundingStrategy::MidpointAwayFromZero); target .to_i64() .ok_or_else(|| ApiError::BadRequest("amount out of range".into())) } #[cfg(test)] mod tests { use super::*; use rust_decimal_macros::dec; fn usd() -> Currency { Currency { code: "USD".into(), name: serde_json::json!({"en": "US Dollar"}), symbol: "$".into(), exponent: 2, is_base: true, rate_to_base: dec!(1), enabled: true, } } fn jpy() -> Currency { Currency { code: "JPY".into(), name: serde_json::json!({"en": "Yen"}), symbol: "¥".into(), exponent: 0, is_base: false, rate_to_base: dec!(150), enabled: true, } } #[test] fn same_currency_is_identity() { assert_eq!(convert_minor(199, &usd(), &usd()).unwrap(), 199); } #[test] fn rejects_negative() { assert!(convert_minor(-1, &usd(), &usd()).is_err()); } #[test] fn converts_via_base_half_up() { // 1.00 USD -> 150 JPY at rate 150 assert_eq!(convert_minor(100, &usd(), &jpy()).unwrap(), 150); } }