use axum::{ extract::State, http::{HeaderMap, StatusCode}, Json, }; use db::repos::{subscriptions, users}; use serde::{Deserialize, Serialize}; use crate::{middleware::jwt::JwtAuth, state::AppState}; use axum::Extension; #[derive(Debug, Deserialize)] pub struct CreateCheckoutRequest { pub price_id: String, } #[derive(Debug, Serialize)] pub struct CheckoutResponse { pub checkout_url: String, } pub async fn create_checkout( State(state): State, Extension(auth): Extension, Json(body): Json, ) -> Result, StatusCode> { let stripe_secret = std::env::var("STRIPE_SECRET_KEY").map_err(|_| StatusCode::NOT_IMPLEMENTED)?; let user = users::find_by_id(&state.db, auth.user_id) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; // Create Stripe customer via HTTP API directly (simpler than SDK for MVP) let client = reqwest::Client::new(); let customer_res = client .post("https://api.stripe.com/v1/customers") .basic_auth(&stripe_secret, Some("")) .form(&[("email", &user.email)]) .send() .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let customer_data: serde_json::Value = customer_res.json().await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let customer_id = customer_data["id"].as_str().unwrap_or(""); let success_url = std::env::var("STRIPE_SUCCESS_URL") .unwrap_or_else(|_| "http://localhost:3000/dashboard?success=true".to_string()); let cancel_url = std::env::var("STRIPE_CANCEL_URL") .unwrap_or_else(|_| "http://localhost:3000/dashboard?canceled=true".to_string()); let session_res = client .post("https://api.stripe.com/v1/checkout/sessions") .basic_auth(&stripe_secret, Some("")) .form(&[ ("customer", customer_id), ("success_url", &success_url), ("cancel_url", &cancel_url), ("mode", "subscription"), ("line_items[0][price]", &body.price_id), ("line_items[0][quantity]", "1"), ("metadata[user_id]", &auth.user_id.to_string()), ]) .send() .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let session_data: serde_json::Value = session_res.json().await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let url = session_data["url"].as_str().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; Ok(Json(CheckoutResponse { checkout_url: url.to_string() })) } #[derive(Debug, Deserialize)] pub struct StripeWebhook { #[serde(rename = "type")] pub event_type: String, pub data: StripeEventData, } #[derive(Debug, Deserialize)] pub struct StripeEventData { pub object: serde_json::Value, } pub async fn webhook( State(state): State, headers: HeaderMap, body: String, ) -> Result { let stripe_secret = std::env::var("STRIPE_WEBHOOK_SECRET").unwrap_or_default(); // Verify webhook signature if configured if !stripe_secret.is_empty() { let sig = headers .get("stripe-signature") .and_then(|v| v.to_str().ok()) .ok_or(StatusCode::BAD_REQUEST)?; // In production, verify signature using Stripe library // For MVP, we log and process tracing::info!("Webhook signature: {}", sig); } let event: serde_json::Value = serde_json::from_str(&body).map_err(|_| StatusCode::BAD_REQUEST)?; let event_type = event["type"].as_str().unwrap_or(""); match event_type { "checkout.session.completed" => { if let Some(metadata) = event["data"]["object"]["metadata"].as_object() { if let Some(user_id_str) = metadata.get("user_id").and_then(|v| v.as_str()) { if let Ok(user_id) = uuid::Uuid::parse_str(user_id_str) { let customer_id = event["data"]["object"]["customer"].as_str().unwrap_or(""); let subscription_id = event["data"]["object"]["subscription"].as_str().unwrap_or(""); let _ = subscriptions::create_or_update( &state.db, user_id, Some(customer_id), Some(subscription_id), None, "active", "paid", ).await; tracing::info!("Subscription activated for user {}", user_id); } } } } "invoice.payment_succeeded" => { tracing::info!("Invoice payment succeeded"); } "customer.subscription.deleted" => { if let Some(sub_id) = event["data"]["object"]["id"].as_str() { let _ = subscriptions::update_status(&state.db, sub_id, "canceled").await; tracing::info!("Subscription {} canceled", sub_id); } } _ => {} } Ok(StatusCode::OK) }