This repository has been archived on 2025-12-01. You can view files and clone it, but cannot push or open issues or pull requests.
Files
projectmycelium_old/src/controllers/wallet.rs

1143 lines
46 KiB
Rust

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::<String>("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<String>,
}
#[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<Decimal>,
pub monthly_limit: Option<Decimal>,
}
#[derive(Debug, Serialize)]
pub struct WalletResponse {
pub success: bool,
pub message: String,
pub transaction_id: Option<String>,
pub new_balance: Option<Decimal>,
}
#[derive(Debug, Serialize)]
pub struct WalletInfo {
pub balance: Decimal,
pub currency: String,
pub recent_transactions: Vec<Transaction>,
}
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::<String>("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::<String>("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<User> {
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<dyn std::error::Error>> {
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<Tera>, session: Session) -> Result<impl Responder> {
let mut ctx = crate::models::builders::ContextBuilder::new()
.build();
// Check if user is logged in
if session.get::<String>("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::<String>("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 mc = currency_service.get_currency("MC").expect("MC currency must be available");
display_currency = "MC".to_string();
(mc, "MC".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", &currency.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<serde_json::Value> = 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<Tera>, session: Session) -> Result<impl Responder> {
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::<String>("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 mc = currency_service.get_currency("MC").expect("MC currency must be available");
display_currency = "MC".to_string();
(mc, "MC".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", &currency.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<serde_json::Value> = 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<BuyCreditsRequest>,
session: Session,
) -> Result<impl Responder> {
// 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::<String>("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<SellCreditsRequest>,
session: Session,
) -> Result<impl Responder> {
let req_id = uuid::Uuid::new_v4().to_string();
// Get user email from session
let user_email = match session.get::<String>("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<TransferCreditsRequest>,
session: Session,
) -> Result<impl Responder> {
let req_id = uuid::Uuid::new_v4().to_string();
// Get user email from session
let user_email = match session.get::<String>("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<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("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<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("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<impl Responder> {
// Get user email from session
let user_email = match session.get::<String>("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<serde_json::Value> = 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<crate::services::instant_purchase::InstantPurchaseRequest>,
session: Session,
) -> Result<impl Responder> {
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<crate::services::instant_purchase::QuickTopupRequest>,
session: Session,
) -> Result<impl Responder> {
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<impl Responder> {
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::<String>("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<std::collections::HashMap<String, String>>,
session: Session,
) -> Result<impl Responder> {
let amount_str = query.get("amount").ok_or_else(|| {
actix_web::error::ErrorBadRequest("Missing amount parameter")
})?;
let amount = amount_str.parse::<rust_decimal::Decimal>()
.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<impl Responder> {
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<ConfigureAutoTopUpRequest>,
session: Session,
) -> Result<impl Responder> {
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<impl Responder> {
let user_email = session.get::<String>("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<serde_json::Value>,
session: Session,
) -> Result<impl Responder> {
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<impl Responder> {
// Check authentication
if let Err(response) = Self::check_authentication(&session) {
return Ok(response);
}
let user_email = session.get::<String>("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()
}
}