use std::sync::Arc;
use anyhow::Result;
use axum::Router;
use tokio::net::TcpListener;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use reasonkit_web::stripe::{
stripe_webhook_router, CustomerEvent, InvoiceEvent, StripeWebhookConfig, StripeWebhookState,
SubscriptionEvent, SubscriptionHandler,
};
struct MySubscriptionHandler {
}
impl MySubscriptionHandler {
fn new() -> Self {
Self {}
}
}
#[async_trait::async_trait]
impl SubscriptionHandler for MySubscriptionHandler {
async fn on_subscription_created(&self, event: &SubscriptionEvent) -> Result<()> {
let sub = &event.subscription;
tracing::info!(
subscription_id = %sub.id,
customer_id = %sub.customer,
status = ?sub.status,
"New subscription created"
);
if let Some(item) = sub.items.data.first() {
let price_id = &item.price.id;
let product_id = &item.price.product;
tracing::info!(
price_id = %price_id,
product_id = %product_id,
quantity = item.quantity,
"Subscription plan details"
);
}
Ok(())
}
async fn on_subscription_updated(&self, event: &SubscriptionEvent) -> Result<()> {
let sub = &event.subscription;
tracing::info!(
subscription_id = %sub.id,
customer_id = %sub.customer,
status = ?sub.status,
cancel_at_period_end = sub.cancel_at_period_end,
"Subscription updated"
);
if let Some(prev) = &event.previous_attributes {
if let Some(prev_status) = prev.get("status").and_then(|v| v.as_str()) {
tracing::info!(
previous_status = prev_status,
new_status = ?sub.status,
"Subscription status changed"
);
}
}
if sub.cancel_at_period_end {
tracing::warn!(
subscription_id = %sub.id,
period_end = sub.current_period_end,
"Subscription scheduled for cancellation"
);
}
if sub.status.is_active() {
}
Ok(())
}
async fn on_subscription_deleted(&self, event: &SubscriptionEvent) -> Result<()> {
let sub = &event.subscription;
tracing::warn!(
subscription_id = %sub.id,
customer_id = %sub.customer,
ended_at = ?sub.ended_at,
"Subscription deleted"
);
Ok(())
}
async fn on_payment_succeeded(&self, event: &InvoiceEvent) -> Result<()> {
let invoice = &event.invoice;
tracing::info!(
invoice_id = %invoice.id,
customer_id = %invoice.customer,
amount = invoice.amount_paid,
currency = %invoice.currency,
"Payment succeeded"
);
Ok(())
}
async fn on_payment_failed(&self, event: &InvoiceEvent) -> Result<()> {
let invoice = &event.invoice;
tracing::error!(
invoice_id = %invoice.id,
customer_id = %invoice.customer,
amount_due = invoice.amount_due,
billing_reason = ?invoice.billing_reason,
"Payment failed - REQUIRES ATTENTION"
);
if let Some(url) = &invoice.hosted_invoice_url {
tracing::info!(
invoice_url = %url,
"Customer can retry payment at this URL"
);
}
Ok(())
}
async fn on_customer_created(&self, event: &CustomerEvent) -> Result<()> {
let customer = &event.customer;
tracing::info!(
customer_id = %customer.id,
email = ?customer.email,
"New customer created"
);
Ok(())
}
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "stripe_webhook_example=debug,reasonkit_web=debug".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let config = StripeWebhookConfig::from_env()
.expect("STRIPE_WEBHOOK_SECRET environment variable must be set");
tracing::info!("Stripe webhook configuration loaded");
let handler = Arc::new(MySubscriptionHandler::new());
let (state, processor_handle) = StripeWebhookState::new(config, handler);
let state = Arc::new(state);
tokio::spawn(async move {
processor_handle.run().await;
});
tracing::info!("Background event processor started");
let app = Router::new()
.merge(stripe_webhook_router(state.clone()))
.route("/health", axum::routing::get(|| async { "OK" }));
let addr = "0.0.0.0:3000";
let listener = TcpListener::bind(addr).await?;
tracing::info!("Stripe webhook server listening on {}", addr);
tracing::info!("Webhook endpoint: POST {}/webhooks/stripe", addr);
tracing::info!("");
tracing::info!("Test with Stripe CLI:");
tracing::info!(" stripe listen --forward-to localhost:3000/webhooks/stripe");
tracing::info!(" stripe trigger customer.subscription.created");
axum::serve(listener, app).await?;
Ok(())
}