1242 lines
47 KiB
Rust
1242 lines
47 KiB
Rust
use crate::models::order::{Order, OrderItem, OrderStatus, Cart, CartItem, PaymentDetails, PaymentMethod};
|
|
use crate::models::product::Product;
|
|
use crate::services::{currency::CurrencyService, product::ProductService, user_persistence::UserPersistence};
|
|
use rust_decimal::Decimal;
|
|
use rust_decimal::prelude::ToPrimitive;
|
|
use rust_decimal_macros::dec;
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
use chrono::Utc;
|
|
use uuid::Uuid;
|
|
use actix_session::Session;
|
|
use serde::Serialize;
|
|
|
|
/// Service for handling order and cart operations
|
|
pub struct OrderService {
|
|
currency_service: CurrencyService,
|
|
product_service: ProductService,
|
|
}
|
|
|
|
/// Singleton order storage for demo purposes
|
|
pub struct OrderStorage {
|
|
orders: HashMap<String, Order>,
|
|
}
|
|
|
|
impl OrderStorage {
|
|
fn new() -> Self {
|
|
Self {
|
|
orders: HashMap::default(),
|
|
}
|
|
}
|
|
|
|
/// Get singleton instance of order storage
|
|
pub fn instance() -> Arc<Mutex<OrderStorage>> {
|
|
use std::sync::Once;
|
|
static mut INSTANCE: Option<Arc<Mutex<OrderStorage>>> = None;
|
|
static ONCE: Once = Once::new();
|
|
|
|
unsafe {
|
|
ONCE.call_once(|| {
|
|
INSTANCE = Some(Arc::new(Mutex::new(OrderStorage::new())));
|
|
});
|
|
INSTANCE.as_ref().unwrap().clone()
|
|
}
|
|
}
|
|
|
|
pub fn insert_order(&mut self, order_id: String, order: Order) {
|
|
self.orders.insert(order_id, order);
|
|
}
|
|
|
|
pub fn get_order(&self, order_id: &str) -> Option<&Order> {
|
|
self.orders.get(order_id)
|
|
}
|
|
|
|
pub fn get_user_orders(&self, user_id: &str) -> Vec<&Order> {
|
|
self.orders.values()
|
|
.filter(|order| order.user_id == user_id)
|
|
.collect()
|
|
}
|
|
|
|
pub fn update_order_status(&mut self, order_id: &str, status: OrderStatus) -> Result<(), String> {
|
|
if let Some(order) = self.orders.get_mut(order_id) {
|
|
order.update_status(status);
|
|
Ok(())
|
|
} else {
|
|
Err("Order not found".to_string())
|
|
}
|
|
}
|
|
|
|
pub fn get_order_mut(&mut self, order_id: &str) -> Option<&mut Order> {
|
|
self.orders.get_mut(order_id)
|
|
}
|
|
|
|
pub fn get_orders_count(&self) -> usize {
|
|
self.orders.len()
|
|
}
|
|
|
|
pub fn get_all_orders(&self) -> Vec<&Order> {
|
|
self.orders.values().collect()
|
|
}
|
|
}
|
|
|
|
/// Mock payment gateway for simulating payment processing
|
|
pub struct MockPaymentGateway {
|
|
pub success_rate: f32, // Configurable for testing
|
|
}
|
|
|
|
/// Payment request structure
|
|
#[derive(Debug, Clone)]
|
|
pub struct PaymentRequest {
|
|
pub order_id: String,
|
|
pub amount: Decimal,
|
|
pub currency: String,
|
|
pub payment_method: PaymentMethod,
|
|
pub user_id: String,
|
|
}
|
|
|
|
/// Payment result
|
|
#[derive(Debug, Clone)]
|
|
pub struct PaymentResult {
|
|
pub success: bool,
|
|
pub transaction_id: Option<String>,
|
|
pub error_message: Option<String>,
|
|
pub payment_details: Option<PaymentDetails>,
|
|
}
|
|
|
|
impl OrderService {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
currency_service: CurrencyService::new(),
|
|
product_service: ProductService::new(),
|
|
}
|
|
}
|
|
|
|
pub fn new_with_config(
|
|
currency_service: CurrencyService,
|
|
product_service: ProductService,
|
|
_auto_save: bool,
|
|
) -> Self {
|
|
Self {
|
|
currency_service,
|
|
product_service,
|
|
}
|
|
}
|
|
|
|
pub fn builder() -> crate::models::builders::OrderServiceBuilder {
|
|
crate::models::builders::OrderServiceBuilder::new()
|
|
}
|
|
|
|
// Cart Operations
|
|
|
|
/// Get or create cart for user from session
|
|
pub fn get_or_create_cart(&self, user_id: &str, session: &Session) -> Cart {
|
|
let cart_key = if user_id.is_empty() {
|
|
"guest_cart"
|
|
} else {
|
|
"user_cart"
|
|
};
|
|
|
|
// For logged-in users, ALWAYS load from persistent storage first
|
|
if !user_id.is_empty() {
|
|
let persistent_cart = self.load_user_cart_from_storage(user_id);
|
|
|
|
// Update session with persistent cart data to ensure consistency
|
|
if let Err(e) = self.save_cart_to_session(user_id, session, &persistent_cart) {
|
|
}
|
|
|
|
return persistent_cart;
|
|
}
|
|
|
|
// For guest users, try to get cart from session
|
|
if let Ok(Some(cart_json)) = session.get::<String>(cart_key) {
|
|
if let Ok(cart) = serde_json::from_str::<Cart>(&cart_json) {
|
|
return cart;
|
|
}
|
|
}
|
|
|
|
// Create new cart for guest users
|
|
Cart::new(user_id.to_string())
|
|
}
|
|
|
|
/// Load user cart from persistent storage
|
|
fn load_user_cart_from_storage(&self, user_id: &str) -> Cart {
|
|
// Sanitize user ID for filesystem compatibility
|
|
let sanitized_id = user_id.replace('@', "_at_").replace('.', "_");
|
|
let cart_file = format!("user_data/{}_cart.json", sanitized_id);
|
|
|
|
|
|
|
|
if let Ok(cart_json) = std::fs::read_to_string(&cart_file) {
|
|
|
|
|
|
match serde_json::from_str::<Cart>(&cart_json) {
|
|
Ok(cart) => {
|
|
|
|
return cart;
|
|
},
|
|
Err(e) => {
|
|
|
|
// Return empty cart instead of corrupted data
|
|
return Cart::new(user_id.to_string());
|
|
}
|
|
}
|
|
} else {
|
|
|
|
}
|
|
|
|
Cart::new(user_id.to_string())
|
|
}
|
|
|
|
/// Save user cart to persistent storage
|
|
fn save_user_cart_to_storage(&self, user_id: &str, cart: &Cart) -> Result<(), String> {
|
|
// Sanitize user ID for filesystem compatibility
|
|
let sanitized_id = user_id.replace('@', "_at_").replace('.', "_");
|
|
let cart_file = format!("user_data/{}_cart.json", sanitized_id);
|
|
|
|
// Ensure user_data directory exists
|
|
let _ = std::fs::create_dir_all("user_data");
|
|
|
|
let cart_json = serde_json::to_string_pretty(cart)
|
|
.map_err(|e| format!("Failed to serialize cart: {}", e))?;
|
|
|
|
std::fs::write(&cart_file, cart_json)
|
|
.map_err(|e| format!("Failed to save cart: {}", e))
|
|
}
|
|
|
|
/// Save cart to session
|
|
fn save_cart_to_session(&self, user_id: &str, session: &Session, cart: &Cart) -> Result<(), String> {
|
|
let cart_key = if user_id.is_empty() {
|
|
"guest_cart"
|
|
} else {
|
|
"user_cart"
|
|
};
|
|
|
|
let cart_json = serde_json::to_string(cart)
|
|
.map_err(|e| format!("Failed to serialize cart: {}", e))?;
|
|
|
|
session.insert(cart_key, cart_json)
|
|
.map_err(|e| format!("Failed to save cart to session: {}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Add item to cart
|
|
pub fn add_to_cart(
|
|
&self,
|
|
user_id: &str,
|
|
session: &Session,
|
|
product_id: String,
|
|
quantity: u32,
|
|
specifications: Option<HashMap<String, serde_json::Value>>,
|
|
) -> Result<(), String> {
|
|
|
|
|
|
// Debug: Log available products
|
|
let all_products = self.product_service.get_all_products();
|
|
|
|
|
|
// Validate product exists
|
|
if let Some(found_product) = self.product_service.get_product_by_id(&product_id) {
|
|
|
|
} else {
|
|
|
|
return Err("Product not found".to_string());
|
|
}
|
|
|
|
eprintln!("[ORDER SERVICE] Getting or creating cart for user_id: '{}'", user_id);
|
|
let mut cart = self.get_or_create_cart(user_id, session);
|
|
eprintln!("[ORDER SERVICE] Cart retrieved/created - current items: {}", cart.items.len());
|
|
|
|
let cart_item = if let Some(specs) = specifications {
|
|
CartItem::with_specifications(product_id.clone(), quantity, specs)
|
|
} else {
|
|
CartItem::new(product_id.clone(), quantity)
|
|
};
|
|
eprintln!("[ORDER SERVICE] Created cart item for product: {}", product_id);
|
|
|
|
cart.add_item(cart_item);
|
|
eprintln!("[ORDER SERVICE] Added item to cart - total items now: {}", cart.items.len());
|
|
|
|
eprintln!("[ORDER SERVICE] Saving cart to session...");
|
|
match self.save_cart_to_session(user_id, session, &cart) {
|
|
Ok(()) => eprintln!("[ORDER SERVICE] Cart saved to session successfully"),
|
|
Err(e) => {
|
|
eprintln!("[ORDER SERVICE ERROR] Failed to save cart to session: {}", e);
|
|
return Err(e);
|
|
}
|
|
}
|
|
|
|
// Save to persistent storage for logged-in users
|
|
if !user_id.is_empty() {
|
|
eprintln!("[ORDER SERVICE] Saving cart to persistent storage for user: {}", user_id);
|
|
match self.save_user_cart_to_storage(user_id, &cart) {
|
|
Ok(()) => eprintln!("[ORDER SERVICE] Cart saved to persistent storage successfully"),
|
|
Err(e) => {
|
|
eprintln!("[ORDER SERVICE ERROR] Failed to save cart to persistent storage: {}", e);
|
|
return Err(e);
|
|
}
|
|
}
|
|
} else {
|
|
eprintln!("[ORDER SERVICE] Guest user - skipping persistent storage");
|
|
}
|
|
|
|
eprintln!("[ORDER SERVICE] add_to_cart completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove item from cart
|
|
pub fn remove_from_cart(
|
|
&self,
|
|
user_id: &str,
|
|
session: &Session,
|
|
product_id: &str,
|
|
) -> Result<bool, String> {
|
|
let mut cart = self.get_or_create_cart(user_id, session);
|
|
let removed = cart.remove_item(product_id);
|
|
|
|
if removed {
|
|
// Save to session first
|
|
self.save_cart_to_session(user_id, session, &cart)?;
|
|
|
|
// For logged-in users, also save to persistent storage
|
|
if !user_id.is_empty() {
|
|
self.save_user_cart_to_storage(user_id, &cart)?;
|
|
}
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
/// Update cart item quantity
|
|
pub fn update_cart_item_quantity(
|
|
&self,
|
|
user_id: &str,
|
|
session: &Session,
|
|
product_id: &str,
|
|
quantity: u32,
|
|
) -> Result<bool, String> {
|
|
let mut cart = self.get_or_create_cart(user_id, session);
|
|
let result = cart.update_item_quantity(product_id, quantity);
|
|
self.save_cart_to_session(user_id, session, &cart)?;
|
|
|
|
// Save to persistent storage for logged-in users
|
|
if !user_id.is_empty() {
|
|
self.save_user_cart_to_storage(user_id, &cart)?;
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// Get cart contents with product details and pricing
|
|
pub fn get_cart_with_details(
|
|
&self,
|
|
user_id: &str,
|
|
session: &Session,
|
|
display_currency: &str,
|
|
) -> Result<CartDetails, String> {
|
|
let mut cart = self.get_or_create_cart(user_id, session);
|
|
|
|
let mut cart_details = CartDetails {
|
|
cart: cart.clone(),
|
|
items: Vec::default(),
|
|
subtotal: dec!(0),
|
|
total: dec!(0),
|
|
currency: display_currency.to_string(),
|
|
item_count: 0,
|
|
};
|
|
|
|
let mut removed_invalid = false;
|
|
for cart_item in &cart.items {
|
|
if let Some(product) = self.product_service.get_product_by_id(&cart_item.product_id) {
|
|
let price = self.currency_service.create_price(
|
|
product.base_price,
|
|
&product.base_currency,
|
|
display_currency,
|
|
)?;
|
|
|
|
let item_total = price.display_amount * Decimal::from(cart_item.quantity);
|
|
cart_details.subtotal += item_total;
|
|
|
|
cart_details.items.push(CartItemDetails {
|
|
cart_item: cart_item.clone(),
|
|
product: product.clone(),
|
|
unit_price: price,
|
|
total_price: item_total,
|
|
});
|
|
} else {
|
|
// Product no longer exists; mark for cleanup and exclude from details
|
|
removed_invalid = true;
|
|
}
|
|
}
|
|
|
|
// Compute item_count based on valid items only
|
|
cart_details.item_count = cart_details
|
|
.items
|
|
.iter()
|
|
.map(|i| i.cart_item.quantity)
|
|
.sum();
|
|
|
|
cart_details.total = cart_details.subtotal; // Add taxes, fees, etc. here
|
|
|
|
// If invalid items were found, clean them from the persisted cart and session
|
|
if removed_invalid {
|
|
// Retain only items that still have valid products
|
|
cart.items.retain(|ci| self.product_service.get_product_by_id(&ci.product_id).is_some());
|
|
|
|
// Update metadata
|
|
cart.updated_at = chrono::Utc::now();
|
|
|
|
// Persist the cleaned cart
|
|
let _ = self.save_cart_to_session(user_id, session, &cart);
|
|
if !user_id.is_empty() {
|
|
let _ = self.save_user_cart_to_storage(user_id, &cart);
|
|
}
|
|
|
|
// Ensure returned details reflect cleaned cart structure
|
|
cart_details.cart = cart.clone();
|
|
}
|
|
|
|
Ok(cart_details)
|
|
}
|
|
|
|
/// Clear cart
|
|
pub fn clear_cart(&self, user_id: &str, session: &Session) -> Result<(), String> {
|
|
let mut cart = self.get_or_create_cart(user_id, session);
|
|
cart.clear();
|
|
|
|
// Save cleared cart to session
|
|
self.save_cart_to_session(user_id, session, &cart)?;
|
|
|
|
// For logged-in users, also clear persistent storage
|
|
if !user_id.is_empty() {
|
|
self.save_user_cart_to_storage(user_id, &cart)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Transfer guest cart items to user cart during login
|
|
pub fn transfer_guest_cart_to_user(&self, user_id: &str, session: &Session) -> Result<usize, String> {
|
|
|
|
// Get guest cart if it exists
|
|
let guest_cart = if let Ok(Some(guest_cart_json)) = session.get::<String>("guest_cart") {
|
|
if let Ok(guest_cart) = serde_json::from_str::<Cart>(&guest_cart_json) {
|
|
guest_cart
|
|
} else {
|
|
return Ok(0);
|
|
}
|
|
} else {
|
|
return Ok(0);
|
|
};
|
|
|
|
// If guest cart is empty, nothing to transfer
|
|
if guest_cart.items.is_empty() {
|
|
return Ok(0);
|
|
}
|
|
|
|
// Load user's existing persistent cart from user data
|
|
let mut user_cart = self.load_user_cart_from_storage(user_id);
|
|
|
|
let items_transferred = guest_cart.items.len();
|
|
|
|
// Transfer all items from guest cart to user cart
|
|
for guest_item in guest_cart.items {
|
|
|
|
// Update the cart item to reflect the new user
|
|
let mut user_item = guest_item;
|
|
user_item.updated_at = chrono::Utc::now();
|
|
|
|
// Check if item already exists in user cart
|
|
if let Some(existing_item) = user_cart.items.iter_mut().find(|item| item.product_id == user_item.product_id) {
|
|
// Merge quantities if same product exists
|
|
let old_qty = existing_item.quantity;
|
|
existing_item.quantity += user_item.quantity;
|
|
existing_item.updated_at = chrono::Utc::now();
|
|
} else {
|
|
// Add new item to user cart
|
|
user_cart.items.push(user_item.clone());
|
|
}
|
|
}
|
|
|
|
// Update user cart metadata
|
|
user_cart.user_id = user_id.to_string();
|
|
user_cart.updated_at = chrono::Utc::now();
|
|
|
|
// Save updated user cart to persistent storage FIRST
|
|
match self.save_user_cart_to_storage(user_id, &user_cart) {
|
|
Ok(()) => {
|
|
// Cart saved successfully to storage
|
|
},
|
|
Err(e) => {
|
|
return Err(format!("Failed to save cart to storage: {}", e));
|
|
}
|
|
}
|
|
|
|
// Then save to session
|
|
match self.save_cart_to_session(user_id, session, &user_cart) {
|
|
Ok(()) => {
|
|
// Don't fail the transfer if session save fails, persistent storage is more important
|
|
},
|
|
Err(_) => {
|
|
// Don't fail the transfer if session save fails, persistent storage is more important
|
|
}
|
|
}
|
|
|
|
// Clear guest cart after successful transfer
|
|
session.remove("guest_cart");
|
|
Ok(items_transferred)
|
|
}
|
|
|
|
// Order Operations
|
|
|
|
/// Create order from cart
|
|
pub fn create_order_from_cart(
|
|
&self,
|
|
user_id: &str,
|
|
session: &Session,
|
|
payment_currency: &str,
|
|
) -> Result<String, String> {
|
|
let cart_details = self.get_cart_with_details(user_id, session, payment_currency)?;
|
|
|
|
if cart_details.items.is_empty() {
|
|
return Err("Cart is empty".to_string());
|
|
}
|
|
|
|
let order_id = Uuid::new_v4().to_string();
|
|
// Use CurrencyService base currency (USD) rather than mock data
|
|
let base_currency = self.currency_service.get_base_currency().code.clone();
|
|
|
|
let conversion_rate = if payment_currency == base_currency {
|
|
dec!(1.0)
|
|
} else {
|
|
self.currency_service.get_exchange_rate_to_base(payment_currency)?
|
|
};
|
|
|
|
// Normalize user ID for consistent guest user handling
|
|
let normalized_user_id = if user_id.is_empty() {
|
|
"guest".to_string()
|
|
} else {
|
|
user_id.to_string()
|
|
};
|
|
|
|
let mut order = Order::new(
|
|
order_id.clone(),
|
|
normalized_user_id,
|
|
base_currency.clone(),
|
|
payment_currency.to_string(),
|
|
conversion_rate,
|
|
);
|
|
|
|
// Convert cart items to order items
|
|
for item_detail in cart_details.items {
|
|
let order_item = OrderItem::new(
|
|
item_detail.product.id,
|
|
item_detail.product.name,
|
|
item_detail.product.category_id,
|
|
item_detail.cart_item.quantity,
|
|
item_detail.product.base_price,
|
|
item_detail.product.provider_id,
|
|
item_detail.product.provider_name,
|
|
);
|
|
order.add_item(order_item);
|
|
}
|
|
|
|
// Store order in singleton storage
|
|
{
|
|
let order_storage = OrderStorage::instance();
|
|
let mut storage = order_storage.lock().unwrap();
|
|
storage.insert_order(order_id.clone(), order);
|
|
}
|
|
|
|
// Clear the cart after creating order
|
|
self.clear_cart(user_id, session)?;
|
|
|
|
Ok(order_id)
|
|
}
|
|
|
|
/// Get order by ID
|
|
pub fn get_order(&self, order_id: &str) -> Option<Order> {
|
|
let order_storage = OrderStorage::instance();
|
|
let storage = order_storage.lock().unwrap();
|
|
storage.get_order(order_id).cloned()
|
|
}
|
|
|
|
/// Get orders for user (sorted by creation date, latest first)
|
|
pub fn get_user_orders(&self, user_id: &str) -> Vec<Order> {
|
|
// Load user's persistent data and return their orders
|
|
if let Some(user_data) = crate::services::user_persistence::UserPersistence::load_user_data(user_id) {
|
|
let mut orders = user_data.orders;
|
|
// Sort by created_at in descending order (latest first)
|
|
orders.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
|
orders
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
/// Update order status
|
|
pub fn update_order_status(&self, order_id: &str, status: OrderStatus) -> Result<(), String> {
|
|
let order_storage = OrderStorage::instance();
|
|
let mut storage = order_storage.lock().unwrap();
|
|
storage.update_order_status(order_id, status)
|
|
}
|
|
|
|
/// Process payment for order
|
|
pub fn process_payment(
|
|
&self,
|
|
order_id: &str,
|
|
payment_method: PaymentMethod,
|
|
) -> Result<PaymentResult, String> {
|
|
let order = {
|
|
let order_storage = OrderStorage::instance();
|
|
let storage = order_storage.lock().unwrap();
|
|
storage.get_order(order_id).cloned()
|
|
.ok_or("Order not found")?
|
|
};
|
|
|
|
// Check if this is a wallet payment and handle it with persistent data
|
|
let is_wallet_payment = matches!(payment_method,
|
|
PaymentMethod::Token { ref token_type, .. } if token_type == "USD"
|
|
);
|
|
|
|
let payment_result = if is_wallet_payment {
|
|
// Handle wallet payment with persistent data
|
|
self.process_wallet_payment_with_persistent_data(&order, &payment_method)?
|
|
} else {
|
|
// Use MockPaymentGateway for other payment methods
|
|
let payment_request = PaymentRequest {
|
|
order_id: order_id.to_string(),
|
|
amount: order.currency_total,
|
|
currency: order.currency_used.clone(),
|
|
payment_method: payment_method.clone(),
|
|
user_id: order.user_id.clone(),
|
|
};
|
|
let gateway = MockPaymentGateway { success_rate: 0.95 };
|
|
gateway.process_payment(payment_request)
|
|
};
|
|
|
|
// Update order with payment details
|
|
{
|
|
let order_storage = OrderStorage::instance();
|
|
let mut storage = order_storage.lock().unwrap();
|
|
if let Some(order) = storage.get_order_mut(order_id) {
|
|
if payment_result.success {
|
|
order.update_status(OrderStatus::Completed);
|
|
if let Some(payment_details) = &payment_result.payment_details {
|
|
order.set_payment_details(payment_details.clone());
|
|
}
|
|
|
|
// PHASE 1: Create application deployments for successful app orders
|
|
if let Err(e) = self.create_application_deployments_from_order(&order) {
|
|
}
|
|
|
|
// PHASE 2: Create service bookings for successful service orders
|
|
if let Err(e) = self.create_service_bookings_from_order(&order) {
|
|
}
|
|
} else {
|
|
order.update_status(OrderStatus::Failed);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(payment_result)
|
|
}
|
|
|
|
/// Process wallet payment using persistent data (no mock code)
|
|
fn process_wallet_payment_with_persistent_data(
|
|
&self,
|
|
order: &Order,
|
|
payment_method: &PaymentMethod,
|
|
) -> Result<PaymentResult, String> {
|
|
use crate::services::user_persistence::UserPersistence;
|
|
|
|
// Extract user email from user_id (assuming user_id is email)
|
|
let user_email = &order.user_id;
|
|
|
|
// Load user data
|
|
let mut persistent_data = UserPersistence::load_user_data(user_email)
|
|
.ok_or_else(|| "User data not found".to_string())?;
|
|
|
|
// Check if user has sufficient balance
|
|
if persistent_data.wallet_balance_usd < order.currency_total {
|
|
let mut payment_details = PaymentDetails::new(
|
|
Uuid::new_v4().to_string(),
|
|
payment_method.clone(),
|
|
);
|
|
payment_details.mark_failed(format!(
|
|
"Insufficient balance. Required: ${:.2}, Available: ${:.2}",
|
|
order.currency_total,
|
|
persistent_data.wallet_balance_usd
|
|
));
|
|
|
|
return Ok(PaymentResult {
|
|
success: false,
|
|
transaction_id: None,
|
|
error_message: Some(format!(
|
|
"Insufficient balance. Required: ${:.2}, Available: ${:.2}",
|
|
order.currency_total,
|
|
persistent_data.wallet_balance_usd
|
|
)),
|
|
payment_details: Some(payment_details),
|
|
});
|
|
}
|
|
|
|
// Deduct balance
|
|
persistent_data.wallet_balance_usd -= order.currency_total;
|
|
|
|
// Create transaction record
|
|
let transaction_id = Uuid::new_v4().to_string();
|
|
// Use the first product ID for the transaction (TransactionType::Purchase only supports single product_id)
|
|
let product_id = order.items.first()
|
|
.map(|item| item.product_id.clone())
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
let transaction = crate::models::user::Transaction {
|
|
id: transaction_id.clone(),
|
|
user_id: user_email.clone(),
|
|
transaction_type: crate::models::user::TransactionType::Purchase {
|
|
product_id,
|
|
},
|
|
amount: order.currency_total,
|
|
currency: Some("USD".to_string()),
|
|
exchange_rate_usd: Some(rust_decimal::Decimal::ONE),
|
|
amount_usd: Some(order.currency_total),
|
|
description: Some(format!("Order {} payment", order.id)),
|
|
reference_id: Some(order.id.clone()),
|
|
metadata: None,
|
|
timestamp: chrono::Utc::now(),
|
|
status: crate::models::user::TransactionStatus::Completed,
|
|
};
|
|
|
|
// Add transaction to history
|
|
persistent_data.transactions.push(transaction);
|
|
|
|
// Build payment details now so we can persist them with the order
|
|
let mut payment_details = PaymentDetails::new(
|
|
transaction_id.clone(),
|
|
payment_method.clone(),
|
|
);
|
|
payment_details.mark_completed(transaction_id.clone());
|
|
|
|
// Persist the order as Completed with payment details for dashboard views
|
|
let mut order_to_persist = order.clone();
|
|
order_to_persist.update_status(OrderStatus::Completed);
|
|
order_to_persist.set_payment_details(payment_details.clone());
|
|
persistent_data.orders.push(order_to_persist);
|
|
|
|
// Save updated user data (includes transactions and updated order)
|
|
UserPersistence::save_user_data(&persistent_data)
|
|
.map_err(|e| format!("Failed to save user data: {}", e))?;
|
|
|
|
// Create successful payment result
|
|
Ok(PaymentResult {
|
|
success: true,
|
|
transaction_id: Some(transaction_id),
|
|
error_message: None,
|
|
payment_details: Some(payment_details),
|
|
})
|
|
}
|
|
|
|
/// Get order statistics
|
|
pub fn get_order_statistics(&self) -> HashMap<String, serde_json::Value> {
|
|
let order_storage = OrderStorage::instance();
|
|
let storage = order_storage.lock().unwrap();
|
|
|
|
let mut stats = HashMap::default();
|
|
|
|
stats.insert("total_orders".to_string(),
|
|
serde_json::Value::Number(serde_json::Number::from(storage.get_orders_count())));
|
|
|
|
// Orders by status
|
|
let mut status_counts: HashMap<String, i32> = HashMap::default();
|
|
for order in storage.get_all_orders() {
|
|
let status_str = format!("{:?}", order.status);
|
|
*status_counts.entry(status_str).or_insert(0) += 1;
|
|
}
|
|
|
|
let status_stats: Vec<serde_json::Value> = status_counts.iter()
|
|
.map(|(status, count)| {
|
|
serde_json::json!({
|
|
"status": status,
|
|
"count": count
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
stats.insert("orders_by_status".to_string(), serde_json::Value::Array(status_stats));
|
|
|
|
// Revenue calculation
|
|
let total_revenue: Decimal = storage.get_all_orders().iter()
|
|
.filter(|o| matches!(o.status, OrderStatus::Completed | OrderStatus::Confirmed))
|
|
.map(|o| o.total_base)
|
|
.sum();
|
|
|
|
// Use CurrencyService base currency (USD) rather than mock data
|
|
let currency = self.currency_service.get_base_currency().code.clone();
|
|
stats.insert("total_revenue".to_string(), serde_json::json!({
|
|
"amount": total_revenue.to_string(),
|
|
"currency": currency
|
|
}));
|
|
|
|
stats
|
|
}
|
|
|
|
/// PHASE 1: Create application deployments when apps are successfully ordered
|
|
fn create_application_deployments_from_order(&self, order: &Order) -> Result<(), String> {
|
|
use crate::services::user_persistence::{UserPersistence, AppDeployment};
|
|
use crate::models::user::ResourceUtilization;
|
|
use chrono::Utc;
|
|
|
|
// Get customer information from order
|
|
let customer_email = order.user_id.clone();
|
|
let customer_name = if customer_email == "guest" {
|
|
"Guest User".to_string()
|
|
} else {
|
|
// Try to get customer name from persistent data
|
|
if let Some(customer_data) = UserPersistence::load_user_data(&customer_email) {
|
|
customer_data.name.unwrap_or_else(|| customer_email.clone())
|
|
} else {
|
|
customer_email.clone()
|
|
}
|
|
};
|
|
|
|
// Process each order item
|
|
for item in &order.items {
|
|
// Only create deployments for application products
|
|
if item.product_category == "application" {
|
|
|
|
// Find the application provider by looking up who published this app
|
|
if let Some(application_provider_email) = self.find_application_provider(&item.product_id) {
|
|
|
|
// Create deployment for each quantity ordered
|
|
for _i in 0..item.quantity {
|
|
let deployment_id = format!("dep-{}-{}",
|
|
&order.id[..8],
|
|
&uuid::Uuid::new_v4().to_string()[..8]
|
|
);
|
|
|
|
let deployment = AppDeployment::builder()
|
|
.id(&deployment_id)
|
|
.app_id(&item.product_id)
|
|
.app_name(&item.product_name)
|
|
.customer_name(&customer_name)
|
|
.customer_email(&customer_email)
|
|
.deployed_date(&Utc::now().format("%Y-%m-%d").to_string())
|
|
.status("Active")
|
|
.health_score(98.0 + (rand::random::<f32>() * 2.0)) // 98-100%
|
|
.region(&self.select_deployment_region())
|
|
.instances(1) // Default to 1 instance per deployment
|
|
.resource_usage(ResourceUtilization {
|
|
cpu: 15 + (rand::random::<i32>() % 20), // 15-35%
|
|
memory: 25 + (rand::random::<i32>() % 30), // 25-55%
|
|
storage: 10 + (rand::random::<i32>() % 15), // 10-25%
|
|
network: 5 + (rand::random::<i32>() % 10), // 5-15%
|
|
})
|
|
.monthly_revenue_usd((item.unit_price_base.to_f64().unwrap_or(0.0) * 0.8) as i32) // 80% to provider
|
|
.last_updated(&Utc::now().format("%Y-%m-%d %H:%M:%S").to_string())
|
|
.auto_healing(true) // Default to enabled for new deployments
|
|
.build()
|
|
.unwrap();
|
|
|
|
// Add deployment to application provider's data
|
|
if let Err(e) = UserPersistence::add_user_application_deployment(&application_provider_email, deployment.clone()) {
|
|
} else {
|
|
}
|
|
|
|
// Also add deployment to customer's data (for future user dashboard)
|
|
if customer_email != "guest" {
|
|
if let Err(e) = UserPersistence::add_user_application_deployment(&customer_email, deployment) {
|
|
} else {
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Find the application provider (user who published the app) by product ID
|
|
fn find_application_provider(&self, product_id: &str) -> Option<String> {
|
|
// Get all user data files and search for the app
|
|
let user_data_dir = std::path::Path::new("user_data");
|
|
if !user_data_dir.exists() {
|
|
return None;
|
|
}
|
|
|
|
if let Ok(entries) = std::fs::read_dir(user_data_dir) {
|
|
for entry in entries.flatten() {
|
|
if let Some(file_name) = entry.file_name().to_str() {
|
|
if file_name.ends_with(".json")
|
|
&& file_name.contains("_at_")
|
|
&& !file_name.contains("_cart")
|
|
&& file_name != "session_data.json"
|
|
{
|
|
// Extract email from filename
|
|
let user_email = file_name
|
|
.trim_end_matches(".json")
|
|
.replace("_at_", "@")
|
|
.replace("_", ".");
|
|
|
|
// Check if this user has the app
|
|
let user_apps = UserPersistence::get_user_apps(&user_email);
|
|
for app in user_apps {
|
|
if app.id == product_id {
|
|
return Some(user_email);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Select a deployment region (simple round-robin for demo)
|
|
fn select_deployment_region(&self) -> String {
|
|
let regions = vec![
|
|
"US-East-1", "US-West-2", "EU-Central-1",
|
|
"Asia-Pacific-1", "Canada-Central-1"
|
|
];
|
|
let index = (rand::random::<usize>()) % regions.len();
|
|
regions[index].to_string()
|
|
}
|
|
|
|
/// PHASE 2: Create service bookings when services are successfully ordered
|
|
pub fn create_service_bookings_from_order(&self, order: &Order) -> Result<(), String> {
|
|
use crate::services::user_persistence::{UserPersistence};
|
|
use crate::models::user::{ServiceRequest};
|
|
use chrono::Utc;
|
|
|
|
// Get customer information from order
|
|
let customer_email = order.user_id.clone();
|
|
let customer_name = if customer_email == "guest" {
|
|
"Guest User".to_string()
|
|
} else {
|
|
// Try to get customer name from persistent data
|
|
if let Some(customer_data) = UserPersistence::load_user_data(&customer_email) {
|
|
customer_data.name.unwrap_or_else(|| customer_email.clone())
|
|
} else {
|
|
customer_email.clone()
|
|
}
|
|
};
|
|
|
|
log::info!(
|
|
target: "order_service",
|
|
"create_service_bookings_from_order:start order_id={} customer_email={}",
|
|
order.id,
|
|
customer_email
|
|
);
|
|
|
|
// Process each order item
|
|
for item in &order.items {
|
|
// Only create bookings for service products
|
|
if item.product_category == "service" {
|
|
|
|
// Find the service provider by looking up who published this service
|
|
if let Some(service_provider_email) = self.find_service_provider(&item.product_id) {
|
|
log::info!(
|
|
target: "order_service",
|
|
"provider_found order_id={} product_id={} provider_email={}",
|
|
order.id,
|
|
item.product_id,
|
|
service_provider_email
|
|
);
|
|
|
|
// Create service request for provider (same as existing pattern)
|
|
for _i in 0..item.quantity {
|
|
let request_id = format!("req-{}-{}",
|
|
&order.id[..8],
|
|
&uuid::Uuid::new_v4().to_string()[..8]
|
|
);
|
|
|
|
let service_request = ServiceRequest {
|
|
id: request_id.clone(),
|
|
customer_email: customer_email.clone(),
|
|
service_id: item.product_id.clone(),
|
|
description: Some(format!("Service booking from marketplace order {}", order.id)),
|
|
status: "Pending".to_string(),
|
|
estimated_hours: Some((item.unit_price_base.to_f64().unwrap_or(0.0) / 75.0) as i32), // Assume $75/hour
|
|
hourly_rate_usd: Some(rust_decimal::Decimal::from(75)),
|
|
total_cost_usd: Some(item.unit_price_base),
|
|
progress_percentage: Some(0.0),
|
|
created_date: Some(Utc::now().format("%Y-%m-%d").to_string()),
|
|
completed_date: None,
|
|
progress: None,
|
|
priority: "Medium".to_string(),
|
|
hours_worked: None,
|
|
notes: None,
|
|
service_name: item.product_name.clone(),
|
|
budget: item.unit_price_base,
|
|
requested_date: Utc::now().format("%Y-%m-%d").to_string(),
|
|
client_phone: None,
|
|
client_name: Some(customer_name.clone()),
|
|
client_email: Some(customer_email.clone()),
|
|
};
|
|
|
|
// Add request to service provider's data
|
|
if let Err(e) = UserPersistence::add_user_service_request(&service_provider_email, service_request.clone()) {
|
|
log::error!(
|
|
target: "order_service",
|
|
"add_user_service_request:failed order_id={} provider_email={} request_id={} err={}",
|
|
order.id,
|
|
service_provider_email,
|
|
request_id,
|
|
e
|
|
);
|
|
return Err(format!(
|
|
"Failed to persist service request for provider {}: {}",
|
|
service_provider_email, e
|
|
));
|
|
} else {
|
|
log::info!(
|
|
target: "order_service",
|
|
"add_user_service_request:succeeded order_id={} provider_email={} request_id={}",
|
|
order.id,
|
|
service_provider_email,
|
|
request_id
|
|
);
|
|
}
|
|
|
|
// Create service booking for customer
|
|
let service_booking = UserPersistence::create_service_booking_from_request(
|
|
&service_request,
|
|
&customer_email,
|
|
&service_provider_email
|
|
);
|
|
|
|
// Add booking to customer's data
|
|
if customer_email != "guest" {
|
|
if let Err(e) = UserPersistence::add_user_service_booking(&customer_email, service_booking.clone()) {
|
|
log::error!(
|
|
target: "order_service",
|
|
"add_user_service_booking:failed order_id={} customer_email={} booking_id={} err={}",
|
|
order.id,
|
|
customer_email,
|
|
service_booking.id,
|
|
e
|
|
);
|
|
return Err(format!(
|
|
"Failed to persist service booking for customer {}: {}",
|
|
customer_email, e
|
|
));
|
|
} else {
|
|
log::info!(
|
|
target: "order_service",
|
|
"add_user_service_booking:succeeded order_id={} customer_email={} booking_id={}",
|
|
order.id,
|
|
customer_email,
|
|
service_booking.id
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log::warn!(
|
|
target: "order_service",
|
|
"provider_not_found order_id={} product_id={} product_name={}",
|
|
order.id,
|
|
item.product_id,
|
|
item.product_name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
log::info!(
|
|
target: "order_service",
|
|
"create_service_bookings_from_order:success order_id={}",
|
|
order.id
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Find the service provider (user who published the service) by product ID
|
|
fn find_service_provider(&self, product_id: &str) -> Option<String> {
|
|
// Search per-user persisted data first
|
|
let user_data_dir = std::path::Path::new("user_data");
|
|
if user_data_dir.exists() {
|
|
if let Ok(entries) = std::fs::read_dir(user_data_dir) {
|
|
for entry in entries.flatten() {
|
|
if let Some(file_name) = entry.file_name().to_str() {
|
|
if file_name.ends_with(".json")
|
|
&& file_name.contains("_at_")
|
|
&& !file_name.contains("_cart")
|
|
&& file_name != "session_data.json"
|
|
{
|
|
// Extract email from filename
|
|
let user_email = file_name
|
|
.trim_end_matches(".json")
|
|
.replace("_at_", "@")
|
|
.replace("_", ".");
|
|
|
|
// Check if this user has the service
|
|
let user_services = UserPersistence::get_user_services(&user_email);
|
|
for service in user_services {
|
|
if service.id == product_id {
|
|
return Some(user_email);
|
|
}
|
|
}
|
|
|
|
// Also check if this user has the product (published services live under products)
|
|
let user_products = UserPersistence::get_user_products(&user_email);
|
|
for product in user_products {
|
|
if product.id == product_id {
|
|
return Some(user_email);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: look up in the global product catalog to resolve provider
|
|
let all_products = self.product_service.get_all_products();
|
|
if let Some(p) = all_products.into_iter().find(|p| p.id == product_id) {
|
|
return Some(p.provider_id);
|
|
}
|
|
|
|
// If nothing matched, return None
|
|
None
|
|
}
|
|
|
|
}
|
|
|
|
/// Cart details with product information and pricing
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct CartDetails {
|
|
pub cart: Cart,
|
|
pub items: Vec<CartItemDetails>,
|
|
pub subtotal: Decimal,
|
|
pub total: Decimal,
|
|
pub currency: String,
|
|
pub item_count: u32,
|
|
}
|
|
|
|
/// Cart item with product details and pricing
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct CartItemDetails {
|
|
pub cart_item: CartItem,
|
|
pub product: Product,
|
|
pub unit_price: crate::models::currency::Price,
|
|
pub total_price: Decimal,
|
|
}
|
|
|
|
|
|
|
|
impl MockPaymentGateway {
|
|
pub fn process_payment(&self, request: PaymentRequest) -> PaymentResult {
|
|
// Simulate payment processing delay
|
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
|
|
// Simulate success/failure based on success rate
|
|
let success = rand::random::<f32>() < self.success_rate;
|
|
|
|
if success {
|
|
let transaction_id = Uuid::new_v4().to_string();
|
|
let mut payment_details = PaymentDetails::new(
|
|
Uuid::new_v4().to_string(),
|
|
request.payment_method,
|
|
);
|
|
payment_details.mark_completed(transaction_id.clone());
|
|
|
|
PaymentResult {
|
|
success: true,
|
|
transaction_id: Some(transaction_id),
|
|
error_message: None,
|
|
payment_details: Some(payment_details),
|
|
}
|
|
} else {
|
|
let mut payment_details = PaymentDetails::new(
|
|
Uuid::new_v4().to_string(),
|
|
request.payment_method,
|
|
);
|
|
payment_details.mark_failed("Payment processing failed".to_string());
|
|
|
|
PaymentResult {
|
|
success: false,
|
|
transaction_id: None,
|
|
error_message: Some("Payment processing failed. Please try again.".to_string()),
|
|
payment_details: Some(payment_details),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for OrderService {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Utility functions for order operations
|
|
pub mod utils {
|
|
use super::*;
|
|
|
|
/// Calculate order total with taxes and fees
|
|
pub fn calculate_order_total(
|
|
subtotal: Decimal,
|
|
tax_rate: Option<f32>,
|
|
shipping_fee: Option<Decimal>,
|
|
discount: Option<Decimal>,
|
|
) -> Decimal {
|
|
let mut total = subtotal;
|
|
|
|
// Apply tax
|
|
if let Some(rate) = tax_rate {
|
|
total += subtotal * Decimal::from_f32_retain(rate).unwrap_or(dec!(0));
|
|
}
|
|
|
|
// Add shipping
|
|
if let Some(shipping) = shipping_fee {
|
|
total += shipping;
|
|
}
|
|
|
|
// Apply discount
|
|
if let Some(discount_amount) = discount {
|
|
total -= discount_amount;
|
|
}
|
|
|
|
total.max(dec!(0)) // Ensure total is not negative
|
|
}
|
|
|
|
/// Generate order confirmation number
|
|
pub fn generate_order_confirmation(order_id: &str) -> String {
|
|
let timestamp = Utc::now().timestamp();
|
|
let short_id = &order_id[..8];
|
|
format!("TF-{}-{}", timestamp, short_id.to_uppercase())
|
|
}
|
|
|
|
/// Validate payment method
|
|
pub fn validate_payment_method(payment_method: &PaymentMethod) -> Result<(), String> {
|
|
match payment_method {
|
|
PaymentMethod::CreditCard { last_four, .. } => {
|
|
if last_four.len() != 4 || !last_four.chars().all(|c| c.is_ascii_digit()) {
|
|
return Err("Invalid credit card last four digits".to_string());
|
|
}
|
|
},
|
|
PaymentMethod::BankTransfer { account_last_four, .. } => {
|
|
if account_last_four.len() != 4 || !account_last_four.chars().all(|c| c.is_ascii_digit()) {
|
|
return Err("Invalid bank account last four digits".to_string());
|
|
}
|
|
},
|
|
PaymentMethod::Cryptocurrency { wallet_address, .. } => {
|
|
if wallet_address.is_empty() {
|
|
return Err("Wallet address is required".to_string());
|
|
}
|
|
},
|
|
PaymentMethod::Token { wallet_address, .. } => {
|
|
if wallet_address.is_empty() {
|
|
return Err("Wallet address is required".to_string());
|
|
}
|
|
},
|
|
PaymentMethod::Mock { .. } => {
|
|
// Mock payment method is always valid
|
|
},
|
|
}
|
|
Ok(())
|
|
}
|
|
} |