use async_trait::async_trait;
use rust_decimal::{prelude::FromPrimitive, Decimal};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use crate::checkout::Checkout;
use crate::error::Error;
use crate::money::Money;
use crate::shipping::Fulfillment;
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Tax {
Percentage { title: String, value: Decimal },
Absolute { title: String, value: Money },
}
impl Tax {
pub fn new_percentage(title: &str, value: &str) -> Self {
Self::Percentage {
title: title.to_string(),
value: Decimal::from_str(value).unwrap(),
}
}
pub fn new_absolute(title: &str, value: Money) -> Self {
Self::Absolute {
title: title.to_string(),
value,
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct LineItem {
pub title: String,
pub quantity: u64,
pub price: Money,
pub discount: Money,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct Invoice {
pub line_items: Vec<LineItem>,
pub shipping: Option<LineItem>,
pub line_item_taxes: Vec<Tax>,
pub shipping_taxes: Vec<Tax>,
pub subtotal: Money,
pub shipping_total: Money,
pub total_line_item_taxes: Money,
pub total_shipping_taxes: Money,
pub total_taxes: Money,
pub total: Money,
pub initial_charge_amount: Money,
}
#[async_trait]
pub trait InvoiceCalculator {
async fn line_item_taxes<P: Sync + Send>(
&mut self,
_co: &Checkout<P>,
) -> Result<Vec<Tax>, Error> {
Ok(vec![])
}
async fn shipping_taxes<P: Sync + Send>(
&mut self,
_co: &Checkout<P>,
) -> Result<Vec<Tax>, Error> {
Ok(vec![])
}
async fn initial_charge_ratio<P: Sync + Send>(
&mut self,
_co: &Checkout<P>,
) -> Result<Decimal, Error> {
Ok(Decimal::from_str("1").unwrap())
}
async fn generate_invoice<P: Sync + Send>(
&mut self,
co: &Checkout<P>,
) -> Result<Invoice, Error> {
let line_items: Vec<LineItem> = co
.items
.iter()
.map(|i| LineItem {
title: i.title.clone(),
price: i.price.clone(),
quantity: i.quantity.clone(),
discount: i.discount.clone(),
})
.collect();
let shipping = match &co.fulfillment {
Some(ft) => match ft {
Fulfillment::Pickup => None,
Fulfillment::Shipping { quote } => Some(LineItem {
title: String::from("Shipping"),
quantity: 1,
price: quote.price.clone(),
discount: quote.discount.clone(),
}),
},
None => None,
};
let subtotal = line_items
.iter()
.fold(Money::new(co.currency.clone(), "0"), |t, li| {
t.add(
&li.price
.sub(&li.discount)
.mul(&Decimal::from_u64(li.quantity).unwrap()),
)
});
let shipping_total = match &shipping {
Some(s) => s.price.mul(&Decimal::from_u64(s.quantity).unwrap()),
None => Money::new(co.currency.clone(), "0"),
};
let line_item_taxes = self.line_item_taxes(co).await?;
let total_line_item_taxes = line_item_taxes.iter().fold(
Money::new(co.currency.clone(), "0"),
|t, tax| match &tax {
Tax::Absolute { title: _, value } => t.add(value),
Tax::Percentage { title: _, value } => t.add(&subtotal.mul(value)),
},
);
let shipping_taxes = self.shipping_taxes(co).await?;
let total_shipping_taxes =
shipping_taxes
.iter()
.fold(Money::new(co.currency.clone(), "0"), |t, tax| match &tax {
Tax::Absolute { title: _, value } => t.add(value),
Tax::Percentage { title: _, value } => t.add(&shipping_total.mul(value)),
});
let total_taxes = total_line_item_taxes.add(&total_shipping_taxes);
let total = subtotal.add(&shipping_total).add(&total_taxes);
let initial_charge_amount = total.mul(&self.initial_charge_ratio(co).await?);
Ok(Invoice {
line_items,
shipping,
line_item_taxes,
shipping_taxes,
subtotal,
shipping_total,
total_line_item_taxes,
total_shipping_taxes,
total_taxes,
total,
initial_charge_amount,
})
}
}