money.rs (4493B)
1 mod common; 2 3 use radroots_core::{Currency, Decimal, Money, money::Error}; 4 5 #[test] 6 fn zero_and_checked_constructor_preserve_invariants() { 7 let zero = Money::zero(Currency::USD); 8 assert!(zero.is_zero()); 9 assert_eq!(zero.amount(), Decimal::ZERO); 10 assert_eq!(zero.currency(), Currency::USD); 11 12 assert_eq!( 13 Money::try_new(common::dec("-0.01"), Currency::USD), 14 Err(Error::NegativeAmount) 15 ); 16 let signed_zero = Money::try_new(common::dec("-0.00"), Currency::USD).unwrap(); 17 assert_eq!(signed_zero.amount(), Decimal::ZERO); 18 } 19 20 #[test] 21 fn quantization_is_deterministic_midpoint_away_from_zero() { 22 let a = common::money("1.234", "USD").quantize_to_currency(); 23 let b = common::money("1.235", "USD").quantize_to_currency(); 24 assert_eq!(a.amount(), common::dec("1.23")); 25 assert_eq!(b.amount(), common::dec("1.24")); 26 } 27 28 #[test] 29 fn checked_arithmetic_reports_domain_failures() { 30 let one = common::money("1.00", "USD"); 31 let two = common::money("2.00", "USD"); 32 let euros = common::money("3.00", "EUR"); 33 34 assert_eq!(one.checked_add(&two).unwrap().amount(), common::dec("3")); 35 assert_eq!(one.checked_add(&euros), Err(Error::CurrencyMismatch)); 36 assert_eq!(two.checked_sub(&one).unwrap().amount(), common::dec("1")); 37 assert_eq!(one.checked_sub(&two), Err(Error::NegativeAmount)); 38 assert_eq!( 39 one.checked_mul_decimal(common::dec("-1")), 40 Err(Error::NegativeAmount) 41 ); 42 assert_eq!( 43 one.checked_div_decimal(Decimal::ZERO), 44 Err(Error::DivisionByZero) 45 ); 46 47 let max = Money::try_new(Decimal::MAX, Currency::USD).unwrap(); 48 assert_eq!(max.checked_add(&one), Err(Error::ArithmeticOverflow)); 49 } 50 51 #[test] 52 fn minor_units_exact_and_rounded_cover_currency_exponents() { 53 let exact = common::money("1.23", "USD"); 54 let fractional = common::money("1.234", "USD"); 55 let rounded = common::money("1.235", "USD"); 56 assert_eq!(exact.to_minor_units_u64_exact(), Ok(123)); 57 assert_eq!( 58 fractional.to_minor_units_u64_exact(), 59 Err(Error::NotWholeMinorUnits) 60 ); 61 assert_eq!(rounded.to_minor_units_u64_rounded(), Ok(124)); 62 63 assert_eq!( 64 common::money("123", "JPY").to_minor_units_u64_exact(), 65 Ok(123) 66 ); 67 assert_eq!( 68 common::money("1.234", "KWD").to_minor_units_u64_exact(), 69 Ok(1234) 70 ); 71 } 72 73 #[test] 74 fn minor_unit_constructors_roundtrip_and_detect_u32_overflow() { 75 for currency in [Currency::JPY, Currency::USD, common::currency("KWD")] { 76 for minor in [0, 1, 99, 12_345, u32::MAX as u64] { 77 let money = Money::from_minor_units_u64(minor, currency); 78 assert_eq!(money.to_minor_units_u64_exact(), Ok(minor)); 79 } 80 } 81 82 let from_u32 = Money::from_minor_units_u32(505, Currency::USD); 83 assert_eq!(from_u32.amount(), common::dec("5.05")); 84 assert_eq!(from_u32.to_minor_units_u32_rounded(), Ok(505)); 85 86 let too_large = Money::from_minor_units_u64(u64::from(u32::MAX) + 1, Currency::USD); 87 assert_eq!( 88 too_large.to_minor_units_u32_exact(), 89 Err(Error::AmountOverflow) 90 ); 91 assert_eq!( 92 too_large.to_minor_units_u32_rounded(), 93 Err(Error::AmountOverflow) 94 ); 95 } 96 97 #[test] 98 fn exact_scale_change_never_rounds() { 99 let money = common::money("1.2300", "USD"); 100 assert_eq!(money.try_with_scale_exact(2).unwrap().amount().scale(), 2); 101 assert_eq!( 102 common::money("1.235", "USD").try_with_scale_exact(2), 103 Err(Error::PrecisionLoss) 104 ); 105 } 106 107 #[test] 108 fn display_and_error_messages_are_stable() { 109 assert_eq!(common::money("10", "USD").to_string(), "10 USD"); 110 assert_eq!( 111 Error::NegativeAmount.to_string(), 112 "money amount must be ≥ 0" 113 ); 114 assert_eq!( 115 Error::NotWholeMinorUnits.to_string(), 116 "money not a whole number of minor units" 117 ); 118 assert_eq!( 119 Error::AmountOverflow.to_string(), 120 "money minor-unit conversion overflow" 121 ); 122 assert_eq!( 123 Error::CurrencyMismatch.to_string(), 124 "money currency mismatch" 125 ); 126 assert_eq!( 127 Error::ArithmeticOverflow.to_string(), 128 "money arithmetic overflow" 129 ); 130 assert_eq!(Error::DivisionByZero.to_string(), "money division by zero"); 131 assert_eq!( 132 Error::ScaleOutOfRange.to_string(), 133 "money scale is outside the supported range" 134 ); 135 assert_eq!( 136 Error::PrecisionLoss.to_string(), 137 "money operation would lose precision" 138 ); 139 }