use decimal_money::{Currency, CurrencyAmount, MoneyError};
use rust_decimal::Decimal;
#[derive(Debug, thiserror::Error)]
pub enum PriceError {
#[error("tax rate must be between 0 and 100, got {0}")]
InvalidTaxRate(Decimal),
#[error(transparent)]
Money(#[from] MoneyError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Price {
pub net: CurrencyAmount,
pub tax_rate: Decimal,
}
impl Price {
pub fn new(net_amount: impl Into<Decimal>, currency: Currency, tax_rate: Decimal) -> Result<Self, PriceError> {
if tax_rate < Decimal::ZERO || tax_rate > Decimal::from(100) {
return Err(PriceError::InvalidTaxRate(tax_rate));
}
Ok(Self {
net: CurrencyAmount::new(net_amount, currency),
tax_rate,
})
}
#[must_use]
pub fn tax_amount(&self) -> CurrencyAmount {
let tax = self.net.amount * self.tax_rate / Decimal::from(100);
CurrencyAmount::new(tax, self.net.currency)
}
#[must_use]
pub fn gross(&self) -> CurrencyAmount {
(self.net.clone() + self.tax_amount()).expect("same currency")
}
#[must_use]
pub fn discount_of_gross(&self, percent: Decimal) -> CurrencyAmount {
let gross = self.gross();
CurrencyAmount::new(gross.amount * percent / Decimal::from(100), gross.currency)
}
#[must_use]
pub fn gross_minus_discount(&self, percent: Decimal) -> CurrencyAmount {
let gross = self.gross();
let pct = percent.clamp(Decimal::ZERO, Decimal::from(100));
let discount = gross.amount * pct / Decimal::from(100);
let discounted = (gross.amount - discount).max(Decimal::ZERO);
CurrencyAmount::new(discounted, gross.currency)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn vat_20() {
let p = Price::new(dec!(100), Currency::GBP, dec!(20)).unwrap();
assert_eq!(p.tax_amount().amount, dec!(20));
assert_eq!(p.gross().amount, dec!(120));
}
#[test]
fn zero_tax() {
let p = Price::new(dec!(50), Currency::GBP, dec!(0)).unwrap();
assert_eq!(p.gross().amount, dec!(50));
}
#[test]
fn invalid_tax_rejected() {
assert!(Price::new(dec!(10), Currency::GBP, dec!(101)).is_err());
assert!(Price::new(dec!(10), Currency::GBP, dec!(-1)).is_err());
}
#[test]
fn discount_of_gross() {
let p = Price::new(dec!(100), Currency::GBP, dec!(0)).unwrap();
assert_eq!(p.discount_of_gross(dec!(10)).amount, dec!(10));
}
#[test]
fn gross_minus_discount_clamps() {
let p = Price::new(dec!(100), Currency::GBP, dec!(0)).unwrap();
assert_eq!(p.gross_minus_discount(dec!(100)).amount, dec!(0));
assert_eq!(p.gross_minus_discount(dec!(200)).amount, dec!(0));
}
}