quantity_price.rs (4634B)
1 mod common; 2 3 use radroots_core::{ 4 Currency, Decimal, Money, Quantity, QuantityPrice, Unit, 5 pricing::{Error, QuantityPriceOps}, 6 }; 7 8 fn unit_price(amount: &str, per: &str, unit: Unit) -> QuantityPrice { 9 QuantityPrice::try_new(common::money(amount, "USD"), common::qty(per, unit)) 10 .expect("valid price") 11 } 12 13 #[test] 14 fn checked_cost_scales_by_ratio_and_rounds_deterministically() { 15 let price = unit_price("10", "1", Unit::MassKg); 16 let cost = price.try_cost_for(&common::qty("2", Unit::MassKg)).unwrap(); 17 assert_eq!(cost.amount(), common::dec("20")); 18 19 let rounded = unit_price("1.005", "1", Unit::Each) 20 .try_cost_for_rounded(&common::qty("2", Unit::Each)) 21 .unwrap(); 22 assert_eq!(rounded.amount(), common::dec("2.01")); 23 } 24 25 #[test] 26 fn checked_constructor_rejects_zero_per_quantity() { 27 assert_eq!( 28 QuantityPrice::try_new(common::money("1", "USD"), Quantity::zero(Unit::Each)), 29 Err(Error::PerQuantityZero) 30 ); 31 } 32 33 #[test] 34 fn checked_cost_never_silently_converts_failures_to_zero() { 35 let price = unit_price("10", "1", Unit::Each); 36 assert_eq!( 37 price.try_cost_for(&common::qty("1", Unit::MassKg)), 38 Err(Error::UnitMismatch { 39 have: Unit::MassKg, 40 want: Unit::Each, 41 }) 42 ); 43 assert_eq!( 44 price.try_cost_for_amount_in(common::dec("-1"), Unit::Each), 45 Err(Error::NegativeRequestedQuantity) 46 ); 47 assert_eq!( 48 price.try_cost_for_rounded(&common::qty("1", Unit::MassKg)), 49 Err(Error::UnitMismatch { 50 have: Unit::MassKg, 51 want: Unit::Each, 52 }) 53 ); 54 } 55 56 #[test] 57 fn checked_cost_reports_arithmetic_overflow() { 58 let price = QuantityPrice::try_new( 59 Money::try_new(Decimal::MAX, Currency::USD).unwrap(), 60 Quantity::try_new(Decimal::ONE, Unit::Each).unwrap(), 61 ) 62 .unwrap(); 63 assert_eq!( 64 price.try_cost_for(&Quantity::try_new(Decimal::from(2u32), Unit::Each).unwrap()), 65 Err(Error::ArithmeticOverflow) 66 ); 67 } 68 69 #[test] 70 fn amount_and_quantity_conversion_support_mass_and_volume() { 71 let mass = unit_price("10", "1", Unit::MassKg); 72 assert_eq!( 73 mass.try_cost_for_amount_in(common::dec("500"), Unit::MassG) 74 .unwrap() 75 .amount(), 76 common::dec("5") 77 ); 78 assert_eq!( 79 mass.try_cost_for_quantity_in(&common::qty("250", Unit::MassG)) 80 .unwrap() 81 .amount(), 82 common::dec("2.5") 83 ); 84 assert_eq!( 85 mass.try_cost_for_amount_in(common::dec("1"), Unit::Each), 86 Err(Error::NonConvertibleUnits { 87 from: Unit::Each, 88 to: Unit::MassKg, 89 }) 90 ); 91 92 let volume = unit_price("10", "1", Unit::VolumeL); 93 assert_eq!( 94 volume 95 .try_cost_for_amount_in(common::dec("500"), Unit::VolumeMl) 96 .unwrap() 97 .amount(), 98 common::dec("5") 99 ); 100 } 101 102 #[test] 103 fn unit_price_conversion_is_checked_and_uses_accessors() { 104 let base = unit_price("5", "2", Unit::MassKg); 105 let same = base.try_to_unit_price(Unit::MassKg).unwrap(); 106 assert_eq!(same.quantity().unit(), Unit::MassKg); 107 assert_eq!(same.quantity().amount(), Decimal::ONE); 108 assert_eq!(same.amount().amount(), common::dec("2.5")); 109 assert_eq!( 110 base.try_to_unit_price(Unit::VolumeMl), 111 Err(Error::NonConvertibleUnits { 112 from: Unit::MassKg, 113 to: Unit::VolumeMl, 114 }) 115 ); 116 117 let canonical = unit_price("6.99", "1", Unit::MassLb) 118 .try_to_canonical_unit_price() 119 .unwrap(); 120 assert_eq!(canonical.quantity().unit(), Unit::MassG); 121 assert_eq!(canonical.quantity().amount(), Decimal::ONE); 122 assert!(canonical.is_price_per_canonical_unit()); 123 } 124 125 #[test] 126 fn unit_price_conversion_detects_precision_underflow() { 127 let tiny = unit_price("1", "0.0000000000000000000000000001", Unit::VolumeMl); 128 assert_eq!( 129 tiny.try_to_unit_price(Unit::VolumeL), 130 Err(Error::PerQuantityZero) 131 ); 132 } 133 134 #[test] 135 fn pricing_error_messages_are_stable() { 136 assert_eq!( 137 Error::PerQuantityZero.to_string(), 138 "price quantity must be greater than zero" 139 ); 140 assert_eq!( 141 Error::PerQuantityNegative.to_string(), 142 "price quantity must not be negative" 143 ); 144 assert_eq!( 145 Error::NegativePrice.to_string(), 146 "price amount must not be negative" 147 ); 148 assert_eq!( 149 Error::NegativeRequestedQuantity.to_string(), 150 "requested quantity must not be negative" 151 ); 152 assert_eq!( 153 Error::ArithmeticOverflow.to_string(), 154 "price arithmetic overflow" 155 ); 156 }