correlate 0.3.0

correlate is a standalone server that listens for Stripe webhook events and sends notification emails about successful orders.
use chrono::{DateTime, Duration, Local, TimeZone};
use rocket::serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;

/// Custom date type for Stripe timestamps
#[derive(Debug, Clone)]
pub struct StripeDate(DateTime<Local>);

impl StripeDate {
    pub fn new(timestamp: i64) -> Self {
        Self(Local.timestamp_opt(timestamp, 0).unwrap())
    }

    pub fn into_inner(self) -> DateTime<Local> {
        self.0
    }

    /// Provides human-readable relative time (e.g., "2 hours ago")
    pub fn relative_time(&self) -> String {
        let duration = Local::now() - self.0;

        match duration {
            d if d < Duration::minutes(1) => "just now".to_string(),
            d if d < Duration::hours(1) => format!("{} minutes ago", d.num_minutes()),
            d if d < Duration::days(1) => format!("{} hours ago", d.num_hours()),
            d if d < Duration::days(7) => format!("{} days ago", d.num_days()),
            d if d < Duration::days(30) => format!("{} weeks ago", d.num_weeks()),
            d if d < Duration::days(365) => format!("{} months ago", d.num_days() / 30),
            d => format!("{} years ago", d.num_days() / 365),
        }
    }
}

impl fmt::Display for StripeDate {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0.format("%Y-%m-%d %H:%M:%S %Z"))
    }
}

impl<'de> Deserialize<'de> for StripeDate {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        // Stripe sends timestamps as seconds since epoch
        let timestamp = i64::deserialize(deserializer)?;
        Ok(StripeDate::new(timestamp))
    }
}

impl Serialize for StripeDate {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_i64(self.0.timestamp())
    }
}