Files
vmall/apps/api/src/money.rs
T
Chengdong ZhangandCursor 5e866d08f4 refactor(api): split Axum handlers into handler/service/repo modules
Keep the REST contract; move domain logic out of route files so checkout, fulfillment, and identity can be reused across customer, shop, and admin surfaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 14:59:36 +08:00

71 lines
2.1 KiB
Rust

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<i64, ApiError> {
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);
}
}