price_ext.rs (7794B)
1 use crate::listing::model::{RadrootsTradeListingSubtotal, RadrootsTradeListingTotal}; 2 use radroots_core::{ 3 RadrootsCoreDecimal, RadrootsCoreQuantity, RadrootsCoreQuantityPriceError, 4 RadrootsCoreQuantityPriceOps, 5 }; 6 use radroots_events::listing::RadrootsListingBin; 7 8 pub trait BinPricingExt { 9 fn subtotal_for_count(&self, bin_count: u32) -> RadrootsTradeListingSubtotal; 10 fn total_for_count(&self, bin_count: u32) -> RadrootsTradeListingTotal; 11 } 12 13 pub trait BinPricingTryExt { 14 fn try_subtotal_for_count( 15 &self, 16 bin_count: u32, 17 ) -> Result<RadrootsTradeListingSubtotal, RadrootsCoreQuantityPriceError>; 18 fn try_total_for_count( 19 &self, 20 bin_count: u32, 21 ) -> Result<RadrootsTradeListingTotal, RadrootsCoreQuantityPriceError>; 22 } 23 24 #[inline] 25 fn effective_quantity(bin: &RadrootsListingBin, bin_count: u32) -> RadrootsCoreQuantity { 26 let amount = bin.quantity.amount * RadrootsCoreDecimal::from(bin_count); 27 RadrootsCoreQuantity::new(amount, bin.quantity.unit) 28 } 29 30 impl BinPricingExt for RadrootsListingBin { 31 fn subtotal_for_count(&self, bin_count: u32) -> RadrootsTradeListingSubtotal { 32 let effective_qty = effective_quantity(self, bin_count); 33 let money = self 34 .price_per_canonical_unit 35 .cost_for_rounded(&effective_qty); 36 let currency = money.currency; 37 38 RadrootsTradeListingSubtotal { 39 price_amount: money, 40 price_currency: currency, 41 quantity_amount: effective_qty.amount, 42 quantity_unit: effective_qty.unit, 43 } 44 } 45 46 fn total_for_count(&self, bin_count: u32) -> RadrootsTradeListingTotal { 47 let sub = self.subtotal_for_count(bin_count); 48 RadrootsTradeListingTotal { 49 price_amount: sub.price_amount, 50 price_currency: sub.price_currency, 51 quantity_amount: sub.quantity_amount, 52 quantity_unit: sub.quantity_unit, 53 } 54 } 55 } 56 57 impl BinPricingTryExt for RadrootsListingBin { 58 fn try_subtotal_for_count( 59 &self, 60 bin_count: u32, 61 ) -> Result<RadrootsTradeListingSubtotal, RadrootsCoreQuantityPriceError> { 62 let effective_qty = effective_quantity(self, bin_count); 63 let money = self 64 .price_per_canonical_unit 65 .try_cost_for_rounded(&effective_qty)?; 66 let currency = money.currency; 67 68 Ok(RadrootsTradeListingSubtotal { 69 price_amount: money, 70 price_currency: currency, 71 quantity_amount: effective_qty.amount, 72 quantity_unit: effective_qty.unit, 73 }) 74 } 75 76 fn try_total_for_count( 77 &self, 78 bin_count: u32, 79 ) -> Result<RadrootsTradeListingTotal, RadrootsCoreQuantityPriceError> { 80 let sub = self.try_subtotal_for_count(bin_count)?; 81 Ok(RadrootsTradeListingTotal { 82 price_amount: sub.price_amount, 83 price_currency: sub.price_currency, 84 quantity_amount: sub.quantity_amount, 85 quantity_unit: sub.quantity_unit, 86 }) 87 } 88 } 89 90 #[cfg(test)] 91 mod tests { 92 use super::{BinPricingExt, BinPricingTryExt}; 93 use radroots_core::{ 94 RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, 95 RadrootsCoreQuantityPrice, RadrootsCoreQuantityPriceError, RadrootsCoreUnit, 96 }; 97 use radroots_events::ids::RadrootsInventoryBinId; 98 use radroots_events::listing::RadrootsListingBin; 99 100 fn bin_id(raw: &str) -> RadrootsInventoryBinId { 101 RadrootsInventoryBinId::parse(raw).expect("bin id") 102 } 103 104 fn valid_bin() -> RadrootsListingBin { 105 RadrootsListingBin { 106 bin_id: bin_id("bin-1"), 107 quantity: RadrootsCoreQuantity::new( 108 RadrootsCoreDecimal::from(2u32), 109 RadrootsCoreUnit::MassG, 110 ), 111 price_per_canonical_unit: RadrootsCoreQuantityPrice::new( 112 RadrootsCoreMoney::new(RadrootsCoreDecimal::from(5u32), RadrootsCoreCurrency::USD), 113 RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::MassG), 114 ), 115 display_amount: None, 116 display_unit: None, 117 display_label: None, 118 display_price: None, 119 display_price_unit: None, 120 } 121 } 122 123 #[test] 124 fn try_subtotal_for_rejects_unit_mismatch() { 125 let bin = RadrootsListingBin { 126 bin_id: bin_id("bin-1"), 127 quantity: RadrootsCoreQuantity::new( 128 RadrootsCoreDecimal::from(1u32), 129 RadrootsCoreUnit::MassG, 130 ), 131 price_per_canonical_unit: RadrootsCoreQuantityPrice::new( 132 RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD), 133 RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each), 134 ), 135 display_amount: None, 136 display_unit: None, 137 display_label: None, 138 display_price: None, 139 display_price_unit: None, 140 }; 141 142 let err = bin.try_subtotal_for_count(1).unwrap_err(); 143 assert_eq!( 144 err, 145 RadrootsCoreQuantityPriceError::UnitMismatch { 146 have: RadrootsCoreUnit::MassG, 147 want: RadrootsCoreUnit::Each, 148 } 149 ); 150 } 151 152 #[test] 153 fn subtotal_and_total_for_count_follow_effective_quantity() { 154 let bin = valid_bin(); 155 let subtotal = bin.subtotal_for_count(3); 156 let total = bin.total_for_count(3); 157 158 assert_eq!(subtotal.quantity_amount, RadrootsCoreDecimal::from(6u32)); 159 assert_eq!(subtotal.quantity_unit, RadrootsCoreUnit::MassG); 160 assert_eq!( 161 subtotal.price_amount.amount, 162 RadrootsCoreDecimal::from(30u32) 163 ); 164 assert_eq!(subtotal.price_currency, RadrootsCoreCurrency::USD); 165 166 assert_eq!(total.quantity_amount, subtotal.quantity_amount); 167 assert_eq!(total.quantity_unit, subtotal.quantity_unit); 168 assert_eq!(total.price_amount, subtotal.price_amount); 169 assert_eq!(total.price_currency, subtotal.price_currency); 170 } 171 172 #[test] 173 fn try_subtotal_and_try_total_match_non_fallible_paths() { 174 let bin = valid_bin(); 175 let subtotal = bin.try_subtotal_for_count(4).expect("subtotal"); 176 let total = bin.try_total_for_count(4).expect("total"); 177 178 assert_eq!(subtotal.quantity_amount, RadrootsCoreDecimal::from(8u32)); 179 assert_eq!( 180 subtotal.price_amount.amount, 181 RadrootsCoreDecimal::from(40u32) 182 ); 183 assert_eq!(total.quantity_amount, subtotal.quantity_amount); 184 assert_eq!(total.price_amount, subtotal.price_amount); 185 } 186 187 #[test] 188 fn try_total_for_count_propagates_subtotal_errors() { 189 let bin = RadrootsListingBin { 190 bin_id: bin_id("bin-1"), 191 quantity: RadrootsCoreQuantity::new( 192 RadrootsCoreDecimal::from(1u32), 193 RadrootsCoreUnit::MassG, 194 ), 195 price_per_canonical_unit: RadrootsCoreQuantityPrice::new( 196 RadrootsCoreMoney::new(RadrootsCoreDecimal::from(10u32), RadrootsCoreCurrency::USD), 197 RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each), 198 ), 199 display_amount: None, 200 display_unit: None, 201 display_label: None, 202 display_price: None, 203 display_price_unit: None, 204 }; 205 206 let err = bin.try_total_for_count(1).unwrap_err(); 207 assert_eq!( 208 err, 209 RadrootsCoreQuantityPriceError::UnitMismatch { 210 have: RadrootsCoreUnit::MassG, 211 want: RadrootsCoreUnit::Each, 212 } 213 ); 214 } 215 }