lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }