correlate 0.3.0

correlate is a standalone server that listens for Stripe webhook events and sends notification emails about successful orders.
use super::{customer::StripeCustomerDetails, date::StripeDate, StripeCurrency};
use rocket::serde::Deserialize;
use std::fmt;
use validator::Validate;

/// Payment modes supported by Stripe Checkout
#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
#[serde(crate = "rocket::serde")]
pub enum StripeMode {
    Payment,
    Subscription,
    Setup,
}

/// Payment status of a Stripe Checkout session
#[derive(Debug, Deserialize)]
#[serde(crate = "rocket::serde")]
#[serde(rename_all = "lowercase")]
pub enum StripePaymentStatus {
    Paid,
    Unpaid,
    #[serde(rename = "no_payment_required")]
    NoPaymentRequired,
}

/// The type of this Stripe object
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
#[serde(crate = "rocket::serde")]
pub enum StripeObjectType {
    /// Object type, should always be "checkout.session"
    #[serde(rename = "checkout.session")]
    CheckoutSession,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
#[serde(crate = "rocket::serde")]
pub enum StripeSessionStatus {
    /// The checkout session is still in progress. Payment processing has not started
    Open,
    /// The checkout session is complete. Payment processing may still be in progress
    Complete,
    /// The checkout session has expired. No further processing will occur
    Expired,
}
impl fmt::Display for StripeSessionStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Open => write!(f, "open"),
            Self::Complete => write!(f, "complete"),
            Self::Expired => write!(f, "expired"),
        }
    }
}

/// Represents a Stripe Checkout session with all relevant details
#[derive(Debug, Deserialize, Validate)]
#[serde(crate = "rocket::serde")]
pub struct StripeCheckoutSession {
    /// Unique identifier for the checkout session
    #[validate(contains(pattern = "cs_"))]
    pub id: String,
    /// Object type, should always be "checkout.session"
    #[serde(rename = "object")]
    pub object_type: StripeObjectType,
    /// Subtotal amount in smallest currency unit (e.g., cents)
    pub amount_subtotal: u32,
    /// Total amount including taxes and discounts
    pub amount_total: u32,
    /// Creation date of checkout session in local timezone
    pub created: StripeDate,
    /// Currency of the session
    pub currency: StripeCurrency,
    /// Mode of the checkout session
    pub mode: StripeMode,
    /// Current payment status
    pub payment_status: StripePaymentStatus,
    /// Current session status
    pub status: StripeSessionStatus,
    /// Details about customer that placed order
    pub customer_details: Option<StripeCustomerDetails>,
    /// URL for redirecting back to the checkout session
    pub url: String,
}

/// Validation methods for StripeCheckoutSession
impl StripeCheckoutSession {
    /// Validates the session object type
    pub fn has_valid_object_type(&self) -> bool {
        self.object_type == StripeObjectType::CheckoutSession
    }

    /// Validates if the session is completed and paid
    pub fn is_successful(&self) -> bool {
        matches!(self.status, StripeSessionStatus::Complete) && self.is_paid()
    }

    /// Validates if the session is paid
    fn is_paid(&self) -> bool {
        matches!(self.payment_status, StripePaymentStatus::Paid)
    }

    /// Returns the formatted amount with currency symbol.
    ///
    /// # Examples
    ///
    /// ```
    /// let charge = StripeCharge { amount: 1000, currency: "usd", ... };
    /// assert_eq!(charge.formatted_amount(), "$10.00");
    /// ```
    pub fn formatted_amount_total(&self) -> String {
        let symbol = match self.currency {
            StripeCurrency::USD => "$",
            StripeCurrency::CAD => "CA$\u{a0}",
            StripeCurrency::EUR => "",
            StripeCurrency::GBP => "£",
        };

        let amount = self.amount_total as f64 / 100.0;
        format!("{}{:.2}", symbol, amount)
    }
}