price_ext.rs (6210B)
1 use crate::listing::operational_listing::model::{ 2 RadrootsOperationalListingSubtotal, RadrootsOperationalListingTotal, 3 }; 4 use radroots_core::pricing::{Error as PricingError, QuantityPriceOps}; 5 use radroots_core::{Decimal, Quantity}; 6 use radroots_event::listing::operational::OperationalListingBin; 7 8 pub trait BinPricingTryExt { 9 fn try_subtotal_for_count( 10 &self, 11 bin_count: u32, 12 ) -> Result<RadrootsOperationalListingSubtotal, PricingError>; 13 fn try_total_for_count( 14 &self, 15 bin_count: u32, 16 ) -> Result<RadrootsOperationalListingTotal, PricingError>; 17 } 18 19 #[inline] 20 fn effective_quantity( 21 bin: &OperationalListingBin, 22 bin_count: u32, 23 ) -> Result<Quantity, PricingError> { 24 let amount = bin 25 .quantity 26 .amount() 27 .checked_mul(Decimal::from(bin_count)) 28 .map_err(|_| PricingError::ArithmeticOverflow)?; 29 Quantity::try_new(amount, bin.quantity.unit()).map_err(|_| PricingError::ArithmeticOverflow) 30 } 31 32 impl BinPricingTryExt for OperationalListingBin { 33 fn try_subtotal_for_count( 34 &self, 35 bin_count: u32, 36 ) -> Result<RadrootsOperationalListingSubtotal, PricingError> { 37 let effective_qty = effective_quantity(self, bin_count)?; 38 let money = self 39 .price_per_canonical_unit 40 .try_cost_for_rounded(&effective_qty)?; 41 let currency = money.currency(); 42 43 Ok(RadrootsOperationalListingSubtotal { 44 price_amount: money, 45 price_currency: currency, 46 quantity_amount: effective_qty.amount(), 47 quantity_unit: effective_qty.unit(), 48 }) 49 } 50 51 fn try_total_for_count( 52 &self, 53 bin_count: u32, 54 ) -> Result<RadrootsOperationalListingTotal, PricingError> { 55 let sub = self.try_subtotal_for_count(bin_count)?; 56 Ok(RadrootsOperationalListingTotal { 57 price_amount: sub.price_amount, 58 price_currency: sub.price_currency, 59 quantity_amount: sub.quantity_amount, 60 quantity_unit: sub.quantity_unit, 61 }) 62 } 63 } 64 65 #[cfg(test)] 66 mod tests { 67 use super::BinPricingTryExt; 68 use radroots_core::pricing::Error as PricingError; 69 use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; 70 use radroots_event::id::InventoryBinId; 71 use radroots_event::listing::operational::OperationalListingBin; 72 73 fn bin_id(raw: &str) -> InventoryBinId { 74 InventoryBinId::parse(raw).expect("bin id") 75 } 76 77 fn valid_bin() -> OperationalListingBin { 78 OperationalListingBin { 79 bin_id: bin_id("bin-1"), 80 quantity: Quantity::try_new(Decimal::from(2u32), Unit::MassG).unwrap(), 81 price_per_canonical_unit: QuantityPrice::try_new( 82 Money::try_new(Decimal::from(5u32), Currency::USD).unwrap(), 83 Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), 84 ) 85 .unwrap(), 86 display_amount: None, 87 display_unit: None, 88 display_label: None, 89 display_price: None, 90 display_price_unit: None, 91 } 92 } 93 94 #[test] 95 fn try_subtotal_for_rejects_unit_mismatch() { 96 let bin = OperationalListingBin { 97 bin_id: bin_id("bin-1"), 98 quantity: Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), 99 price_per_canonical_unit: QuantityPrice::try_new( 100 Money::try_new(Decimal::from(10u32), Currency::USD).unwrap(), 101 Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(), 102 ) 103 .unwrap(), 104 display_amount: None, 105 display_unit: None, 106 display_label: None, 107 display_price: None, 108 display_price_unit: None, 109 }; 110 111 let err = bin.try_subtotal_for_count(1).unwrap_err(); 112 assert_eq!( 113 err, 114 PricingError::UnitMismatch { 115 have: Unit::MassG, 116 want: Unit::Each, 117 } 118 ); 119 } 120 121 #[test] 122 fn subtotal_and_total_for_count_follow_effective_quantity() { 123 let bin = valid_bin(); 124 let subtotal = bin.try_subtotal_for_count(3).unwrap(); 125 let total = bin.try_total_for_count(3).unwrap(); 126 127 assert_eq!(subtotal.quantity_amount, Decimal::from(6u32)); 128 assert_eq!(subtotal.quantity_unit, Unit::MassG); 129 assert_eq!(subtotal.price_amount.amount(), Decimal::from(30u32)); 130 assert_eq!(subtotal.price_currency, Currency::USD); 131 132 assert_eq!(total.quantity_amount, subtotal.quantity_amount); 133 assert_eq!(total.quantity_unit, subtotal.quantity_unit); 134 assert_eq!(total.price_amount, subtotal.price_amount); 135 assert_eq!(total.price_currency, subtotal.price_currency); 136 } 137 138 #[test] 139 fn try_subtotal_and_try_total_match() { 140 let bin = valid_bin(); 141 let subtotal = bin.try_subtotal_for_count(4).expect("subtotal"); 142 let total = bin.try_total_for_count(4).expect("total"); 143 144 assert_eq!(subtotal.quantity_amount, Decimal::from(8u32)); 145 assert_eq!(subtotal.price_amount.amount(), Decimal::from(40u32)); 146 assert_eq!(total.quantity_amount, subtotal.quantity_amount); 147 assert_eq!(total.price_amount, subtotal.price_amount); 148 } 149 150 #[test] 151 fn try_total_for_count_propagates_subtotal_errors() { 152 let bin = OperationalListingBin { 153 bin_id: bin_id("bin-1"), 154 quantity: Quantity::try_new(Decimal::from(1u32), Unit::MassG).unwrap(), 155 price_per_canonical_unit: QuantityPrice::try_new( 156 Money::try_new(Decimal::from(10u32), Currency::USD).unwrap(), 157 Quantity::try_new(Decimal::from(1u32), Unit::Each).unwrap(), 158 ) 159 .unwrap(), 160 display_amount: None, 161 display_unit: None, 162 display_label: None, 163 display_price: None, 164 display_price_unit: None, 165 }; 166 167 let err = bin.try_total_for_count(1).unwrap_err(); 168 assert_eq!( 169 err, 170 PricingError::UnitMismatch { 171 have: Unit::MassG, 172 want: Unit::Each, 173 } 174 ); 175 } 176 }