use actix_web::{HttpResponse, HttpRequest, web, post}; use stripe::{Event, EventType, EventObject, Webhook}; use sqlx::PgPool; use crate::{ http_error::HttpError, models::{ purchase::{Purchase, Status}, user::User }, STRIPE_WEBHOOK_SECRET }; #[post("/webhook/stripe")] pub async fn route( req: HttpRequest, db: web::Data, body: web::Bytes ) -> Result { let payload = std::str::from_utf8(&body).unwrap(); let signature = get_signature(&req)?; let secret = STRIPE_WEBHOOK_SECRET.get().unwrap(); let event = Webhook::construct_event(payload, &signature, secret) .map_err(|_| HttpError::InvalidInput("Failed to verify webhook".into()))?; match event.type_ { EventType::PaymentIntentSucceeded => {handle_success(event, &db).await?}, EventType::PaymentIntentPaymentFailed => {handle_failure(event, &db).await?}, _ => () }; Ok(HttpResponse::Ok().finish()) } fn get_signature(req: &HttpRequest) -> Result { let signature = req.headers() .get("stripe-signature") .and_then(|v| v.to_str().ok()) .ok_or_else(|| HttpError::InvalidInput("No signature".into()))? .to_string(); Ok(signature) } async fn handle_success(event: Event, db: &PgPool) -> Result<(), HttpError> { let id = match event.data.object { EventObject::PaymentIntent(pi) => pi.id.to_string(), _ => {return Err(HttpError::InvalidInput("No payment intent ID".into()))} }; let p = Purchase::update_status(db, id, Status::Succeeded).await?; User::add_tokens(db, p.user_id, p.pack_count * p.tokens_per_pack).await?; Ok(()) } async fn handle_failure(event: Event, db: &PgPool) -> Result<(), HttpError> { let id = match event.data.object { EventObject::PaymentIntent(pi) => pi.id.to_string(), _ => {return Err(HttpError::InvalidInput("No payment intent ID".into()))} }; Purchase::update_status(db, id, Status::Succeeded).await?; Ok(()) }