use actix_web::{web, Result, Responder}; use actix_session::Session; use serde::{Deserialize, Serialize}; use crate::models::user::{User, Transaction, TransactionType, TransactionStatus, UserActivity, ActivityType, ActivityImportance}; use crate::config::get_app_config; use crate::services::user_persistence::{UserPersistentData, UserPersistence}; use crate::services::currency::CurrencyService; use crate::utils::render_template; use crate::utils::ResponseBuilder; use tera::Tera; use chrono::Utc; use rust_decimal::Decimal; use rust_decimal_macros::dec; /// Controller for handling wallet operations pub struct WalletController; impl WalletController { /// Check if user is authenticated, redirect to register if not fn check_authentication(session: &Session) -> Result<(), actix_web::HttpResponse> { match session.get::("user") { Ok(Some(_)) => Ok(()), _ => Err(ResponseBuilder::redirect("/register").build().unwrap()) } } } // New Credits request structs #[derive(Debug, Deserialize)] pub struct BuyCreditsRequest { pub amount: Decimal, pub payment_method: String, // "credit_card", "paypal", "bank_transfer" } #[derive(Debug, Deserialize)] pub struct SellCreditsRequest { pub amount: Decimal, pub currency: String, // "USD", "EUR", "GBP" pub payout_method: String, // "bank", "card" } #[derive(Debug, Deserialize)] pub struct TransferCreditsRequest { pub to_user: String, pub amount: Decimal, pub note: Option, } #[derive(Debug, Deserialize)] pub struct ConfigureAutoTopUpRequest { pub enabled: bool, pub threshold_amount: Decimal, pub topup_amount: Decimal, pub payment_method_id: String, pub daily_limit: Option, pub monthly_limit: Option, } #[derive(Debug, Serialize)] pub struct WalletResponse { pub success: bool, pub message: String, pub transaction_id: Option, pub new_balance: Option, } #[derive(Debug, Serialize)] pub struct WalletInfo { pub balance: Decimal, pub currency: String, pub recent_transactions: Vec, } impl WalletController { /// Enhanced user loading with persistent data only (no mock data) pub async fn load_user_with_persistent_data(session: &Session, req_id: Option<&str>) -> Option<(User, crate::services::user_persistence::UserPersistentData)> { // Get basic user data let user_json = session.get::("user").ok()??; let mut user: User = serde_json::from_str(&user_json).ok()?; // Get user email for loading persistent data let user_email = session.get::("user_email").ok()??; // Load persistent data (no mock data) let persistent_data = UserPersistence::load_user_data_locked(&user_email, req_id).await .unwrap_or_else(|| UserPersistentData { user_email: user_email.clone(), wallet_balance_usd: dec!(0), // New users start with 0 USD ..Default::default() }); // Using persistent data only - no mock data needed Some((user, persistent_data)) } /// Legacy function for backward compatibility - now uses persistent data pub async fn load_user_with_session_data(session: &Session, req_id: Option<&str>) -> Option { if let Some((user, _)) = Self::load_user_with_persistent_data(session, req_id).await { Some(user) } else { None } } /// Credit USD to a recipient user (for transfers) async fn credit_recipient(recipient_email: &str, sender_email: &str, amount: Decimal, transaction_id: &str, req_id: Option<&str>) -> Result<(), Box> { use crate::services::user_persistence::UserPersistence; use chrono::Utc; // Load existing recipient's persistent data (do NOT auto-create) let mut recipient_data = match UserPersistence::load_user_data_locked(recipient_email, req_id).await { Some(data) => data, None => { // Recipient does not exist in marketplace; reject transfer return Err("Recipient user not found".into()); } }; // Add the received amount recipient_data.wallet_balance_usd += amount; // Create a receive transaction let receive_transaction = Transaction { id: format!("{}-receive", transaction_id), user_id: recipient_email.to_string(), transaction_type: TransactionType::Earning { source: "Transfer Received".to_string() }, amount, currency: Some("USD".to_string()), exchange_rate_usd: Some(rust_decimal::Decimal::ONE), amount_usd: Some(amount), description: Some(format!("Transfer received from {}", sender_email)), reference_id: Some(format!("transfer-receive-{}", uuid::Uuid::new_v4())), metadata: None, timestamp: Utc::now(), status: TransactionStatus::Completed, }; // Add transaction to history recipient_data.transactions.push(receive_transaction); // Save recipient's updated data UserPersistence::save_user_data_locked(&recipient_data, req_id).await?; Ok(()) } /// Render the wallet page within dashboard layout pub async fn dashboard_wallet_page(tmpl: web::Data, session: Session) -> Result { let mut ctx = crate::models::builders::ContextBuilder::new() .build(); // Check if user is logged in if session.get::("user").unwrap_or(None).is_none() { // User is not logged in, show welcome page with login/register options ctx.insert("active_page", "dashboard"); ctx.insert("active_section", "wallet"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); return render_template(&tmpl, "dashboard/welcome.html", &ctx); } ctx.insert("active_page", "dashboard"); ctx.insert("active_section", "wallet"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Load user with persistent data only (no mock data) let req_id = uuid::Uuid::new_v4().to_string(); if let Some((user, persistent_data)) = Self::load_user_with_persistent_data(&session, Some(&req_id)).await { if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); } ctx.insert("user", &user); // Currency handling: determine user's preferred currency and convert amounts for display let currency_service = CurrencyService::new(); let mut display_currency = currency_service.get_user_preferred_currency(&session); // Validate currency; fallback to USD if unsupported let (currency, effective_currency) = match currency_service.get_currency(&display_currency) { Some(c) => (c, display_currency.clone()), None => { let usd = currency_service.get_currency("USD").expect("USD currency must be available"); display_currency = "USD".to_string(); (usd, "USD".to_string()) } }; // Convert wallet balance (stored in USD) to display currency let wallet_balance_display = if effective_currency == "USD" { persistent_data.wallet_balance_usd } else { currency_service .convert_amount(persistent_data.wallet_balance_usd, "USD", &effective_currency) .unwrap_or(Decimal::ZERO) }; ctx.insert("wallet_balance", &wallet_balance_display); ctx.insert("display_currency", &effective_currency); ctx.insert("currency_symbol", ¤cy.symbol); // Extract owned products and active rentals from persistent data ctx.insert("owned_products", &persistent_data.active_product_rentals); ctx.insert("active_rentals", &persistent_data.node_rentals); // Extract and format transaction history from persistent data let mut sorted_transactions = persistent_data.transactions.clone(); sorted_transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); let formatted_transactions: Vec = sorted_transactions .iter() .map(|t| { // Convert transaction amount (stored in USD) to display currency let amount_display = if effective_currency == "USD" { t.amount } else { currency_service .convert_amount(t.amount, "USD", &effective_currency) .unwrap_or(Decimal::ZERO) }; serde_json::json!({ "id": t.id, "user_id": t.user_id, "transaction_type": t.transaction_type, "amount": amount_display, "status": t.status, "formatted_timestamp": t.timestamp.format("%Y-%m-%d %H:%M").to_string(), "timestamp": t.timestamp.to_rfc3339() }) }) .collect(); ctx.insert("transaction_history", &formatted_transactions); } render_template(&tmpl, "dashboard/wallet.html", &ctx) } /// Render the wallet page (legacy standalone route - kept for compatibility) pub async fn wallet_page(tmpl: web::Data, session: Session) -> Result { let mut ctx = crate::models::builders::ContextBuilder::new() .build(); ctx.insert("active_page", "wallet"); let config = get_app_config(); ctx.insert("gitea_enabled", &config.is_gitea_enabled()); // Load user with persistent data only (no mock data) let req_id = uuid::Uuid::new_v4().to_string(); if let Some((user, persistent_data)) = Self::load_user_with_persistent_data(&session, Some(&req_id)).await { if let Ok(Some(user_json)) = session.get::("user") { ctx.insert("user_json", &user_json); } ctx.insert("user", &user); // Determine preferred currency and convert values for display let currency_service = CurrencyService::new(); let mut display_currency = currency_service.get_user_preferred_currency(&session); let (currency, effective_currency) = match currency_service.get_currency(&display_currency) { Some(c) => (c, display_currency.clone()), None => { let usd = currency_service.get_currency("USD").expect("USD currency must be available"); display_currency = "USD".to_string(); (usd, "USD".to_string()) } }; let wallet_balance_display = if effective_currency == "USD" { persistent_data.wallet_balance_usd } else { currency_service .convert_amount(persistent_data.wallet_balance_usd, "USD", &effective_currency) .unwrap_or(Decimal::ZERO) }; ctx.insert("wallet_balance", &wallet_balance_display); ctx.insert("display_currency", &effective_currency); ctx.insert("currency_symbol", ¤cy.symbol); // Extract owned products and active rentals from persistent data ctx.insert("owned_products", &persistent_data.active_product_rentals); ctx.insert("active_rentals", &persistent_data.node_rentals); // Extract and format transaction history from persistent data let mut sorted_transactions = persistent_data.transactions.clone(); sorted_transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); let formatted_transactions: Vec = sorted_transactions .iter() .map(|t| { let amount_display = if effective_currency == "USD" { t.amount } else { currency_service .convert_amount(t.amount, "USD", &effective_currency) .unwrap_or(Decimal::ZERO) }; serde_json::json!({ "id": t.id, "user_id": t.user_id, "transaction_type": t.transaction_type, "amount": amount_display, "status": t.status, "formatted_timestamp": t.timestamp.format("%Y-%m-%d %H:%M").to_string(), "timestamp": t.timestamp.to_rfc3339() }) }) .collect(); ctx.insert("transaction_history", &formatted_transactions); } render_template(&tmpl, "wallet/index.html", &ctx) } // NEW CREDITS API METHODS /// Buy Credits (USD-based) pub async fn buy_credits( request: web::Json, session: Session, ) -> Result { // Load user with session data let req_id = uuid::Uuid::new_v4().to_string(); let _user = match Self::load_user_with_session_data(&session, Some(&req_id)).await { Some(u) => u, None => return ResponseBuilder::unauthorized() .json(WalletResponse { success: false, message: "User not authenticated".to_string(), transaction_id: None, new_balance: None, }) .build(), }; let user_email = session.get::("user_email")?.unwrap(); // Validate amount if request.amount <= dec!(0) { return ResponseBuilder::bad_request() .json(WalletResponse { success: false, message: "Amount must be greater than 0".to_string(), transaction_id: None, new_balance: None, }) .build(); } if request.amount > dec!(10000) { return ResponseBuilder::bad_request() .json(WalletResponse { success: false, message: "Maximum purchase amount is $10,000".to_string(), transaction_id: None, new_balance: None, }) .build(); } // Create transaction let transaction_id = uuid::Uuid::new_v4().to_string(); let transaction = Transaction { id: transaction_id.clone(), user_id: user_email.clone(), transaction_type: TransactionType::Purchase { product_id: "credits".to_string(), }, amount: request.amount, currency: Some("USD".to_string()), exchange_rate_usd: Some(rust_decimal::Decimal::ONE), amount_usd: Some(request.amount), description: Some(format!("Credits purchase via {}", request.payment_method)), reference_id: Some(transaction_id.clone()), metadata: None, timestamp: Utc::now(), status: TransactionStatus::Completed, }; // Load and update persistent data (locked) let mut persistent_data = crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await .unwrap_or_else(|| crate::services::user_persistence::UserPersistentData { user_email: user_email.clone(), wallet_balance_usd: dec!(0), ..Default::default() }); // Add balance to persistent data persistent_data.wallet_balance_usd += request.amount; // Add transaction to persistent data persistent_data.transactions.push(transaction.clone()); // Add user activity to persistent data persistent_data.user_activities.insert(0, UserActivity { id: uuid::Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: ActivityType::WalletTransaction, description: format!("Purchased ${} credits via {}", request.amount, request.payment_method), timestamp: Utc::now(), metadata: None, category: "Wallet".to_string(), importance: ActivityImportance::Medium, ip_address: None, user_agent: None, session_id: None, }); // Keep only last 50 activities if persistent_data.user_activities.len() > 50 { persistent_data.user_activities.truncate(50); } // Update user preferences to remember the last payment method if let Some(ref mut preferences) = persistent_data.user_preferences { preferences.last_payment_method = Some(request.payment_method.clone()); } else { // Create default preferences with the payment method persistent_data.user_preferences = Some(crate::models::user::UserPreferences { preferred_currency: "USD".to_string(), preferred_language: "en".to_string(), timezone: "UTC".to_string(), dashboard_layout: "default".to_string(), notification_settings: Some(crate::models::user::NotificationSettings { email_enabled: true, push_enabled: true, sms_enabled: false, slack_webhook: None, discord_webhook: None, enabled: true, push: true, node_offline_alerts: true, maintenance_reminders: true, earnings_reports: true, }), privacy_settings: Some(crate::models::user::PrivacySettings { profile_public: false, email_public: false, activity_public: false, stats_public: false, profile_visibility: "private".to_string(), marketing_emails: false, data_sharing: false, activity_tracking: true, }), theme: "light".to_string(), last_payment_method: Some(request.payment_method.clone()), currency_display: "symbol".to_string(), data_sharing: false, email_notifications: true, ..Default::default() }); } // Save persistent data (locked) if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data_locked(&persistent_data, Some(&req_id)).await { return ResponseBuilder::internal_error() .json(WalletResponse { success: false, message: "Failed to create transaction".to_string(), transaction_id: None, new_balance: None, }) .build(); } let new_balance = persistent_data.wallet_balance_usd; ResponseBuilder::ok() .json(WalletResponse { success: true, message: "Funds added successfully".to_string(), transaction_id: Some(transaction_id), new_balance: Some(new_balance), }) .build() } /// Sell Credits (USD-based) pub async fn sell_credits( request: web::Json, session: Session, ) -> Result { let req_id = uuid::Uuid::new_v4().to_string(); // Get user email from session let user_email = match session.get::("user_email")? { Some(email) => email, None => return ResponseBuilder::unauthorized() .json(WalletResponse { success: false, message: "User not authenticated".to_string(), transaction_id: None, new_balance: None, }) .build(), }; // Load persistent data (locked) let mut persistent_data = match crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await { Some(data) => data, None => return ResponseBuilder::unauthorized() .json(WalletResponse { success: false, message: "User data not found".to_string(), transaction_id: None, new_balance: None, }) .build(), }; // Validate amount if request.amount <= dec!(0) { return ResponseBuilder::bad_request() .json(WalletResponse { success: false, message: "Amount must be greater than 0".to_string(), transaction_id: None, new_balance: None, }) .build(); } // Check if user has sufficient balance (unified insufficient funds contract) if persistent_data.wallet_balance_usd < request.amount { let required = request.amount; let available = persistent_data.wallet_balance_usd; let deficit = required - available; return crate::utils::response_builder::ResponseBuilder::payment_required() .error_envelope( "INSUFFICIENT_FUNDS", "Insufficient balance", serde_json::json!({ "currency": "USD", "wallet_balance_usd": available, "required_usd": required, "deficit_usd": deficit }) ) .build(); } // Calculate exchange amount (Credits are 1:1 with USD) let exchange_amount = request.amount; // Create transaction let transaction_id = uuid::Uuid::new_v4().to_string(); let transaction = Transaction { id: transaction_id.clone(), user_id: user_email.clone(), transaction_type: TransactionType::CreditsSale { amount_usd: request.amount, rate: if exchange_amount != rust_decimal::Decimal::ZERO { request.amount / exchange_amount } else { rust_decimal::Decimal::ONE }, }, amount: -request.amount, currency: Some("USD".to_string()), exchange_rate_usd: Some(rust_decimal::Decimal::ONE), amount_usd: Some(request.amount), description: Some(format!("Credits sale to {} via {}", request.currency, request.payout_method)), reference_id: Some(format!("sale-{}", uuid::Uuid::new_v4())), metadata: None, timestamp: Utc::now(), status: TransactionStatus::Completed, }; // Update persistent data persistent_data.wallet_balance_usd -= request.amount; persistent_data.transactions.push(transaction.clone()); // Add user activity persistent_data.user_activities.insert(0, UserActivity { id: uuid::Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: ActivityType::WalletTransaction, description: format!("Sold ${} credits for {} {} via {}", request.amount, exchange_amount, request.currency, request.payout_method), timestamp: Utc::now(), metadata: None, category: "Wallet".to_string(), importance: ActivityImportance::Medium, ip_address: None, user_agent: None, session_id: None, }); // Keep only last 50 activities if persistent_data.user_activities.len() > 50 { persistent_data.user_activities.truncate(50); } // Save persistent data (locked) if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data_locked(&persistent_data, Some(&req_id)).await { return ResponseBuilder::internal_error() .json(WalletResponse { success: false, message: "Failed to create transaction".to_string(), transaction_id: None, new_balance: None, }) .build(); } let new_balance = persistent_data.wallet_balance_usd; ResponseBuilder::ok() .json(WalletResponse { success: true, message: format!("Successfully sold ${} credits for {:.2} {}", request.amount, exchange_amount, request.currency), transaction_id: Some(transaction_id), new_balance: Some(new_balance), }) .build() } /// Transfer Credits (USD-based) pub async fn transfer_credits( request: web::Json, session: Session, ) -> Result { let req_id = uuid::Uuid::new_v4().to_string(); // Get user email from session let user_email = match session.get::("user_email")? { Some(email) => email, None => return ResponseBuilder::unauthorized() .json(WalletResponse { success: false, message: "User not authenticated".to_string(), transaction_id: None, new_balance: None, }) .build(), }; // Load persistent data (locked) let mut persistent_data = match crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await { Some(data) => data, None => return ResponseBuilder::unauthorized() .json(WalletResponse { success: false, message: "User data not found".to_string(), transaction_id: None, new_balance: None, }) .build(), }; // Validate amount if request.amount <= dec!(0) { return ResponseBuilder::bad_request() .json(WalletResponse { success: false, message: "Amount must be greater than 0".to_string(), transaction_id: None, new_balance: None, }) .build(); } // Check if user has sufficient balance (unified insufficient funds contract) if persistent_data.wallet_balance_usd < request.amount { let required = request.amount; let available = persistent_data.wallet_balance_usd; let deficit = required - available; return crate::utils::response_builder::ResponseBuilder::payment_required() .error_envelope( "INSUFFICIENT_FUNDS", "Insufficient balance", serde_json::json!({ "currency": "USD", "wallet_balance_usd": available, "required_usd": required, "deficit_usd": deficit }) ) .build(); } // Check if transferring to self (structured 400) if user_email == request.to_user { return crate::utils::response_builder::ResponseBuilder::bad_request() .error_envelope( "INVALID_RECIPIENT_SELF", "Cannot transfer to self", serde_json::json!({ "to_user": request.to_user }) ) .build(); } // Validate recipient exists before proceeding (no auto-account creation) if crate::services::user_persistence::UserPersistence::load_user_data_locked(&request.to_user, Some(&req_id)).await.is_none() { return crate::utils::response_builder::ResponseBuilder::not_found() .error_envelope( "RECIPIENT_NOT_FOUND", "Recipient user not found", serde_json::json!({ "to_user": request.to_user }) ) .build(); } // Create transaction let transaction_id = uuid::Uuid::new_v4().to_string(); let transaction = Transaction { id: transaction_id.clone(), user_id: user_email.clone(), transaction_type: TransactionType::CreditsTransfer { to_user: request.to_user.clone(), note: request.note.clone(), }, amount: request.amount, currency: Some("USD".to_string()), exchange_rate_usd: Some(rust_decimal::Decimal::ONE), amount_usd: Some(request.amount), description: Some(format!("Credits transfer to {}: {}", request.to_user, request.note.as_deref().unwrap_or("No note"))), reference_id: Some(format!("transfer-{}", uuid::Uuid::new_v4())), metadata: None, timestamp: Utc::now(), status: TransactionStatus::Completed, }; // Update sender's persistent data persistent_data.wallet_balance_usd -= request.amount; persistent_data.transactions.push(transaction.clone()); // Add user activity let note_text = request.note.as_ref() .map(|n| format!(" ({})", n)) .unwrap_or_default(); persistent_data.user_activities.insert(0, UserActivity { id: uuid::Uuid::new_v4().to_string(), user_email: user_email.clone(), activity_type: ActivityType::WalletTransaction, description: format!("Transferred ${} credits to {}{}", request.amount, request.to_user, note_text), timestamp: Utc::now(), metadata: None, category: "Wallet".to_string(), importance: ActivityImportance::Medium, ip_address: None, user_agent: None, session_id: None, }); // Keep only last 50 activities if persistent_data.user_activities.len() > 50 { persistent_data.user_activities.truncate(50); } // Save sender's persistent data (locked) if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data_locked(&persistent_data, Some(&req_id)).await { return ResponseBuilder::internal_error() .json(WalletResponse { success: false, message: "Failed to create transaction".to_string(), transaction_id: None, new_balance: None, }) .build(); } let new_balance = persistent_data.wallet_balance_usd; // Credit the recipient if let Err(e) = Self::credit_recipient(&request.to_user, &user_email, request.amount, &transaction_id, Some(&req_id)).await { return ResponseBuilder::internal_error() .json(WalletResponse { success: false, message: "Failed to save recipient data".to_string(), transaction_id: None, new_balance: None, }) .build(); } ResponseBuilder::ok() .json(WalletResponse { success: true, message: format!("Successfully transferred ${} to {}", request.amount, request.to_user), transaction_id: Some(transaction_id), new_balance: Some(new_balance), }) .build() } /// Get wallet balance pub async fn get_balance(session: Session) -> Result { // Get user email from session let user_email = match session.get::("user_email")? { Some(email) => email, None => { return ResponseBuilder::unauthorized() .json(serde_json::json!({ "error": "User not authenticated" })) .build(); } }; // Load persistent data (locked) let req_id = uuid::Uuid::new_v4().to_string(); let persistent_data = match crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await { Some(data) => data, None => { return ResponseBuilder::unauthorized() .json(serde_json::json!({ "error": "User data not found" })) .build(); } }; let balance = persistent_data.wallet_balance_usd; ResponseBuilder::ok() .json(serde_json::json!({ "balance": balance, "currency": "USD" })) .build() } /// Get wallet information including balance and recent transactions pub async fn get_wallet_info(session: Session) -> Result { // Get user email from session let user_email = match session.get::("user_email")? { Some(email) => email, None => { return ResponseBuilder::unauthorized() .json(serde_json::json!({ "error": "User not authenticated" })) .build(); } }; // Load persistent data (locked) let req_id = uuid::Uuid::new_v4().to_string(); let persistent_data = match crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await { Some(data) => data, None => { return ResponseBuilder::unauthorized() .json(serde_json::json!({ "error": "User data not found" })) .build(); } }; let balance = persistent_data.wallet_balance_usd; let mut transactions = persistent_data.transactions.clone(); // Sort transactions by timestamp (newest first) and take last 10 transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); transactions.truncate(10); let wallet_info = WalletInfo { balance, currency: "USD".to_string(), recent_transactions: transactions, }; ResponseBuilder::ok().json(wallet_info).build() } /// Get transaction history pub async fn get_transactions(session: Session) -> Result { // Get user email from session let user_email = match session.get::("user_email")? { Some(email) => email, None => { return ResponseBuilder::unauthorized() .json(serde_json::json!({ "error": "User not authenticated" })) .build(); } }; // Load persistent data (locked) let req_id = uuid::Uuid::new_v4().to_string(); let persistent_data = match crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await { Some(data) => data, None => { return ResponseBuilder::unauthorized() .json(serde_json::json!({ "error": "User data not found" })) .build(); } }; let mut transactions = persistent_data.transactions.clone(); // Sort transactions by timestamp (newest first) transactions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); // Format transactions for consistent API response let formatted_transactions: Vec = transactions.iter() .map(|t| { serde_json::json!({ "id": t.id, "user_id": t.user_id, "transaction_type": t.transaction_type, "amount": t.amount, "status": t.status, "formatted_timestamp": t.timestamp.format("%Y-%m-%d %H:%M").to_string(), "timestamp": t.timestamp.to_rfc3339() }) }) .collect(); ResponseBuilder::ok().json(formatted_transactions).build() } /// Execute instant purchase (buy-now functionality) pub async fn instant_purchase( request: web::Json, session: Session, ) -> Result { let instant_purchase_service = crate::services::instant_purchase::InstantPurchaseService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; match instant_purchase_service.execute_instant_purchase(&session, request.into_inner()).await { Ok(response) => ResponseBuilder::ok().json(response).build(), Err(error) => ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": error })).build(), } } /// Execute quick wallet top-up pub async fn quick_topup( request: web::Json, session: Session, ) -> Result { let instant_purchase_service = crate::services::instant_purchase::InstantPurchaseService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; match instant_purchase_service.execute_quick_topup(&session, request.into_inner()).await { Ok(response) => ResponseBuilder::ok().json(response).build(), Err(error) => ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": error })).build(), } } /// Get navbar dropdown data pub async fn get_navbar_data(session: Session) -> Result { let navbar_service = crate::services::navbar::NavbarService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Check if user is authenticated if session.get::("user_email").unwrap_or(None).is_some() { match navbar_service.get_dropdown_data(&session) { Ok(data) => { // The navbar service already includes currency information ResponseBuilder::ok().json(data).build() }, Err(error) => ResponseBuilder::bad_request().json(serde_json::json!({ "error": error })).build(), } } else { // Return guest data let guest_data = crate::services::navbar::NavbarService::get_guest_data(); ResponseBuilder::ok().json(guest_data).build() } } /// Check if user can afford a purchase pub async fn check_affordability( query: web::Query>, session: Session, ) -> Result { let amount_str = query.get("amount").ok_or_else(|| { actix_web::error::ErrorBadRequest("Missing amount parameter") })?; let amount = amount_str.parse::() .map_err(|_| actix_web::error::ErrorBadRequest("Invalid amount format"))?; let instant_purchase_service = crate::services::instant_purchase::InstantPurchaseService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; match instant_purchase_service.check_affordability(&session, amount) { Ok(can_afford) => { if can_afford { ResponseBuilder::ok().json(serde_json::json!({ "can_afford": true })).build() } else { // Get shortfall information match instant_purchase_service.get_balance_shortfall(&session, amount) { Ok(Some(shortfall_info)) => ResponseBuilder::ok().json(serde_json::json!({ "can_afford": false, "shortfall_info": shortfall_info })).build(), Ok(None) => ResponseBuilder::ok().json(serde_json::json!({ "can_afford": true })).build(), Err(error) => ResponseBuilder::bad_request().json(serde_json::json!({ "error": error })).build(), } } }, Err(error) => ResponseBuilder::bad_request().json(serde_json::json!({ "error": error })).build(), } } /// Get quick top-up amounts for user's preferred currency pub async fn get_quick_topup_amounts(session: Session) -> Result { let navbar_service = crate::services::navbar::NavbarService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; match navbar_service.get_quick_topup_amounts(&session) { Ok(amounts) => ResponseBuilder::ok().json(serde_json::json!({ "amounts": amounts })).build(), Err(error) => ResponseBuilder::bad_request().json(serde_json::json!({ "error": error })).build(), } } /// Configure auto top-up settings pub async fn configure_auto_topup( request: actix_web::web::Json, session: Session, ) -> Result { let auto_topup_service = crate::services::auto_topup::AutoTopUpService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; // Convert the DTO to AutoTopUpSettings with proper DateTime fields let settings = crate::services::user_persistence::AutoTopUpSettings { enabled: request.enabled, threshold_amount_usd: request.threshold_amount, topup_amount_usd: request.topup_amount, payment_method_id: request.payment_method_id.clone(), daily_limit_usd: request.daily_limit, monthly_limit_usd: request.monthly_limit, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), }; match auto_topup_service.configure_auto_topup(&session, settings) { Ok(()) => ResponseBuilder::ok().json(serde_json::json!({ "success": true, "message": "Auto top-up configured successfully" })).build(), Err(e) => ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": e })).build(), } } /// Get auto top-up status pub async fn get_auto_topup_status(session: Session) -> Result { let user_email = session.get::("user_email") .map_err(|e| actix_web::error::ErrorInternalServerError(e))? .ok_or_else(|| actix_web::error::ErrorUnauthorized("User not authenticated"))?; let req_id = uuid::Uuid::new_v4().to_string(); let persistent_data = crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await .unwrap_or_default(); let auto_topup_enabled = persistent_data.auto_topup_settings .as_ref() .map(|s| s.enabled) .unwrap_or(false); ResponseBuilder::ok().json(serde_json::json!({ "enabled": auto_topup_enabled, "settings": persistent_data.auto_topup_settings })).build() } /// Trigger auto top-up pub async fn trigger_auto_topup( request: actix_web::web::Json, session: Session, ) -> Result { let auto_topup_service = crate::services::auto_topup::AutoTopUpService::builder() .build() .map_err(|e| actix_web::error::ErrorInternalServerError(e))?; let required_amount = request.get("required_amount") .and_then(|v| v.as_f64()) .map(|f| rust_decimal::Decimal::from_f64_retain(f).unwrap_or_default()) .unwrap_or_default(); match auto_topup_service.check_and_trigger_topup(&session, required_amount).await { Ok(success) => ResponseBuilder::ok().json(serde_json::json!({ "success": success, "message": if success { "Auto top-up completed" } else { "Auto top-up failed" } })).build(), Err(e) => ResponseBuilder::bad_request().json(serde_json::json!({ "success": false, "message": e })).build(), } } /// Get the last used payment method for pre-filling buy credits form pub async fn get_last_payment_method( session: Session, ) -> Result { // Check authentication if let Err(response) = Self::check_authentication(&session) { return Ok(response); } let user_email = session.get::("user_email")?.unwrap(); // Load user persistent data (locked) let req_id = uuid::Uuid::new_v4().to_string(); let persistent_data = crate::services::user_persistence::UserPersistence::load_user_data_locked(&user_email, Some(&req_id)).await; let last_payment_method = persistent_data .and_then(|data| data.user_preferences) .and_then(|prefs| prefs.last_payment_method); ResponseBuilder::ok().json(serde_json::json!({ "success": true, "last_payment_method": last_payment_method })).build() } }