lib

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

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 }