initial commit
This commit is contained in:
526
platform/src/bin/server.rs
Normal file
526
platform/src/bin/server.rs
Normal file
@@ -0,0 +1,526 @@
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user