money.rs (7232B)
1 mod common; 2 3 use radroots_core::{RadrootsCoreCurrency, RadrootsCoreMoney, RadrootsCoreMoneyInvariantError}; 4 use rust_decimal::RoundingStrategy; 5 6 #[test] 7 fn zero_and_is_zero() { 8 let usd = RadrootsCoreCurrency::USD; 9 let zero = RadrootsCoreMoney::zero(usd); 10 assert!(zero.is_zero()); 11 assert_eq!(zero.currency, usd); 12 } 13 14 #[test] 15 fn ensure_non_negative_rejects_negative_amount() { 16 let money = RadrootsCoreMoney::new(common::dec("-1"), RadrootsCoreCurrency::USD); 17 assert_eq!( 18 money.ensure_non_negative(), 19 Err(RadrootsCoreMoneyInvariantError::NegativeAmount) 20 ); 21 } 22 23 #[test] 24 fn ensure_non_negative_accepts_zero_and_positive() { 25 let zero = RadrootsCoreMoney::new(common::dec("0"), RadrootsCoreCurrency::USD); 26 let pos = RadrootsCoreMoney::new(common::dec("1"), RadrootsCoreCurrency::USD); 27 assert_eq!(zero.ensure_non_negative(), Ok(())); 28 assert_eq!(pos.ensure_non_negative(), Ok(())); 29 } 30 31 #[test] 32 fn quantize_to_currency_rounds_midpoint_away_from_zero() { 33 let usd = RadrootsCoreCurrency::USD; 34 let a = RadrootsCoreMoney::new(common::dec("1.234"), usd).quantize_to_currency(); 35 let b = RadrootsCoreMoney::new(common::dec("1.235"), usd).quantize_to_currency(); 36 let c = RadrootsCoreMoney::new(common::dec("-1.235"), usd).quantize_to_currency(); 37 38 assert_eq!(a.amount, common::dec("1.23")); 39 assert_eq!(b.amount, common::dec("1.24")); 40 assert_eq!(c.amount, common::dec("-1.24")); 41 } 42 43 #[test] 44 fn quantize_to_currency_with_strategy_uses_strategy() { 45 let usd = RadrootsCoreCurrency::USD; 46 let a = RadrootsCoreMoney::new(common::dec("1.235"), usd) 47 .quantize_to_currency_with_strategy(RoundingStrategy::MidpointTowardZero); 48 let b = RadrootsCoreMoney::new(common::dec("-1.235"), usd) 49 .quantize_to_currency_with_strategy(RoundingStrategy::MidpointTowardZero); 50 assert_eq!(a.amount, common::dec("1.23")); 51 assert_eq!(b.amount, common::dec("-1.23")); 52 } 53 54 #[test] 55 fn checked_add_and_sub_require_currency_match() { 56 let usd = RadrootsCoreCurrency::USD; 57 let eur = RadrootsCoreCurrency::EUR; 58 let a = RadrootsCoreMoney::new(common::dec("1.00"), usd); 59 let b = RadrootsCoreMoney::new(common::dec("2.00"), usd); 60 let c = RadrootsCoreMoney::new(common::dec("3.00"), eur); 61 62 assert_eq!(a.checked_add(&b).unwrap().amount, common::dec("3.00")); 63 assert_eq!( 64 a.checked_add(&c), 65 Err(RadrootsCoreMoneyInvariantError::CurrencyMismatch) 66 ); 67 assert_eq!(b.checked_sub(&a).unwrap().amount, common::dec("1.00")); 68 } 69 70 #[test] 71 fn checked_sub_mismatch_returns_currency_error() { 72 let usd = RadrootsCoreCurrency::USD; 73 let eur = RadrootsCoreCurrency::EUR; 74 let a = RadrootsCoreMoney::new(common::dec("1.00"), usd); 75 let b = RadrootsCoreMoney::new(common::dec("2.00"), eur); 76 assert_eq!( 77 a.checked_sub(&b), 78 Err(RadrootsCoreMoneyInvariantError::CurrencyMismatch) 79 ); 80 } 81 82 #[test] 83 fn minor_units_exact_and_rounded() { 84 let usd = RadrootsCoreCurrency::USD; 85 let exact = RadrootsCoreMoney::new(common::dec("1.23"), usd); 86 let frac = RadrootsCoreMoney::new(common::dec("1.234"), usd); 87 let rounded = RadrootsCoreMoney::new(common::dec("1.235"), usd); 88 89 assert_eq!(exact.to_minor_units_u64_exact().unwrap(), 123); 90 assert_eq!( 91 frac.to_minor_units_u64_exact(), 92 Err(RadrootsCoreMoneyInvariantError::NotWholeMinorUnits) 93 ); 94 assert_eq!( 95 rounded 96 .to_minor_units_u64_rounded(RoundingStrategy::MidpointAwayFromZero) 97 .unwrap(), 98 124 99 ); 100 } 101 102 #[test] 103 fn minor_units_cover_additional_currency_exponents() { 104 let jpy = RadrootsCoreMoney::new(common::dec("123"), common::currency("JPY")); 105 assert_eq!(jpy.to_minor_units_u64_exact().unwrap(), 123); 106 assert_eq!( 107 jpy.to_minor_units_u64_rounded(RoundingStrategy::MidpointAwayFromZero) 108 .unwrap(), 109 123 110 ); 111 112 let kwd = RadrootsCoreMoney::new(common::dec("1.234"), common::currency("KWD")); 113 assert_eq!(kwd.to_minor_units_u64_exact().unwrap(), 1234); 114 } 115 116 #[test] 117 fn minor_units_u32_overflow_is_detected() { 118 let usd = RadrootsCoreCurrency::USD; 119 let too_large = RadrootsCoreMoney::from_minor_units_u64(u64::from(u32::MAX) + 1, usd); 120 assert_eq!( 121 too_large.to_minor_units_u32_exact(), 122 Err(RadrootsCoreMoneyInvariantError::AmountOverflow) 123 ); 124 } 125 126 #[test] 127 fn minor_units_u32_exact_success_path_is_exercised() { 128 let usd = RadrootsCoreCurrency::USD; 129 let m = RadrootsCoreMoney::new(common::dec("42.01"), usd); 130 assert_eq!(m.to_minor_units_u32_exact().unwrap(), 4201); 131 let fractional = RadrootsCoreMoney::new(common::dec("1.001"), usd); 132 assert_eq!( 133 fractional.to_minor_units_u32_exact(), 134 Err(RadrootsCoreMoneyInvariantError::NotWholeMinorUnits) 135 ); 136 } 137 138 #[test] 139 fn from_minor_units_u32_and_u32_rounded_paths_are_exercised() { 140 let usd = RadrootsCoreCurrency::USD; 141 let from_u32 = RadrootsCoreMoney::from_minor_units_u32(505, usd); 142 assert_eq!(from_u32.amount, common::dec("5.05")); 143 assert_eq!( 144 from_u32 145 .to_minor_units_u32_rounded(RoundingStrategy::MidpointAwayFromZero) 146 .unwrap(), 147 505 148 ); 149 } 150 151 #[test] 152 fn minor_units_u32_rounded_overflow_is_detected() { 153 let usd = RadrootsCoreCurrency::USD; 154 let too_large = RadrootsCoreMoney::from_minor_units_u64(u64::from(u32::MAX) + 1, usd); 155 assert_eq!( 156 too_large.to_minor_units_u32_rounded(RoundingStrategy::MidpointAwayFromZero), 157 Err(RadrootsCoreMoneyInvariantError::AmountOverflow) 158 ); 159 let negative = RadrootsCoreMoney::new(common::dec("-1.00"), usd); 160 assert_eq!( 161 negative.to_minor_units_u32_rounded(RoundingStrategy::MidpointAwayFromZero), 162 Err(RadrootsCoreMoneyInvariantError::AmountOverflow) 163 ); 164 } 165 166 #[test] 167 fn with_scale_path_is_exercised() { 168 let usd = RadrootsCoreCurrency::USD; 169 let m = RadrootsCoreMoney::new(common::dec("1.2300"), usd).with_scale(1); 170 assert_eq!(m.amount, common::dec("1.2")); 171 } 172 173 #[test] 174 fn from_minor_units_roundtrips() { 175 let usd = RadrootsCoreCurrency::USD; 176 let money = RadrootsCoreMoney::from_minor_units_u64(12345, usd); 177 assert_eq!(money.to_minor_units_u64_exact().unwrap(), 12345); 178 } 179 180 #[test] 181 fn display_and_operator_impl_paths_are_exercised() { 182 let usd = RadrootsCoreCurrency::USD; 183 let m = RadrootsCoreMoney::new(common::dec("10"), usd); 184 assert_eq!(m.to_string(), "10 USD"); 185 186 let times = m.clone() * common::dec("2"); 187 assert_eq!(times.amount, common::dec("20")); 188 let divided = m / common::dec("4"); 189 assert_eq!(divided.amount, common::dec("2.5")); 190 } 191 192 #[test] 193 fn invariant_error_display_variants_are_exercised() { 194 assert_eq!( 195 RadrootsCoreMoneyInvariantError::NegativeAmount.to_string(), 196 "money amount must be ≥ 0" 197 ); 198 assert_eq!( 199 RadrootsCoreMoneyInvariantError::NotWholeMinorUnits.to_string(), 200 "money not a whole number of minor units" 201 ); 202 assert_eq!( 203 RadrootsCoreMoneyInvariantError::AmountOverflow.to_string(), 204 "money minor-unit conversion overflow" 205 ); 206 assert_eq!( 207 RadrootsCoreMoneyInvariantError::CurrencyMismatch.to_string(), 208 "money currency mismatch" 209 ); 210 }