use crate::error::Error;
use crate::inventory::InventoryController;
use crate::item::ItemTitleResolver;
use crate::money::{Currency, Money};
use address::Address;
use async_trait::async_trait;
pub use chrono::{DateTime, Utc};
use inventory::{ReserveResult, StockIssue};
use invoice::{Invoice, InvoiceCalculator};
use item::Item;
use price::PriceCalculator;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use shipping::{FulfillmentType, FulfillmentTypeSelection, ShippingCalculator, ShippingQuote};
use std::matches;
use uuid::Uuid;
pub mod address;
pub mod error;
pub mod inventory;
pub mod invoice;
pub mod item;
pub mod money;
pub mod price;
pub mod server;
pub mod shipping;
#[derive(Serialize, Deserialize, JsonSchema)]
pub enum State {
Shopping,
ItemsConfirmed(DateTime<Utc>),
Completed,
Abandoned,
}
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Checkout {
pub id: Uuid,
pub state: State,
pub currency: Currency,
pub promo_codes: Vec<String>,
pub items: Vec<Item>,
pub stock_issues: Vec<StockIssue>,
pub fulfillment_type: Option<FulfillmentType>,
pub shipping_address: Option<Address>,
pub shipping_quotes: Vec<ShippingQuote>,
pub invoice: Option<Invoice>,
}
impl Checkout {
async fn enter_shopping_state<C: CheckoutContext + Send>(
&mut self,
ctx: &mut C,
) -> Result<(), CheckoutError> {
match self.state {
State::Shopping => Ok(()),
State::ItemsConfirmed(_) => {
ctx.free_items(&self.items).await?;
self.fulfillment_type = None;
self.shipping_quotes = vec![];
Ok(self.state = State::Shopping)
}
_ => Err(new_invalid_state_error(
"must be in the shopping or items_confirmed state",
)),
}
}
async fn update_invoice<C: CheckoutContext + Send>(
&mut self,
ctx: &mut C,
) -> Result<(), CheckoutError> {
Ok(self.invoice = Some(ctx.generate_invoice(self).await?))
}
pub async fn create<C: CheckoutContext + Send>(
ctx: &mut C,
currency: Currency,
) -> Result<Self, CheckoutError> {
let checkout = Checkout {
id: Uuid::new_v4(),
state: State::Shopping,
currency,
promo_codes: vec![],
items: vec![],
stock_issues: vec![],
fulfillment_type: None,
shipping_address: None,
shipping_quotes: vec![],
invoice: None,
};
ctx.on_create(&checkout).await?;
Ok(checkout)
}
pub async fn add_item<C: CheckoutContext + Send>(
&mut self,
ctx: &mut C,
sku: String,
quantity: u64,
) -> Result<(), CheckoutError> {
self.enter_shopping_state(ctx).await?;
let mut is_added = false;
for item in self.items.iter_mut() {
if item.sku == sku {
item.quantity += quantity;
is_added = true;
break;
}
}
if !is_added {
let title = ctx.resolve_item_title(&sku).await?;
let item = Item {
sku: sku.to_string(),
title,
quantity,
price: Money::new(self.currency.clone(), "0"),
discount: Money::new(self.currency.clone(), "0"),
};
self.items.push(item);
}
ctx.update_item_prices(&self.currency, &self.promo_codes, &mut self.items)
.await?;
ctx.on_add_item(self, &sku, quantity).await
}
pub async fn remove_item<C: CheckoutContext + Send>(
&mut self,
ctx: &mut C,
sku: String,
quantity: u64,
) -> Result<(), CheckoutError> {
self.enter_shopping_state(ctx).await?;
let mut index_to_remove: Option<usize> = None;
for (index, item) in self.items.iter_mut().enumerate() {
if item.sku == sku {
if item.quantity <= quantity {
index_to_remove = Some(index);
break;
}
item.quantity -= quantity;
break;
}
}
if let Some(index) = index_to_remove {
self.items.remove(index);
}
ctx.update_item_prices(&self.currency, &self.promo_codes, &mut self.items)
.await?;
ctx.on_remove_item(self, &sku, quantity).await
}
pub async fn confirm_items<C: CheckoutContext + Send>(
&mut self,
ctx: &mut C,
) -> Result<(), CheckoutError> {
match self.state {
State::ItemsConfirmed(_) => Ok(()),
State::Shopping => {
match ctx.reserve_items(&self.items).await? {
ReserveResult::Success(deadline) => {
self.fulfillment_type = None;
if let Some(address) = &self.shipping_address {
self.shipping_quotes = ctx
.get_shipping_quotes(
&self.currency,
&self.promo_codes,
&self.items,
address,
)
.await?;
}
ctx.update_item_prices(&self.currency, &self.promo_codes, &mut self.items)
.await?;
self.update_invoice(ctx).await?;
self.state = State::ItemsConfirmed(deadline);
self.stock_issues = vec![];
ctx.on_confirm_items(self).await?;
}
ReserveResult::StockIssue(stock_issues) => {
self.stock_issues = stock_issues;
}
}
Ok(())
}
_ => Err(new_invalid_state_error(
"must be in the shopping or items_confirmed state",
)),
}
}
pub async fn update_shipping_address<C: CheckoutContext + Send>(
&mut self,
ctx: &mut C,
address: Address,
) -> Result<(), CheckoutError> {
self.shipping_address = Some(address);
self.fulfillment_type = None;
if let State::ItemsConfirmed(_) = self.state {
self.shipping_quotes = ctx
.get_shipping_quotes(
&self.currency,
&self.promo_codes,
&self.items,
self.shipping_address.as_ref().unwrap(),
)
.await?;
}
ctx.on_update_shipping_address(self).await
}
pub async fn update_fulfillment_type<C: CheckoutContext + Send>(
&mut self,
ctx: &mut C,
fulfillment_type: FulfillmentTypeSelection,
) -> Result<(), CheckoutError> {
if !matches!(self.state, State::ItemsConfirmed(_)) {
return Err(new_invalid_state_error(
"must be in the items_confirmed state",
));
}
match fulfillment_type {
FulfillmentTypeSelection::Pickup => {
self.fulfillment_type = Some(FulfillmentType::Pickup);
}
FulfillmentTypeSelection::Shipping(quote_id) => {
let mut updated = false;
for quote in self.shipping_quotes.iter() {
if quote.id == quote_id {
self.fulfillment_type = Some(FulfillmentType::Shipping(quote.clone()));
updated = true;
break;
}
}
if !updated {
return Err(new_bad_request_error(
"the requested quote is not available",
));
}
}
}
self.update_invoice(ctx).await?;
ctx.on_update_fulfillment_type(self).await
}
}
#[async_trait]
pub trait CheckoutContext:
InvoiceCalculator + ItemTitleResolver + InventoryController + ShippingCalculator + PriceCalculator
{
async fn start_transaction(&mut self) -> Result<(), CheckoutError> {
Ok(())
}
async fn commit_transaction(&mut self) -> Result<(), CheckoutError> {
Ok(())
}
async fn abort_transaction(&mut self) -> Result<(), CheckoutError> {
Ok(())
}
async fn on_create(&mut self, _co: &Checkout) -> Result<(), CheckoutError> {
Ok(())
}
async fn on_add_item(
&mut self,
_co: &Checkout,
_sku: &str,
_quantity: u64,
) -> Result<(), CheckoutError> {
Ok(())
}
async fn on_remove_item(
&mut self,
_co: &Checkout,
_sku: &str,
_quantity: u64,
) -> Result<(), CheckoutError> {
Ok(())
}
async fn on_confirm_items(&mut self, _co: &Checkout) -> Result<(), CheckoutError> {
Ok(())
}
async fn on_update_shipping_address(&mut self, _co: &Checkout) -> Result<(), CheckoutError> {
Ok(())
}
async fn on_update_fulfillment_type(&mut self, _co: &Checkout) -> Result<(), CheckoutError> {
Ok(())
}
}
#[async_trait]
pub trait CheckoutContextProvider {
type Context: CheckoutContext;
async fn new_context(&self) -> Result<Self::Context, CheckoutError>;
}
#[derive(Debug, Serialize, Deserialize)]
pub enum CheckoutError {
InvalidState(Error),
BadRequest(Error),
}
fn new_invalid_state_error(msg: &str) -> CheckoutError {
CheckoutError::InvalidState(Error {
code: String::from("300"),
message: msg.to_string(),
})
}
fn new_bad_request_error(msg: &str) -> CheckoutError {
CheckoutError::BadRequest(Error {
code: String::from("400"),
message: msg.to_string(),
})
}