lib

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

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 }