freezone/platform/src/bin/server.rs
2025-06-27 04:13:31 +02:00

526 lines
18 KiB
Rust

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<String>,
company_phone: Option<String>,
company_website: Option<String>,
company_address: Option<String>,
company_industry: Option<String>,
company_purpose: Option<String>,
fiscal_year_end: Option<String>,
shareholders: Option<String>,
payment_plan: String,
agreements: Vec<String>,
final_agreement: bool,
}
#[derive(Debug, Deserialize)]
struct CreateResidentPaymentIntentRequest {
resident_name: String,
email: String,
phone: Option<String>,
date_of_birth: Option<String>,
nationality: Option<String>,
passport_number: Option<String>,
address: Option<String>,
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<String>,
}
#[derive(Debug, Deserialize)]
struct WebhookQuery {
#[serde(rename = "payment_intent")]
payment_intent_id: Option<String>,
#[serde(rename = "payment_intent_client_secret")]
client_secret: Option<String>,
}
// Calculate pricing based on company type and payment plan
fn calculate_amount(company_type: &str, payment_plan: &str) -> Result<i64, String> {
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<CreatePaymentIntentRequest>,
) -> Result<ResponseJson<CreatePaymentIntentResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
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<CreateResidentPaymentIntentRequest>,
) -> Result<ResponseJson<CreatePaymentIntentResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
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<StatusCode, (StatusCode, ResponseJson<ErrorResponse>)> {
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<WebhookQuery>) -> 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<serde_json::Value> {
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(())
}