use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::stripe::error::{StripeWebhookError, StripeWebhookResult};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StripeEventType {
#[serde(rename = "customer.created")]
CustomerCreated,
#[serde(rename = "customer.subscription.created")]
SubscriptionCreated,
#[serde(rename = "customer.subscription.updated")]
SubscriptionUpdated,
#[serde(rename = "customer.subscription.deleted")]
SubscriptionDeleted,
#[serde(rename = "invoice.payment_succeeded")]
InvoicePaymentSucceeded,
#[serde(rename = "invoice.payment_failed")]
InvoicePaymentFailed,
#[serde(other)]
Unknown,
}
impl FromStr for StripeEventType {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"customer.created" => Self::CustomerCreated,
"customer.subscription.created" => Self::SubscriptionCreated,
"customer.subscription.updated" => Self::SubscriptionUpdated,
"customer.subscription.deleted" => Self::SubscriptionDeleted,
"invoice.payment_succeeded" => Self::InvoicePaymentSucceeded,
"invoice.payment_failed" => Self::InvoicePaymentFailed,
_ => Self::Unknown,
})
}
}
impl StripeEventType {
pub fn as_str(&self) -> &'static str {
match self {
Self::CustomerCreated => "customer.created",
Self::SubscriptionCreated => "customer.subscription.created",
Self::SubscriptionUpdated => "customer.subscription.updated",
Self::SubscriptionDeleted => "customer.subscription.deleted",
Self::InvoicePaymentSucceeded => "invoice.payment_succeeded",
Self::InvoicePaymentFailed => "invoice.payment_failed",
Self::Unknown => "unknown",
}
}
pub fn is_known(&self) -> bool {
!matches!(self, Self::Unknown)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StripeEvent {
pub id: String,
#[serde(rename = "type")]
pub event_type: String,
pub created: i64,
#[serde(default)]
pub api_version: Option<String>,
pub livemode: bool,
#[serde(default)]
pub pending_webhooks: u32,
pub data: EventData,
#[serde(default)]
pub request: Option<EventRequest>,
}
impl StripeEvent {
pub fn from_bytes(bytes: &[u8]) -> StripeWebhookResult<Self> {
serde_json::from_slice(bytes).map_err(|e| StripeWebhookError::InvalidPayload(e.to_string()))
}
pub fn typed_event_type(&self) -> StripeEventType {
StripeEventType::from_str(&self.event_type).unwrap()
}
pub fn as_subscription(&self) -> StripeWebhookResult<SubscriptionEvent> {
match self.typed_event_type() {
StripeEventType::SubscriptionCreated
| StripeEventType::SubscriptionUpdated
| StripeEventType::SubscriptionDeleted => {
let subscription: Subscription =
serde_json::from_value(self.data.object.clone())
.map_err(|e| StripeWebhookError::InvalidPayload(e.to_string()))?;
Ok(SubscriptionEvent {
event_id: self.id.clone(),
event_type: self.typed_event_type(),
subscription,
previous_attributes: self.data.previous_attributes.clone(),
})
}
_ => Err(StripeWebhookError::InvalidPayload(format!(
"Event {} is not a subscription event",
self.event_type
))),
}
}
pub fn as_invoice(&self) -> StripeWebhookResult<InvoiceEvent> {
match self.typed_event_type() {
StripeEventType::InvoicePaymentSucceeded | StripeEventType::InvoicePaymentFailed => {
let invoice: Invoice = serde_json::from_value(self.data.object.clone())
.map_err(|e| StripeWebhookError::InvalidPayload(e.to_string()))?;
Ok(InvoiceEvent {
event_id: self.id.clone(),
event_type: self.typed_event_type(),
invoice,
})
}
_ => Err(StripeWebhookError::InvalidPayload(format!(
"Event {} is not an invoice event",
self.event_type
))),
}
}
pub fn as_customer(&self) -> StripeWebhookResult<CustomerEvent> {
match self.typed_event_type() {
StripeEventType::CustomerCreated => {
let customer: Customer = serde_json::from_value(self.data.object.clone())
.map_err(|e| StripeWebhookError::InvalidPayload(e.to_string()))?;
Ok(CustomerEvent {
event_id: self.id.clone(),
event_type: self.typed_event_type(),
customer,
})
}
_ => Err(StripeWebhookError::InvalidPayload(format!(
"Event {} is not a customer event",
self.event_type
))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventData {
pub object: serde_json::Value,
#[serde(default)]
pub previous_attributes: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventRequest {
pub id: Option<String>,
pub idempotency_key: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SubscriptionEvent {
pub event_id: String,
pub event_type: StripeEventType,
pub subscription: Subscription,
pub previous_attributes: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Subscription {
pub id: String,
pub customer: String,
pub status: SubscriptionStatus,
pub current_period_start: i64,
pub current_period_end: i64,
#[serde(default)]
pub cancel_at_period_end: bool,
pub canceled_at: Option<i64>,
pub ended_at: Option<i64>,
pub trial_end: Option<i64>,
pub items: SubscriptionItems,
#[serde(default)]
pub metadata: serde_json::Value,
pub livemode: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionStatus {
Active,
PastDue,
Unpaid,
Canceled,
Incomplete,
IncompleteExpired,
Trialing,
Paused,
#[serde(other)]
Unknown,
}
impl SubscriptionStatus {
pub fn is_active(&self) -> bool {
matches!(self, Self::Active | Self::Trialing)
}
pub fn requires_payment_action(&self) -> bool {
matches!(self, Self::PastDue | Self::Unpaid | Self::Incomplete)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionItems {
pub data: Vec<SubscriptionItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscriptionItem {
pub id: String,
pub price: Price,
#[serde(default = "default_quantity")]
pub quantity: u32,
}
fn default_quantity() -> u32 {
1
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Price {
pub id: String,
pub product: String,
pub unit_amount: Option<i64>,
pub currency: String,
pub recurring: Option<Recurring>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recurring {
pub interval: String,
pub interval_count: u32,
}
#[derive(Debug, Clone)]
pub struct InvoiceEvent {
pub event_id: String,
pub event_type: StripeEventType,
pub invoice: Invoice,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
pub id: String,
pub customer: String,
pub subscription: Option<String>,
pub status: InvoiceStatus,
pub amount_due: i64,
pub amount_paid: i64,
pub amount_remaining: i64,
pub currency: String,
pub billing_reason: Option<String>,
pub customer_email: Option<String>,
pub hosted_invoice_url: Option<String>,
pub invoice_pdf: Option<String>,
pub payment_intent: Option<String>,
pub created: i64,
pub period_start: i64,
pub period_end: i64,
pub livemode: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InvoiceStatus {
Draft,
Open,
Paid,
Uncollectible,
Void,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone)]
pub struct CustomerEvent {
pub event_id: String,
pub event_type: StripeEventType,
pub customer: Customer,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Customer {
pub id: String,
pub email: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub created: i64,
#[serde(default)]
pub metadata: serde_json::Value,
pub livemode: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_type_parsing() {
assert_eq!(
StripeEventType::from_str("customer.subscription.created").unwrap(),
StripeEventType::SubscriptionCreated
);
assert_eq!(
StripeEventType::from_str("invoice.payment_succeeded").unwrap(),
StripeEventType::InvoicePaymentSucceeded
);
assert_eq!(
StripeEventType::from_str("unknown.event").unwrap(),
StripeEventType::Unknown
);
}
#[test]
fn test_subscription_status() {
assert!(SubscriptionStatus::Active.is_active());
assert!(SubscriptionStatus::Trialing.is_active());
assert!(!SubscriptionStatus::Canceled.is_active());
assert!(SubscriptionStatus::PastDue.requires_payment_action());
assert!(!SubscriptionStatus::Active.requires_payment_action());
}
#[test]
fn test_parse_subscription_event() {
let json = r#"{
"id": "evt_1234567890",
"type": "customer.subscription.created",
"created": 1614556800,
"livemode": false,
"pending_webhooks": 1,
"data": {
"object": {
"id": "sub_1234567890",
"customer": "cus_1234567890",
"status": "active",
"current_period_start": 1614556800,
"current_period_end": 1617235200,
"cancel_at_period_end": false,
"items": {
"data": [{
"id": "si_1234567890",
"price": {
"id": "price_1234567890",
"product": "prod_1234567890",
"unit_amount": 2000,
"currency": "usd",
"recurring": {
"interval": "month",
"interval_count": 1
}
},
"quantity": 1
}]
},
"metadata": {},
"livemode": false
}
}
}"#;
let event = StripeEvent::from_bytes(json.as_bytes()).unwrap();
assert_eq!(event.id, "evt_1234567890");
assert_eq!(
event.typed_event_type(),
StripeEventType::SubscriptionCreated
);
let sub_event = event.as_subscription().unwrap();
assert_eq!(sub_event.subscription.id, "sub_1234567890");
assert_eq!(sub_event.subscription.status, SubscriptionStatus::Active);
}
#[test]
fn test_parse_invoice_event() {
let json = r#"{
"id": "evt_invoice_1234",
"type": "invoice.payment_succeeded",
"created": 1614556800,
"livemode": false,
"pending_webhooks": 1,
"data": {
"object": {
"id": "in_1234567890",
"customer": "cus_1234567890",
"subscription": "sub_1234567890",
"status": "paid",
"amount_due": 2000,
"amount_paid": 2000,
"amount_remaining": 0,
"currency": "usd",
"created": 1614556800,
"period_start": 1614556800,
"period_end": 1617235200,
"livemode": false
}
}
}"#;
let event = StripeEvent::from_bytes(json.as_bytes()).unwrap();
let invoice_event = event.as_invoice().unwrap();
assert_eq!(invoice_event.invoice.id, "in_1234567890");
assert_eq!(invoice_event.invoice.status, InvoiceStatus::Paid);
assert_eq!(invoice_event.invoice.amount_paid, 2000);
}
}