use std::{fmt, str::FromStr};
use bigdecimal::{BigDecimal, FromPrimitive};
use crate::hundred;
#[derive(PartialEq)]
pub enum Mode {
Percentual,
AmountLine,
AmountUnit,
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Mode::Percentual => write!(f, "Percentual"),
Mode::AmountLine => write!(f, "AmountLine"),
Mode::AmountUnit => write!(f, "AmountUnit"),
}
}
}
impl Mode {
pub fn from_i8(r#type: i8) -> Option<Self> {
if r#type == 0 {
return Some(Self::Percentual);
}
if r#type == 1 {
return Some(Self::AmountLine);
}
if r#type == 2 {
return Some(Self::AmountUnit);
}
None
}
}
#[derive(Debug)]
pub enum DiscountError<S: Into<String>> {
NegativeValue(S),
OverMaxDiscount(S),
InvalidDecimal(S),
InvalidDiscountMode(S),
Other(S),
}
impl<S: Into<String> + Clone> fmt::Display for DiscountError<S> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DiscountError::NegativeValue(info) => write!(
f,
"Negative value Error. <discountable>, <discount> and <quantity> cannot be negative. {}",
info.clone().into(),
),
DiscountError::OverMaxDiscount(info) => write!(f, "Over max discount error. {}", info.clone().into()),
DiscountError::InvalidDecimal(info) => write!(f, "Invalid decimal value error {}", info.clone().into()),
DiscountError::InvalidDiscountMode(info) => write!(f, "Invalid discount Stage value. {}", info.clone().into()),
DiscountError::Other(info) => write!(f, "Unknown error! {}", info.clone().into()),
}
}
}
pub trait Discounter {
fn add_discount_from_f64(
&mut self,
discount: f64,
discount_mode: Mode,
) -> Option<DiscountError<String>>;
fn add_discount_from_str<S: Into<String>>(
&mut self,
discount: S,
discount_mode: Mode,
) -> Option<DiscountError<String>>;
fn add_discount(
&mut self,
discount: BigDecimal,
discount_mode: Mode,
) -> Option<DiscountError<String>>;
fn compute_from_f64(
&self,
unit_value: f64,
qty: f64,
max_discount_allowed: Option<f64>,
) -> Result<(BigDecimal, BigDecimal), DiscountError<String>>;
fn compute(
&self,
unit_value: BigDecimal,
qty: BigDecimal,
max_discount_allowed: Option<BigDecimal>,
) -> Result<(BigDecimal, BigDecimal), DiscountError<String>>;
fn compute_from_str<S: Into<String>>(
&self,
unit_value: S,
qty: S,
max_discount_allowed: Option<S>,
) -> Result<(BigDecimal, BigDecimal), DiscountError<String>>;
fn un_discount(
&self,
discounted: BigDecimal,
qty: BigDecimal,
) -> Result<(BigDecimal, BigDecimal, BigDecimal), DiscountError<String>>;
fn un_discount_from_f64(
&self,
discounted: f64,
qty: f64,
) -> Result<(BigDecimal, BigDecimal, BigDecimal), DiscountError<String>>;
fn un_discount_from_str<S: Into<String>>(
&self,
discounted: S,
qty: S,
) -> Result<(BigDecimal, BigDecimal, BigDecimal), DiscountError<String>>;
fn ratio(&self, discounted: BigDecimal, discount: BigDecimal) -> BigDecimal {
hundred() * &discount / (&discounted + &discount)
}
}
pub struct DiscountComputer {
percentual: BigDecimal,
amount_line: BigDecimal,
amount_unit: BigDecimal,
}
impl DiscountComputer {
pub fn new() -> Self {
Self {
percentual: crate::zero(),
amount_line: crate::zero(),
amount_unit: crate::zero(),
}
}
}
impl Default for DiscountComputer {
fn default() -> Self {
Self::new()
}
}
impl Discounter for DiscountComputer {
fn add_discount_from_f64(
&mut self,
discount: f64,
discount_mode: Mode,
) -> Option<DiscountError<String>> {
if discount < 0.0f64 {
return Some(DiscountError::NegativeValue(format!(
"negative discount {discount}"
)));
}
if discount > 100.0f64 && discount_mode == Mode::Percentual {
return Some(DiscountError::OverMaxDiscount(format!(
"percentual discount over 100%. {discount}"
)));
}
match discount_mode {
Mode::Percentual => {
self.percentual =
&self.percentual + BigDecimal::from_f64(discount).unwrap_or(crate::zero())
}
Mode::AmountLine => {
self.amount_line =
&self.amount_line + BigDecimal::from_f64(discount).unwrap_or(crate::zero())
}
Mode::AmountUnit => {
self.amount_unit =
&self.amount_unit + BigDecimal::from_f64(discount).unwrap_or(crate::zero())
}
}
None
}
fn add_discount_from_str<S: Into<String>>(
&mut self,
discount: S,
discount_mode: Mode,
) -> Option<DiscountError<String>> {
let d = discount.into();
match BigDecimal::from_str(&d) {
Ok(discount) => self.add_discount(discount.clone(), discount_mode),
Err(err) => Some(DiscountError::InvalidDecimal(format!(
"discount {} err {}",
d, err
))),
}
}
fn add_discount(
&mut self,
discount: BigDecimal,
discount_mode: Mode,
) -> Option<DiscountError<String>> {
if discount < crate::zero() {
return Some(DiscountError::NegativeValue(format!(
"negative discount {}",
discount
)));
}
if discount > crate::hundred() && discount_mode == Mode::Percentual {
return Some(DiscountError::OverMaxDiscount(format!(
"percentual discount over 100%. {}",
discount
)));
}
match discount_mode {
Mode::Percentual => self.percentual = &self.percentual + discount,
Mode::AmountLine => self.amount_line = &self.amount_line + discount,
Mode::AmountUnit => self.amount_unit = &self.amount_unit + discount,
}
None
}
fn compute_from_f64(
&self,
unit_value: f64,
qty: f64,
max_discount_allowed: Option<f64>,
) -> Result<(BigDecimal, BigDecimal), DiscountError<String>> {
let unit_value = BigDecimal::from_f64(unit_value).unwrap_or(crate::inverse());
let qty = BigDecimal::from_f64(qty).unwrap_or(crate::inverse());
let max_discount_allowed = BigDecimal::from_f64(max_discount_allowed.unwrap_or(100.0f64))
.unwrap_or(crate::inverse());
self.compute(unit_value, qty, Some(max_discount_allowed))
}
fn compute(
&self,
unit_value: BigDecimal,
qty: BigDecimal,
max_discount_allowed: Option<BigDecimal>,
) -> Result<(BigDecimal, BigDecimal), DiscountError<String>> {
let max_discount_allowed = max_discount_allowed.unwrap_or(crate::hundred());
if max_discount_allowed < crate::zero() {
return Err(DiscountError::NegativeValue(format!(
"negative <max_discount_allowed> {}",
max_discount_allowed
)));
}
if unit_value < crate::zero() {
return Err(DiscountError::NegativeValue(format!(
"negative <unit_value> {}",
unit_value
)));
}
if qty < crate::zero() {
return Err(DiscountError::NegativeValue(format!(
"negative <qty> {}",
qty
)));
}
let discount_value = &unit_value * &qty * &self.percentual / crate::hundred()
+ &self.amount_unit * &qty
+ &self.amount_line;
if discount_value > max_discount_allowed {
return Err(DiscountError::OverMaxDiscount(format!(
" discount_value {} max_discount_allowed {}",
discount_value, max_discount_allowed
)));
}
let percentual_discount = (&unit_value * &qty - &discount_value) / crate::hundred();
if percentual_discount > crate::hundred() {
return Err(DiscountError::OverMaxDiscount(format!(
"percentual_discount {}",
percentual_discount
)));
}
if percentual_discount < crate::zero() {
return Err(DiscountError::NegativeValue(format!(
"percentual_discount {}",
percentual_discount
)));
}
Ok((discount_value, percentual_discount))
}
fn compute_from_str<S: Into<String>>(
&self,
unit_value: S,
qty: S,
max_discount_allowed: Option<S>,
) -> Result<(BigDecimal, BigDecimal), DiscountError<String>> {
match BigDecimal::from_str(&unit_value.into()) {
Ok(unit_value) => match BigDecimal::from_str(&qty.into()) {
Ok(qty) => match max_discount_allowed {
Some(max_discount_allowed) => {
match BigDecimal::from_str(&max_discount_allowed.into()) {
Ok(max_discount_allowed) => {
self.compute(unit_value, qty, Some(max_discount_allowed))
}
Err(err) => Err(DiscountError::InvalidDecimal(format!("{}", err))),
}
}
None => self.compute(unit_value, qty, None),
},
Err(err) => Err(DiscountError::InvalidDecimal(format!("{}", err))),
},
Err(err) => Err(DiscountError::InvalidDecimal(format!("{}", err))),
}
}
fn un_discount(
&self,
discounted: BigDecimal,
qty: BigDecimal,
) -> Result<(BigDecimal, BigDecimal, BigDecimal), DiscountError<String>> {
if discounted < crate::zero() {
return Err(DiscountError::NegativeValue(format!(
"negative <discounted> {}",
discounted
)));
}
if qty < crate::zero() {
return Err(DiscountError::NegativeValue(format!(
"negative <qty> {}",
qty
)));
}
let percentual = if self.percentual > crate::zero() {
self.percentual.clone()
} else {
crate::one()
};
let discountable = (&discounted + &self.amount_line) / &percentual * crate::hundred()
+ &qty * &self.amount_unit;
let percentual_discount = (&discountable - &discounted) * crate::hundred() / &discountable;
Ok((
discountable.clone(),
discountable - &discounted,
percentual_discount,
))
}
fn un_discount_from_f64(
&self,
discounted: f64,
qty: f64,
) -> Result<(BigDecimal, BigDecimal, BigDecimal), DiscountError<String>> {
self.un_discount(
BigDecimal::from_f64(discounted).unwrap_or(crate::inverse()),
BigDecimal::from_f64(qty).unwrap_or(crate::inverse()),
)
}
fn un_discount_from_str<S: Into<String>>(
&self,
discounted: S,
qty: S,
) -> Result<(BigDecimal, BigDecimal, BigDecimal), DiscountError<String>> {
match BigDecimal::from_str(&discounted.into()) {
Ok(discounted) => match BigDecimal::from_str(&qty.into()) {
Ok(qty) => self.un_discount(discounted, qty),
Err(err) => Err(DiscountError::InvalidDecimal(format!("{}", err))),
},
Err(err) => Err(DiscountError::InvalidDecimal(format!("{}", err))),
}
}
fn ratio(&self, discounted: BigDecimal, discount: BigDecimal) -> BigDecimal {
(&discounted - &discount) * crate::hundred() / &discounted
}
}