use axum::{ extract::{Json, Query}, http::{HeaderMap, StatusCode}, response::Json as ResponseJson, routing::{get, post}, Router, }; use dotenv::dotenv; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, env}; use tower::ServiceBuilder; use tower_http::{ cors::{Any, CorsLayer}, services::ServeDir, }; use tracing::{info, warn, error}; #[derive(Debug, Deserialize)] struct CreatePaymentIntentRequest { company_name: String, company_type: String, company_email: Option, company_phone: Option, company_website: Option, company_address: Option, company_industry: Option, company_purpose: Option, fiscal_year_end: Option, shareholders: Option, payment_plan: String, agreements: Vec, final_agreement: bool, } #[derive(Debug, Deserialize)] struct CreateResidentPaymentIntentRequest { resident_name: String, email: String, phone: Option, date_of_birth: Option, nationality: Option, passport_number: Option, address: Option, payment_plan: String, amount: f64, #[serde(rename = "type")] request_type: String, } #[derive(Debug, Serialize)] struct CreatePaymentIntentResponse { client_secret: String, payment_intent_id: String, } #[derive(Debug, Serialize)] struct ErrorResponse { error: String, details: Option, } #[derive(Debug, Deserialize)] struct WebhookQuery { #[serde(rename = "payment_intent")] payment_intent_id: Option, #[serde(rename = "payment_intent_client_secret")] client_secret: Option, } // Calculate pricing based on company type and payment plan fn calculate_amount(company_type: &str, payment_plan: &str) -> Result { let base_amounts = match company_type { "Single FZC" => (20, 20), // (setup, monthly) "Startup FZC" => (50, 50), "Growth FZC" => (1000, 100), "Global FZC" => (2000, 200), "Cooperative FZC" => (2000, 200), _ => return Err("Invalid company type".to_string()), }; let (setup_fee, monthly_fee) = base_amounts; let twin_fee = 2; // ZDFZ Twin fee let total_monthly = monthly_fee + twin_fee; let amount_cents = match payment_plan { "monthly" => (setup_fee + total_monthly) * 100, "yearly" => (setup_fee + (total_monthly * 12 * 80 / 100)) * 100, // 20% discount "two_year" => (setup_fee + (total_monthly * 24 * 60 / 100)) * 100, // 40% discount _ => return Err("Invalid payment plan".to_string()), }; Ok(amount_cents as i64) } // Create payment intent with Stripe async fn create_payment_intent( Json(payload): Json, ) -> Result, (StatusCode, ResponseJson)> { info!("Creating payment intent for company: {}", payload.company_name); // Validate required fields if !payload.final_agreement { return Err(( StatusCode::BAD_REQUEST, ResponseJson(ErrorResponse { error: "Final agreement must be accepted".to_string(), details: None, }), )); } // Calculate amount based on company type and payment plan let amount = match calculate_amount(&payload.company_type, &payload.payment_plan) { Ok(amount) => amount, Err(e) => { return Err(( StatusCode::BAD_REQUEST, ResponseJson(ErrorResponse { error: e, details: None, }), )); } }; // Get Stripe secret key from environment let stripe_secret_key = env::var("STRIPE_SECRET_KEY").map_err(|_| { error!("STRIPE_SECRET_KEY not found in environment"); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Server configuration error".to_string(), details: Some("Stripe not configured".to_string()), }), ) })?; // Create Stripe client let client = reqwest::Client::new(); // Prepare payment intent data let mut form_data = HashMap::new(); form_data.insert("amount", amount.to_string()); form_data.insert("currency", "usd".to_string()); form_data.insert("automatic_payment_methods[enabled]", "true".to_string()); // Add metadata form_data.insert("metadata[company_name]", payload.company_name.clone()); form_data.insert("metadata[company_type]", payload.company_type.clone()); form_data.insert("metadata[payment_plan]", payload.payment_plan.clone()); if let Some(email) = &payload.company_email { form_data.insert("metadata[company_email]", email.clone()); } // Add description let description = format!( "Company Registration: {} ({})", payload.company_name, payload.company_type ); form_data.insert("description", description); // Call Stripe API let response = client .post("https://api.stripe.com/v1/payment_intents") .header("Authorization", format!("Bearer {}", stripe_secret_key)) .form(&form_data) .send() .await .map_err(|e| { error!("Failed to call Stripe API: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Failed to create payment intent".to_string(), details: Some(e.to_string()), }), ) })?; if !response.status().is_success() { let error_text = response.text().await.unwrap_or_default(); error!("Stripe API error: {}", error_text); return Err(( StatusCode::BAD_REQUEST, ResponseJson(ErrorResponse { error: "Stripe payment intent creation failed".to_string(), details: Some(error_text), }), )); } let stripe_response: serde_json::Value = response.json().await.map_err(|e| { error!("Failed to parse Stripe response: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Invalid response from payment processor".to_string(), details: Some(e.to_string()), }), ) })?; let client_secret = stripe_response["client_secret"] .as_str() .ok_or_else(|| { error!("No client_secret in Stripe response"); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Invalid payment intent response".to_string(), details: None, }), ) })?; let payment_intent_id = stripe_response["id"] .as_str() .ok_or_else(|| { error!("No id in Stripe response"); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Invalid payment intent response".to_string(), details: None, }), ) })?; info!("Payment intent created successfully: {}", payment_intent_id); Ok(ResponseJson(CreatePaymentIntentResponse { client_secret: client_secret.to_string(), payment_intent_id: payment_intent_id.to_string(), })) } // Create payment intent for resident registration async fn create_resident_payment_intent( Json(payload): Json, ) -> Result, (StatusCode, ResponseJson)> { info!("Creating payment intent for resident: {}", payload.resident_name); // Convert amount from dollars to cents let amount_cents = (payload.amount * 100.0) as i64; // Get Stripe secret key from environment let stripe_secret_key = env::var("STRIPE_SECRET_KEY").map_err(|_| { error!("STRIPE_SECRET_KEY not found in environment"); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Server configuration error".to_string(), details: Some("Stripe not configured".to_string()), }), ) })?; // Create Stripe client let client = reqwest::Client::new(); // Prepare payment intent data let mut form_data = HashMap::new(); form_data.insert("amount", amount_cents.to_string()); form_data.insert("currency", "usd".to_string()); form_data.insert("automatic_payment_methods[enabled]", "true".to_string()); // Add metadata form_data.insert("metadata[resident_name]", payload.resident_name.clone()); form_data.insert("metadata[email]", payload.email.clone()); form_data.insert("metadata[payment_plan]", payload.payment_plan.clone()); form_data.insert("metadata[type]", payload.request_type.clone()); if let Some(phone) = &payload.phone { form_data.insert("metadata[phone]", phone.clone()); } if let Some(nationality) = &payload.nationality { form_data.insert("metadata[nationality]", nationality.clone()); } // Add description let description = format!( "Resident Registration: {} ({})", payload.resident_name, payload.payment_plan ); form_data.insert("description", description); // Call Stripe API let response = client .post("https://api.stripe.com/v1/payment_intents") .header("Authorization", format!("Bearer {}", stripe_secret_key)) .form(&form_data) .send() .await .map_err(|e| { error!("Failed to call Stripe API: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Failed to create payment intent".to_string(), details: Some(e.to_string()), }), ) })?; if !response.status().is_success() { let error_text = response.text().await.unwrap_or_default(); error!("Stripe API error: {}", error_text); return Err(( StatusCode::BAD_REQUEST, ResponseJson(ErrorResponse { error: "Stripe payment intent creation failed".to_string(), details: Some(error_text), }), )); } let stripe_response: serde_json::Value = response.json().await.map_err(|e| { error!("Failed to parse Stripe response: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Invalid response from payment processor".to_string(), details: Some(e.to_string()), }), ) })?; let client_secret = stripe_response["client_secret"] .as_str() .ok_or_else(|| { error!("No client_secret in Stripe response"); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Invalid payment intent response".to_string(), details: None, }), ) })?; let payment_intent_id = stripe_response["id"] .as_str() .ok_or_else(|| { error!("No id in Stripe response"); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Invalid payment intent response".to_string(), details: None, }), ) })?; info!("Resident payment intent created successfully: {}", payment_intent_id); Ok(ResponseJson(CreatePaymentIntentResponse { client_secret: client_secret.to_string(), payment_intent_id: payment_intent_id.to_string(), })) } // Handle Stripe webhooks async fn handle_webhook( headers: HeaderMap, body: String, ) -> Result)> { let stripe_signature = headers .get("stripe-signature") .and_then(|v| v.to_str().ok()) .ok_or_else(|| { warn!("Missing Stripe signature header"); ( StatusCode::BAD_REQUEST, ResponseJson(ErrorResponse { error: "Missing signature".to_string(), details: None, }), ) })?; let _webhook_secret = env::var("STRIPE_WEBHOOK_SECRET").map_err(|_| { error!("STRIPE_WEBHOOK_SECRET not found in environment"); ( StatusCode::INTERNAL_SERVER_ERROR, ResponseJson(ErrorResponse { error: "Webhook not configured".to_string(), details: None, }), ) })?; // In a real implementation, you would verify the webhook signature here // For now, we'll just log the event info!("Received webhook with signature: {}", stripe_signature); info!("Webhook body: {}", body); // Parse the webhook event let event: serde_json::Value = serde_json::from_str(&body).map_err(|e| { error!("Failed to parse webhook body: {}", e); ( StatusCode::BAD_REQUEST, ResponseJson(ErrorResponse { error: "Invalid webhook body".to_string(), details: Some(e.to_string()), }), ) })?; let event_type = event["type"].as_str().unwrap_or("unknown"); info!("Processing webhook event: {}", event_type); match event_type { "payment_intent.succeeded" => { let payment_intent = &event["data"]["object"]; let payment_intent_id = payment_intent["id"].as_str().unwrap_or("unknown"); info!("Payment succeeded: {}", payment_intent_id); // Here you would typically: // 1. Update your database to mark the company as registered // 2. Send confirmation emails // 3. Trigger any post-payment workflows } "payment_intent.payment_failed" => { let payment_intent = &event["data"]["object"]; let payment_intent_id = payment_intent["id"].as_str().unwrap_or("unknown"); warn!("Payment failed: {}", payment_intent_id); // Handle failed payment } _ => { info!("Unhandled webhook event type: {}", event_type); } } Ok(StatusCode::OK) } // Payment success redirect async fn payment_success(Query(params): Query) -> axum::response::Redirect { info!("Payment success page accessed"); if let Some(ref payment_intent_id) = params.payment_intent_id { info!("Payment intent ID: {}", payment_intent_id); // In a real implementation, you would: // 1. Verify the payment intent with Stripe // 2. Get the company ID from your database // 3. Redirect to the success page with the actual company ID // For now, we'll use a mock company ID (in real app, get from database) let company_id = 1; // This should be retrieved from your database based on payment_intent_id axum::response::Redirect::to(&format!("/entities/register/success/{}", company_id)) } else { // If no payment intent ID, redirect to entities page axum::response::Redirect::to("/entities") } } // Payment failure redirect async fn payment_failure() -> axum::response::Redirect { info!("Payment failure page accessed"); axum::response::Redirect::to("/entities/register/failure") } // Health check endpoint async fn health_check() -> ResponseJson { ResponseJson(serde_json::json!({ "status": "healthy", "timestamp": chrono::Utc::now().to_rfc3339(), "service": "freezone-platform-server" })) } #[tokio::main] async fn main() -> anyhow::Result<()> { // Load environment variables dotenv().ok(); // Initialize tracing tracing_subscriber::fmt::init(); // Check required environment variables let required_vars = ["STRIPE_SECRET_KEY", "STRIPE_PUBLISHABLE_KEY"]; for var in &required_vars { if env::var(var).is_err() { warn!("Environment variable {} not set", var); } } // Build the application router let app = Router::new() // API routes .route("/api/health", get(health_check)) .route("/company/create-payment-intent", post(create_payment_intent)) .route("/resident/create-payment-intent", post(create_resident_payment_intent)) .route("/company/payment-success", get(payment_success)) .route("/company/payment-failure", get(payment_failure)) .route("/webhooks/stripe", post(handle_webhook)) // Serve static files (WASM, HTML, CSS, JS) .nest_service("/", ServeDir::new(".")) // Add middleware .layer( ServiceBuilder::new() .layer( CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any), ) ); // Get server configuration from environment let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string()); let addr = format!("{}:{}", host, port); info!("Starting server on {}", addr); info!("Health check: http://{}/api/health", addr); info!("Payment endpoint: http://{}/company/create-payment-intent", addr); // Start the server let listener = tokio::net::TcpListener::bind(&addr).await?; axum::serve(listener, app).await?; Ok(()) }