use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use crate::types::Chargeback;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEvent {
pub event_type: String,
pub resource_type: String,
pub resource_id: String,
pub data: serde_json::Value,
pub received_at: DateTime<Utc>,
pub source_ip: IpAddr,
}
impl WebhookEvent {
pub fn new(
event_type: impl Into<String>,
resource_type: impl Into<String>,
resource_id: impl Into<String>,
data: serde_json::Value,
source_ip: IpAddr,
) -> Self {
Self {
event_type: event_type.into(),
resource_type: resource_type.into(),
resource_id: resource_id.into(),
data,
received_at: Utc::now(),
source_ip,
}
}
pub fn is_chargeback_event(&self) -> bool {
self.event_type.starts_with("chargeback.")
}
pub fn is_transaction_event(&self) -> bool {
self.event_type.starts_with("txn.")
}
pub fn is_merchant_event(&self) -> bool {
self.event_type.starts_with("merchant.")
}
pub fn is_disbursement_event(&self) -> bool {
self.event_type.starts_with("disbursement.")
}
pub fn as_chargeback_event(&self) -> Option<ChargebackEvent> {
if !self.is_chargeback_event() {
return None;
}
let chargeback: Chargeback = serde_json::from_value(self.data.clone()).ok()?;
let id = self.resource_id.clone();
match self.event_type.as_str() {
"chargeback.created" => Some(ChargebackEvent::Created {
chargeback_id: id,
data: chargeback,
}),
"chargeback.opened" => Some(ChargebackEvent::Opened {
chargeback_id: id,
data: chargeback,
}),
"chargeback.closed" => Some(ChargebackEvent::Closed {
chargeback_id: id,
data: chargeback,
}),
"chargeback.won" => Some(ChargebackEvent::Won {
chargeback_id: id,
data: chargeback,
}),
"chargeback.lost" => Some(ChargebackEvent::Lost {
chargeback_id: id,
data: chargeback,
}),
_ => Some(ChargebackEvent::Other {
chargeback_id: id,
event_type: self.event_type.clone(),
data: chargeback,
}),
}
}
}
#[derive(Debug, Clone)]
pub enum ChargebackEvent {
Created {
chargeback_id: String,
data: Chargeback,
},
Opened {
chargeback_id: String,
data: Chargeback,
},
Closed {
chargeback_id: String,
data: Chargeback,
},
Won {
chargeback_id: String,
data: Chargeback,
},
Lost {
chargeback_id: String,
data: Chargeback,
},
Other {
chargeback_id: String,
event_type: String,
data: Chargeback,
},
}
impl ChargebackEvent {
pub fn chargeback_id(&self) -> &str {
match self {
Self::Created { chargeback_id, .. }
| Self::Opened { chargeback_id, .. }
| Self::Closed { chargeback_id, .. }
| Self::Won { chargeback_id, .. }
| Self::Lost { chargeback_id, .. }
| Self::Other { chargeback_id, .. } => chargeback_id,
}
}
pub fn data(&self) -> &Chargeback {
match self {
Self::Created { data, .. }
| Self::Opened { data, .. }
| Self::Closed { data, .. }
| Self::Won { data, .. }
| Self::Lost { data, .. }
| Self::Other { data, .. } => data,
}
}
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Closed { .. } | Self::Won { .. } | Self::Lost { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WebhookEventType {
Create,
Update,
Delete,
Ownership,
Batch,
Account,
AccountCreated,
AccountUpdated,
Chargeback,
ChargebackCreated,
ChargebackOpened,
ChargebackClosed,
ChargebackWon,
ChargebackLost,
TransactionCreated,
TransactionApproved,
TransactionFailed,
TransactionCaptured,
TransactionSettled,
TransactionReturned,
MerchantCreated,
MerchantBoarding,
MerchantBoarded,
MerchantClosed,
MerchantFailed,
MerchantHeld,
DisbursementRequested,
DisbursementProcessing,
DisbursementProcessed,
DisbursementFailed,
DisbursementDenied,
DisbursementReturned,
Payout,
Fee,
SubscriptionCreated,
SubscriptionUpdated,
SubscriptionCancelled,
}
impl WebhookEventType {
pub fn as_event_str(&self) -> &'static str {
match self {
Self::Create => "create",
Self::Update => "update",
Self::Delete => "delete",
Self::Ownership => "ownership",
Self::Batch => "batch",
Self::Account => "account",
Self::AccountCreated => "account.created",
Self::AccountUpdated => "account.updated",
Self::Chargeback => "chargeback",
Self::ChargebackCreated => "chargeback.created",
Self::ChargebackOpened => "chargeback.opened",
Self::ChargebackClosed => "chargeback.closed",
Self::ChargebackWon => "chargeback.won",
Self::ChargebackLost => "chargeback.lost",
Self::TransactionCreated => "txn.created",
Self::TransactionApproved => "txn.approved",
Self::TransactionFailed => "txn.failed",
Self::TransactionCaptured => "txn.captured",
Self::TransactionSettled => "txn.settled",
Self::TransactionReturned => "txn.returned",
Self::MerchantCreated => "merchant.created",
Self::MerchantBoarding => "merchant.boarding",
Self::MerchantBoarded => "merchant.boarded",
Self::MerchantClosed => "merchant.closed",
Self::MerchantFailed => "merchant.failed",
Self::MerchantHeld => "merchant.held",
Self::DisbursementRequested => "disbursement.requested",
Self::DisbursementProcessing => "disbursement.processing",
Self::DisbursementProcessed => "disbursement.processed",
Self::DisbursementFailed => "disbursement.failed",
Self::DisbursementDenied => "disbursement.denied",
Self::DisbursementReturned => "disbursement.returned",
Self::Payout => "payout",
Self::Fee => "fee",
Self::SubscriptionCreated => "subscription.created",
Self::SubscriptionUpdated => "subscription.updated",
Self::SubscriptionCancelled => "subscription.cancelled",
}
}
pub fn all_chargeback_events() -> Vec<Self> {
vec![
Self::ChargebackCreated,
Self::ChargebackOpened,
Self::ChargebackClosed,
Self::ChargebackWon,
Self::ChargebackLost,
]
}
pub fn all_transaction_events() -> Vec<Self> {
vec![
Self::TransactionCreated,
Self::TransactionApproved,
Self::TransactionFailed,
Self::TransactionCaptured,
Self::TransactionSettled,
Self::TransactionReturned,
]
}
pub fn all_merchant_events() -> Vec<Self> {
vec![
Self::MerchantCreated,
Self::MerchantBoarding,
Self::MerchantBoarded,
Self::MerchantClosed,
Self::MerchantFailed,
Self::MerchantHeld,
]
}
pub fn all_disbursement_events() -> Vec<Self> {
vec![
Self::DisbursementRequested,
Self::DisbursementProcessing,
Self::DisbursementProcessed,
Self::DisbursementFailed,
Self::DisbursementDenied,
Self::DisbursementReturned,
]
}
}
impl std::fmt::Display for WebhookEventType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_event_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
#[test]
fn test_webhook_event_type_chargeback() {
let event = WebhookEvent::new(
"chargeback.created",
"chargebacks",
"t1_chb_123",
serde_json::json!({}),
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
);
assert!(event.is_chargeback_event());
assert!(!event.is_transaction_event());
}
#[test]
fn test_webhook_event_type_transaction() {
let event = WebhookEvent::new(
"txn.approved",
"txns",
"t1_txn_123",
serde_json::json!({}),
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
);
assert!(!event.is_chargeback_event());
assert!(event.is_transaction_event());
}
#[test]
fn test_webhook_event_type_as_str() {
assert_eq!(WebhookEventType::ChargebackCreated.as_event_str(), "chargeback.created");
assert_eq!(WebhookEventType::TransactionApproved.as_event_str(), "txn.approved");
assert_eq!(WebhookEventType::MerchantBoarded.as_event_str(), "merchant.boarded");
}
#[test]
fn test_all_chargeback_events() {
let events = WebhookEventType::all_chargeback_events();
assert_eq!(events.len(), 5);
assert!(events.contains(&WebhookEventType::ChargebackCreated));
assert!(events.contains(&WebhookEventType::ChargebackLost));
}
#[test]
fn test_chargeback_event_terminal() {
let won = ChargebackEvent::Won {
chargeback_id: "123".to_string(),
data: create_test_chargeback(),
};
assert!(won.is_terminal());
let created = ChargebackEvent::Created {
chargeback_id: "123".to_string(),
data: create_test_chargeback(),
};
assert!(!created.is_terminal());
}
fn create_test_chargeback() -> Chargeback {
serde_json::from_value(serde_json::json!({
"id": "t1_chb_12345678901234567890123"
}))
.unwrap()
}
}