checkout_core 0.0.147

core traits and structs for the checkout_controller crate
Documentation
use async_trait::async_trait;
use cloud_pubsub;
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::checkout::Checkout;
use crate::context::Context;
use crate::error::{new_application_error, Error};
use crate::internal_context::InternalContext;
use crate::money::Money;
use crate::order::Order;

pub mod handlers;

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct Payment<Data> {
    pub id: String,
    pub state: PaymentState,
    pub initiator_id: InitiatorId,
    pub charge_amount: Money,
    pub correlation_id: Option<String>,
    pub data: Option<Data>,
}

#[derive(Serialize, Deserialize, JsonSchema)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum PaymentState {
    Incomplete,
    Succeeded,
    Cancelled,
}

#[derive(Serialize, Deserialize, JsonSchema)]
pub enum InitiatorId {
    Checkout(String),
    Order(String),
}

pub enum Initiator<'a, P: Sync + Send> {
    Checkout(&'a mut Checkout<P>),
    Order(&'a mut Order<P>),
}

#[derive(Serialize, Deserialize)]
pub struct CallbackArgs<T> {
    pub correlation_id: String,
    pub process_args: T,
}

impl<T: DeserializeOwned> cloud_pubsub::FromPubSubMessage for CallbackArgs<T> {
    fn from(message: cloud_pubsub::EncodedMessage) -> Result<Self, cloud_pubsub::error::Error> {
        match message.decode() {
            Ok(bytes) => {
                let val: CallbackArgs<T> = serde_json::from_slice(&bytes).unwrap();
                Ok(val)
            }
            Err(e) => Err(cloud_pubsub::error::Error::from(e)),
        }
    }
}

#[async_trait]
pub trait PaymentProcessor {
    type Data: Sync + Send + Serialize + DeserializeOwned;
    type InitArgs: Sync + Send + Serialize + DeserializeOwned;
    type ProcessArgs: Sync + Send + Serialize + DeserializeOwned;

    async fn initiate_payment(
        &mut self,
        _payment: &Payment<Self::Data>,
        _args: &Self::InitArgs,
    ) -> Result<PaymentData<Self::Data>, Error> {
        Ok(PaymentData {
            correlation_id: None,
            data: None,
        })
    }

    async fn process_payment(
        &mut self,
        payment: &Payment<Self::Data>,
        args: &Self::ProcessArgs,
    ) -> Result<ProcessingResult<Self::Data>, Error>;

    // this is called synchronously and only once per release request
    async fn cancel_payment(
        &mut self,
        payment: &Payment<Self::Data>,
    ) -> Result<CancellationResult, Error>;
}

#[async_trait]
pub trait PaymentStore: PaymentProcessor {
    async fn create_payment(
        &mut self,
        payment: &Payment<<Self as PaymentProcessor>::Data>,
    ) -> Result<(), Error>;

    async fn update_payment(
        &mut self,
        payment: &Payment<<Self as PaymentProcessor>::Data>,
    ) -> Result<(), Error>;

    async fn get_payment<P: Sync + Send + Serialize + DeserializeOwned>(
        &mut self,
        id: &str,
    ) -> Result<Option<P>, Error>;

    async fn get_payment_for_update<P: Sync + Send + Serialize + DeserializeOwned>(
        &mut self,
        id: &str,
    ) -> Result<Option<P>, Error>;

    async fn get_payment_for_update_by_correlation_id<
        P: Sync + Send + Serialize + DeserializeOwned,
    >(
        &mut self,
        _id: &str,
    ) -> Result<Option<P>, Error> {
        Ok(None)
    }
}

pub struct ProcessingResult<Data> {
    pub state: PaymentState,
    pub payment_data: PaymentData<Data>,
}

pub struct CancellationResult {
    pub success: bool,
}

#[derive(Serialize, Deserialize, JsonSchema)]
pub struct PaymentData<Data> {
    pub correlation_id: Option<String>,
    pub data: Option<Data>,
}

// constructor
async fn new<'a, C: Context + Send, P: Sync + Send + Serialize + DeserializeOwned>(
    ctx: &mut C,
    initiator: Initiator<'a, P>,
    charge_amount: Money,
    args: <C as PaymentProcessor>::InitArgs,
) -> Result<Payment<<C as PaymentProcessor>::Data>, Error> {
    let mut payment = Payment {
        id: Uuid::new_v4().to_string(),
        state: PaymentState::Incomplete,
        initiator_id: match &initiator {
            Initiator::Checkout(co) => InitiatorId::Checkout(co.id.clone()),
            Initiator::Order(od) => InitiatorId::Order(od.id.clone()),
        },
        charge_amount,
        correlation_id: None,
        data: None,
    };

    let payment_data = ctx.initiate_payment(&payment, &args).await?;

    payment.correlation_id = payment_data.correlation_id;
    payment.data = payment_data.data;

    ctx.create_payment(&payment).await?;
    Ok(payment)
}

pub async fn process<'a, C: Context + Send, P: Sync + Send + Serialize + DeserializeOwned>(
    internal_ctx: &InternalContext,
    ctx: &mut C,
    payment: &mut Payment<<C as PaymentProcessor>::Data>,
    initiator: Initiator<'a, P>,
    args: <C as PaymentProcessor>::ProcessArgs,
) -> Result<(), Error> {
    match &payment.state {
        PaymentState::Succeeded | PaymentState::Cancelled => {
            return Err(new_application_error(
                "PAYMENT_COMPLETED",
                "the payment has already completed",
            ))
        }
        _ => {}
    }

    let result = ctx.process_payment(payment, &args).await?;

    payment.correlation_id = result.payment_data.correlation_id;
    payment.data = result.payment_data.data;

    payment.state = result.state;

    ctx.update_payment(payment).await?;

    match payment.state {
        PaymentState::Succeeded => match initiator {
            Initiator::Checkout(co) => co.complete(internal_ctx, ctx).await,
            Initiator::Order(od) => od.prompt_confirmation(internal_ctx, ctx).await,
        },
        PaymentState::Cancelled => match initiator {
            Initiator::Checkout(co) => co.release(ctx).await,
            Initiator::Order(od) => od.cancel(internal_ctx, ctx).await,
        },
        _ => Ok(()),
    }
}

pub async fn cancel<'a, C: Context + Send, P: Sync + Send + Serialize + DeserializeOwned>(
    internal_ctx: &InternalContext,
    ctx: &mut C,
    payment: &mut Payment<<C as PaymentProcessor>::Data>,
    initiator: Initiator<'a, P>,
) -> Result<(), Error> {
    match &payment.state {
        PaymentState::Cancelled => Ok(()),
        PaymentState::Incomplete => {
            let cancellation_result = ctx.cancel_payment(payment).await?;
            if !cancellation_result.success {
                return Err(new_application_error(
                    "CANCELLATION_UNAVAILABLE",
                    "the payment can no longer be cancelled",
                ));
            }

            payment.state = PaymentState::Cancelled;
            ctx.update_payment(payment).await?;

            match initiator {
                Initiator::Checkout(co) => co.release(ctx).await,
                Initiator::Order(od) => od.cancel(internal_ctx, ctx).await,
            }
        }
        PaymentState::Succeeded => Err(new_application_error(
            "PAYMENT_COMPLETED",
            "the payment has already completed",
        )),
    }
}