use async_trait::async_trait;
use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::context::Context;
use crate::customer::{Address, Contact};
use crate::error::{new_application_error, new_invalid_state_error, Error};
use crate::internal_context::InternalContext;
use crate::invoice::Invoice;
use crate::item::Item;
use crate::money::{Currency, Money};
use crate::order::Order;
use crate::payment::handlers as payment_handlers;
use crate::payment::{Initiator, PaymentProcessor};
use crate::shipping::{Fulfillment, FulfillmentSelection, ShippingQuote};
pub mod handlers;
#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum State {
Shopping, ItemsConfirmed { reserved_since: DateTime<Utc> }, PaymentInProgress { reserved_since: DateTime<Utc> }, Completed, }
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Checkout<P> {
pub id: String,
pub state: State,
pub currency: Currency,
pub promo_codes: Vec<String>,
pub items: Vec<Item>,
pub contact: Option<Contact>,
pub shipping_address: Option<Address>,
pub fulfillment: Option<Fulfillment>,
pub shipping_quotes: Vec<ShippingQuote>,
pub invoice: Option<Invoice>,
pub payment_id: Option<String>,
pub order_id: Option<String>,
pub payment: Option<P>,
pub order: Option<Order<P>>,
}
#[async_trait]
pub trait CheckoutStore {
async fn create_checkout<P: Sync + Send + Serialize + DeserializeOwned>(
&mut self,
co: &Checkout<P>,
) -> Result<(), Error>;
async fn update_checkout<P: Sync + Send + Serialize + DeserializeOwned>(
&mut self,
co: &Checkout<P>,
) -> Result<(), Error>;
async fn get_checkout<P: Sync + Send + Serialize + DeserializeOwned>(
&mut self,
id: &str,
) -> Result<Option<Checkout<P>>, Error>;
async fn get_checkout_for_update<P: Sync + Send + Serialize + DeserializeOwned>(
&mut self,
id: &str,
) -> Result<Option<Checkout<P>>, Error>;
}
impl<P: Sync + Send + Serialize + DeserializeOwned> Checkout<P> {
pub async fn new<C: Context + Send>(
ctx: &mut C,
currency: Currency,
contact: Option<Contact>,
shipping_address: Option<Address>,
) -> Result<Self, Error> {
let co = Self {
id: Uuid::new_v4().to_string(),
state: State::Shopping,
currency,
promo_codes: vec![],
items: vec![],
contact,
shipping_address,
fulfillment: None,
shipping_quotes: vec![],
invoice: None,
payment_id: None,
order_id: None,
payment: None,
order: None,
};
ctx.create_checkout(&co).await?;
Ok(co)
}
}
impl<P: Sync + Send + Serialize + DeserializeOwned> Checkout<P> {
async fn enter_shopping_state<C: Context + Send>(&mut self, ctx: &mut C) -> Result<(), Error> {
self.reset_shipping_options();
ctx.free_items(&self.items).await?;
Ok(self.state = State::Shopping)
}
async fn enter_items_confirmed_state<C: Context + Send>(
&mut self,
ctx: &mut C,
) -> Result<(), Error> {
self.reset_shipping_options();
match ctx.reserve_items(&self.items).await? {
None => {
if let Some(address) = &self.shipping_address {
self.shipping_quotes = ctx
.get_shipping_quotes(
&self.currency,
&self.promo_codes,
&self.items,
address,
)
.await?;
}
self.update_item_prices(ctx).await?;
self.update_invoice(ctx).await?;
self.state = State::ItemsConfirmed {
reserved_since: Utc::now(),
};
Ok(())
}
Some(stock_issues) => Err(new_application_error("STOCK_ISSUE", stock_issues)),
}
}
async fn enter_payment_in_progress_state<C: Context + Send>(
&mut self,
ctx: &mut C,
args: <C as PaymentProcessor>::InitArgs,
reserved_since: DateTime<Utc>,
) -> Result<(), Error> {
let charge_amount = self.invoice.as_ref().unwrap().initial_charge_amount.clone();
let payment =
payment_handlers::create_handler(ctx, Initiator::Checkout(self), charge_amount, args)
.await?;
self.payment_id = Some(payment.id.clone());
self.state = State::PaymentInProgress { reserved_since };
Ok(())
}
async fn enter_completed_state(&mut self) -> Result<(), Error> {
Ok(self.state = State::Completed)
}
}
impl<P: Sync + Send + Serialize + DeserializeOwned> Checkout<P> {
async fn ensure_shopping_state<C: Context + Send>(&mut self, ctx: &mut C) -> Result<(), Error> {
match self.state {
State::Shopping => Ok(()),
State::ItemsConfirmed { reserved_since: _ } => self.enter_shopping_state(ctx).await,
_ => Err(new_invalid_state_error(
"must be in the Shopping or ItemsConfirmed state",
)),
}
}
async fn ensure_items_confirmed_state<C: Context + Send>(
&mut self,
ctx: &mut C,
) -> Result<(), Error> {
match self.state {
State::ItemsConfirmed { reserved_since: _ } => Ok(()),
State::Shopping => self.enter_items_confirmed_state(ctx).await,
_ => Err(new_invalid_state_error(
"must be in the Shopping or ItemsConfirmed state",
)),
}
}
async fn ensure_payment_in_progress_state<C: Context + Send>(
&mut self,
ctx: &mut C,
args: <C as PaymentProcessor>::InitArgs,
) -> Result<(), Error> {
match self.state {
State::PaymentInProgress { reserved_since: _ } => Ok(()),
State::ItemsConfirmed { reserved_since } => {
if self.contact.is_none() {
return Err(new_application_error(
"MISSING_CONTACT_INFORMATION",
"please provide contact information to continue",
));
}
if self.fulfillment.is_none() {
return Err(new_application_error(
"MISSING_FULFILLMENT_TYPE",
"please select a fulfillment type to continue",
));
}
self.enter_payment_in_progress_state(ctx, args, reserved_since)
.await
}
_ => Err(new_invalid_state_error(
"must be in the ItemsConfirmed or PaymentInProgress state",
)),
}
}
async fn ensure_completed_state(&mut self) -> Result<(), Error> {
match self.state {
State::Completed => Ok(()),
State::ItemsConfirmed { reserved_since: _ } => {
if self.contact.is_none() {
return Err(new_application_error(
"MISSING_CONTACT_INFORMATION",
"please provide contact information to continue",
));
}
if self.fulfillment.is_none() {
return Err(new_application_error(
"MISSING_FULFILLMENT_TYPE",
"please select a fulfillment type to continue",
));
}
self.enter_completed_state().await
}
State::PaymentInProgress { reserved_since: _ } => self.enter_completed_state().await,
_ => Err(new_invalid_state_error(
"must be in the PaymentInProgress or Completed state",
)),
}
}
async fn ensure_not_payment_in_progress_or_completed_state(&mut self) -> Result<(), Error> {
match self.state {
State::PaymentInProgress { reserved_since: _ } | State::Completed => Err(
new_invalid_state_error("should be in any state other than PaymentInProgress or Completed"),
),
_ => Ok(()),
}
}
async fn update_invoice<C: Context + Send>(&mut self, ctx: &mut C) -> Result<(), Error> {
Ok(self.invoice = Some(ctx.generate_invoice(self).await?))
}
async fn update_item_prices<C: Context + Send>(&mut self, ctx: &mut C) -> Result<(), Error> {
let item_prices = ctx
.calculate_item_prices(&self.currency, &self.promo_codes, &self.items)
.await?;
for item_price in item_prices.iter() {
for item in self.items.iter_mut() {
if item.sku == item_price.sku {
item.price = item_price.price.clone();
item.discount = item_price.discount.clone();
break;
}
}
}
Ok(())
}
fn reset_shipping_options(&mut self) {
self.fulfillment = None;
self.shipping_quotes = vec![];
}
}
impl<P: Sync + Send + Serialize + DeserializeOwned> Checkout<P> {
pub async fn add_item<C: Context + Send>(
&mut self,
ctx: &mut C,
sku: String,
quantity: u64,
) -> Result<(), Error> {
self.ensure_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 product = ctx.resolve_product(&self.currency, &sku).await?;
let item = Item {
sku: sku.to_string(),
url: product.url.clone(),
title: product.title.clone(),
description: product.description.clone(),
quantity,
price: product.price.clone(),
discount: Money::new(self.currency.clone(), "0"),
images: product.images.clone(),
};
self.items.push(item);
}
self.update_item_prices(ctx).await?;
self.update_invoice(ctx).await?;
ctx.update_checkout(self).await
}
pub async fn remove_item<C: Context + Send>(
&mut self,
ctx: &mut C,
sku: String,
quantity: u64,
) -> Result<(), Error> {
self.ensure_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);
}
self.update_item_prices(ctx).await?;
self.update_invoice(ctx).await?;
ctx.update_checkout(self).await
}
pub async fn confirm_items<C: Context + Send>(&mut self, ctx: &mut C) -> Result<(), Error> {
self.ensure_items_confirmed_state(ctx).await?;
ctx.update_checkout(self).await
}
pub async fn update_contact<C: Context + Send>(
&mut self,
ctx: &mut C,
contact: Contact,
) -> Result<(), Error> {
self.ensure_not_payment_in_progress_or_completed_state()
.await?;
self.contact = Some(contact);
ctx.update_checkout(self).await
}
pub async fn update_shipping_address<C: Context + Send>(
&mut self,
ctx: &mut C,
address: Address,
) -> Result<(), Error> {
self.ensure_not_payment_in_progress_or_completed_state()
.await?;
self.reset_shipping_options();
self.shipping_address = Some(address);
if let State::ItemsConfirmed { reserved_since: _ } = self.state {
self.shipping_quotes = ctx
.get_shipping_quotes(
&self.currency,
&self.promo_codes,
&self.items,
self.shipping_address.as_ref().unwrap(),
)
.await?;
}
self.update_invoice(ctx).await?;
ctx.update_checkout(self).await
}
pub async fn update_fulfillment<C: Context + Send>(
&mut self,
ctx: &mut C,
fulfillment: FulfillmentSelection,
) -> Result<(), Error> {
self.ensure_items_confirmed_state(ctx).await?;
match fulfillment {
FulfillmentSelection::Pickup => {
self.fulfillment = Some(Fulfillment::Pickup);
}
FulfillmentSelection::Shipping { quote_id } => {
let mut updated = false;
for quote in self.shipping_quotes.iter() {
if quote.id == quote_id {
self.fulfillment = Some(Fulfillment::Shipping {
quote: quote.clone(),
});
updated = true;
break;
}
}
if !updated {
return Err(new_invalid_state_error(
"the requested quote is not available",
));
}
}
}
self.update_invoice(ctx).await?;
ctx.update_checkout(self).await
}
pub async fn initiate_payment<C: Context + Send>(
&mut self,
ctx: &mut C,
args: <C as PaymentProcessor>::InitArgs,
) -> Result<(), Error> {
self.ensure_payment_in_progress_state(ctx, args).await?;
ctx.update_checkout(self).await
}
pub async fn initiate_order<C: Context + Send>(
&mut self,
internal_ctx: &InternalContext,
ctx: &mut C,
payment_args: <C as PaymentProcessor>::InitArgs,
) -> Result<(), Error> {
if let State::ItemsConfirmed { reserved_since } = self.state {
self.ensure_completed_state().await?;
let order = Order::new_from_checkout_missing_payment(
internal_ctx,
ctx,
self,
reserved_since,
payment_args,
)
.await?;
self.order_id = Some(order.id.clone());
return ctx.update_checkout(self).await;
} else {
return Err(new_invalid_state_error(
"must be in the ItemsConfirmed state",
));
}
}
pub async fn release<C: Context + Send>(&mut self, ctx: &mut C) -> Result<(), Error> {
match self.state {
State::Shopping => Ok(()),
State::ItemsConfirmed { reserved_since: _ }
| State::PaymentInProgress { reserved_since: _ } => {
if let State::PaymentInProgress { reserved_since: _ } = self.state {
self.payment_id = None;
}
self.enter_shopping_state(ctx).await?;
self.update_item_prices(ctx).await?;
self.update_invoice(ctx).await?;
ctx.update_checkout(self).await
}
_ => Err(new_invalid_state_error(
"must be in the shopping or items_confirmed state",
)),
}
}
pub async fn complete<C: Context + Send>(
&mut self,
internal_ctx: &InternalContext,
ctx: &mut C,
) -> Result<(), Error> {
if !matches!(self.state, State::PaymentInProgress { reserved_since: _ }) {
return Err(new_invalid_state_error(
"must be in the PaymentInProgress state",
));
}
self.ensure_completed_state().await?;
let order = Order::new_from_checkout(internal_ctx, ctx, self).await?;
self.order_id = Some(order.id.clone());
ctx.update_checkout(self).await
}
pub async fn populate_associations<C: Context + Send>(
&mut self,
ctx: &mut C,
) -> Result<(), Error> {
if let Some(ref payment_id) = self.payment_id {
self.payment = ctx.get_payment(payment_id).await?;
}
if let Some(ref order_id) = self.order_id {
let mut maybe_order: Option<Order<P>> = ctx.get_order(order_id).await?;
if let Some(ref mut order) = maybe_order {
order.populate_associations(ctx).await?;
}
self.order = maybe_order;
}
Ok(())
}
}