quantity_price.rs (7910B)
1 mod common; 2 3 use radroots_core::{ 4 RadrootsCoreQuantityPrice, RadrootsCoreQuantityPriceError, RadrootsCoreQuantityPriceOps, 5 RadrootsCoreUnit, 6 }; 7 8 #[test] 9 fn cost_for_scales_by_ratio() { 10 let price = RadrootsCoreQuantityPrice::new( 11 common::money("10", "USD"), 12 common::qty("1", RadrootsCoreUnit::MassKg), 13 ); 14 let cost = price.cost_for(&common::qty("2", RadrootsCoreUnit::MassKg)); 15 assert_eq!(cost.amount, common::dec("20")); 16 } 17 18 #[test] 19 fn cost_for_returns_zero_on_unit_mismatch() { 20 let price = RadrootsCoreQuantityPrice::new( 21 common::money("10", "USD"), 22 common::qty("1", RadrootsCoreUnit::MassKg), 23 ); 24 let cost = price.cost_for(&common::qty("1", RadrootsCoreUnit::Each)); 25 assert!(cost.amount.is_zero()); 26 } 27 28 #[test] 29 fn cost_for_rounded_and_quantized_price_differ() { 30 let price = RadrootsCoreQuantityPrice::new( 31 common::money("1.005", "USD"), 32 common::qty("1", RadrootsCoreUnit::Each), 33 ); 34 let qty = common::qty("2", RadrootsCoreUnit::Each); 35 let rounded = price.cost_for_rounded(&qty); 36 let quantized = price.cost_for_with_quantized_price(&qty); 37 38 assert_eq!(rounded.amount, common::dec("2.01")); 39 assert_eq!(quantized.amount, common::dec("2.02")); 40 } 41 42 #[test] 43 fn try_cost_for_validates_quantity_and_units() { 44 let price = RadrootsCoreQuantityPrice::new( 45 common::money("10", "USD"), 46 common::qty("1", RadrootsCoreUnit::Each), 47 ); 48 let zero_price = RadrootsCoreQuantityPrice::new( 49 common::money("10", "USD"), 50 common::qty("0", RadrootsCoreUnit::Each), 51 ); 52 53 assert_eq!( 54 zero_price.try_cost_for(&common::qty("1", RadrootsCoreUnit::Each)), 55 Err(RadrootsCoreQuantityPriceError::PerQuantityZero) 56 ); 57 assert_eq!( 58 price.try_cost_for(&common::qty("1", RadrootsCoreUnit::MassKg)), 59 Err(RadrootsCoreQuantityPriceError::UnitMismatch { 60 have: RadrootsCoreUnit::MassKg, 61 want: RadrootsCoreUnit::Each 62 }) 63 ); 64 } 65 66 #[test] 67 fn try_cost_for_rounded_error_path_is_exercised() { 68 let price = RadrootsCoreQuantityPrice::new( 69 common::money("10", "USD"), 70 common::qty("1", RadrootsCoreUnit::Each), 71 ); 72 assert_eq!( 73 price.try_cost_for_rounded(&common::qty("1", RadrootsCoreUnit::MassKg)), 74 Err(RadrootsCoreQuantityPriceError::UnitMismatch { 75 have: RadrootsCoreUnit::MassKg, 76 want: RadrootsCoreUnit::Each 77 }) 78 ); 79 } 80 81 #[test] 82 fn try_cost_for_amount_in_converts_mass_units() { 83 let price = RadrootsCoreQuantityPrice::new( 84 common::money("10", "USD"), 85 common::qty("1", RadrootsCoreUnit::MassKg), 86 ); 87 let cost = price 88 .try_cost_for_amount_in(common::dec("500"), RadrootsCoreUnit::MassG) 89 .unwrap(); 90 assert_eq!(cost.amount, common::dec("5")); 91 } 92 93 #[test] 94 fn try_cost_for_amount_in_converts_volume_units() { 95 let price = RadrootsCoreQuantityPrice::new( 96 common::money("10", "USD"), 97 common::qty("1", RadrootsCoreUnit::VolumeL), 98 ); 99 let cost = price 100 .try_cost_for_amount_in(common::dec("500"), RadrootsCoreUnit::VolumeMl) 101 .unwrap(); 102 assert_eq!(cost.amount, common::dec("5")); 103 } 104 105 #[test] 106 fn try_cost_for_amount_in_rejects_non_convertible_units() { 107 let price = RadrootsCoreQuantityPrice::new( 108 common::money("10", "USD"), 109 common::qty("1", RadrootsCoreUnit::MassKg), 110 ); 111 assert_eq!( 112 price.try_cost_for_amount_in(common::dec("1"), RadrootsCoreUnit::Each), 113 Err(RadrootsCoreQuantityPriceError::NonConvertibleUnits { 114 from: RadrootsCoreUnit::Each, 115 to: RadrootsCoreUnit::MassKg 116 }) 117 ); 118 } 119 120 #[test] 121 fn try_cost_for_amount_in_same_unit_path_is_exercised() { 122 let price = RadrootsCoreQuantityPrice::new( 123 common::money("4", "USD"), 124 common::qty("1", RadrootsCoreUnit::Each), 125 ); 126 let out = price 127 .try_cost_for_amount_in(common::dec("3"), RadrootsCoreUnit::Each) 128 .unwrap(); 129 assert_eq!(out.amount, common::dec("12")); 130 } 131 132 #[test] 133 fn try_cost_for_quantity_in_path_is_exercised() { 134 let price = RadrootsCoreQuantityPrice::new( 135 common::money("10", "USD"), 136 common::qty("1", RadrootsCoreUnit::MassKg), 137 ); 138 let qty = common::qty("250", RadrootsCoreUnit::MassG); 139 let out = price.try_cost_for_quantity_in(&qty).unwrap(); 140 assert_eq!(out.amount, common::dec("2.5")); 141 } 142 143 #[test] 144 fn try_to_unit_price_error_and_same_unit_paths_are_exercised() { 145 let zero = RadrootsCoreQuantityPrice::new( 146 common::money("10", "USD"), 147 common::qty("0", RadrootsCoreUnit::MassKg), 148 ); 149 assert_eq!( 150 zero.try_to_unit_price(RadrootsCoreUnit::MassG), 151 Err(RadrootsCoreQuantityPriceError::PerQuantityZero) 152 ); 153 154 let base = RadrootsCoreQuantityPrice::new( 155 common::money("5", "USD"), 156 common::qty("2", RadrootsCoreUnit::MassKg), 157 ); 158 let same = base.try_to_unit_price(RadrootsCoreUnit::MassKg).unwrap(); 159 assert_eq!(same.quantity.unit, RadrootsCoreUnit::MassKg); 160 assert_eq!(same.quantity.amount, common::dec("1")); 161 assert_eq!(same.amount.amount, common::dec("2.5")); 162 163 let err = base 164 .try_to_unit_price(RadrootsCoreUnit::VolumeMl) 165 .unwrap_err(); 166 assert_eq!( 167 err, 168 RadrootsCoreQuantityPriceError::NonConvertibleUnits { 169 from: RadrootsCoreUnit::MassKg, 170 to: RadrootsCoreUnit::VolumeMl 171 } 172 ); 173 } 174 175 #[test] 176 fn cost_for_and_quantized_price_zero_paths_are_exercised() { 177 let p = RadrootsCoreQuantityPrice::new( 178 common::money("3.33", "USD"), 179 common::qty("1", RadrootsCoreUnit::Each), 180 ); 181 let zero_qty = common::qty("0", RadrootsCoreUnit::Each); 182 assert!(p.cost_for(&zero_qty).amount.is_zero()); 183 assert!(p.cost_for_with_quantized_price(&zero_qty).amount.is_zero()); 184 185 let zero_per = RadrootsCoreQuantityPrice::new( 186 common::money("3.33", "USD"), 187 common::qty("0", RadrootsCoreUnit::Each), 188 ); 189 assert!( 190 zero_per 191 .cost_for(&common::qty("1", RadrootsCoreUnit::Each)) 192 .amount 193 .is_zero() 194 ); 195 assert!( 196 zero_per 197 .cost_for_with_quantized_price(&common::qty("1", RadrootsCoreUnit::Each)) 198 .amount 199 .is_zero() 200 ); 201 202 let mismatch_qty = common::qty("1", RadrootsCoreUnit::MassG); 203 assert!( 204 p.cost_for_with_quantized_price(&mismatch_qty) 205 .amount 206 .is_zero() 207 ); 208 } 209 210 #[test] 211 fn try_to_unit_price_detects_underflow_to_zero_normalized_amount() { 212 let tiny = RadrootsCoreQuantityPrice::new( 213 common::money("1", "USD"), 214 common::qty("0.0000000000000000000000000001", RadrootsCoreUnit::VolumeMl), 215 ); 216 let err = tiny 217 .try_to_unit_price(RadrootsCoreUnit::VolumeL) 218 .unwrap_err(); 219 assert_eq!(err, RadrootsCoreQuantityPriceError::PerQuantityZero); 220 } 221 222 #[test] 223 fn try_to_canonical_unit_price_converts_units() { 224 let price = RadrootsCoreQuantityPrice::new( 225 common::money("6.99", "USD"), 226 common::qty("1", RadrootsCoreUnit::MassLb), 227 ); 228 let canonical = price.try_to_canonical_unit_price().unwrap(); 229 assert_eq!(canonical.quantity.unit, RadrootsCoreUnit::MassG); 230 assert_eq!(canonical.quantity.amount, common::dec("1")); 231 let expected = common::dec("6.99") / common::dec("453.59237"); 232 assert_eq!(canonical.amount.amount, expected); 233 } 234 235 #[test] 236 fn is_price_per_canonical_unit_detects_canonical() { 237 let price = RadrootsCoreQuantityPrice::new( 238 common::money("1.00", "USD"), 239 common::qty("1", RadrootsCoreUnit::MassG), 240 ); 241 assert!(price.is_price_per_canonical_unit()); 242 243 let price = RadrootsCoreQuantityPrice::new( 244 common::money("1.00", "USD"), 245 common::qty("1", RadrootsCoreUnit::MassKg), 246 ); 247 assert!(!price.is_price_per_canonical_unit()); 248 }