use actix_web::{web, HttpResponse, Result, http}; use tera::{Context, Tera}; use chrono::{Utc, Duration}; use serde::Deserialize; use uuid::Uuid; use crate::models::asset::{Asset, AssetType, AssetStatus}; use crate::models::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics}; use crate::controllers::asset::AssetController; use crate::utils::render_template; #[derive(Debug, Deserialize)] pub struct ListingForm { pub title: String, pub description: String, pub asset_id: String, pub price: f64, pub currency: String, pub listing_type: String, pub duration_days: Option, pub tags: Option, } #[derive(Debug, Deserialize)] pub struct BidForm { pub amount: f64, pub currency: String, } #[derive(Debug, Deserialize)] pub struct PurchaseForm { pub agree_to_terms: bool, } pub struct MarketplaceController; impl MarketplaceController { // Display the marketplace dashboard pub async fn index(tmpl: web::Data) -> Result { let mut context = Context::new(); let listings = Self::get_mock_listings(); let stats = MarketplaceStatistics::new(&listings); // Get featured listings (up to 4) let featured_listings: Vec<&Listing> = listings.iter() .filter(|l| l.featured && l.status == ListingStatus::Active) .take(4) .collect(); // Get recent listings (up to 8) let mut recent_listings: Vec<&Listing> = listings.iter() .filter(|l| l.status == ListingStatus::Active) .collect(); // Sort by created_at (newest first) recent_listings.sort_by(|a, b| b.created_at.cmp(&a.created_at)); let recent_listings = recent_listings.into_iter().take(8).collect::>(); // Get recent sales (up to 5) let mut recent_sales: Vec<&Listing> = listings.iter() .filter(|l| l.status == ListingStatus::Sold) .collect(); // Sort by sold_at (newest first) recent_sales.sort_by(|a, b| { let a_sold = a.sold_at.unwrap_or(a.created_at); let b_sold = b.sold_at.unwrap_or(b.created_at); b_sold.cmp(&a_sold) }); let recent_sales = recent_sales.into_iter().take(5).collect::>(); // Add data to context context.insert("active_page", &"marketplace"); context.insert("stats", &stats); context.insert("featured_listings", &featured_listings); context.insert("recent_listings", &recent_listings); context.insert("recent_sales", &recent_sales); render_template(&tmpl, "marketplace/index.html", &context) } // Display all marketplace listings pub async fn list_listings(tmpl: web::Data) -> Result { let mut context = Context::new(); let listings = Self::get_mock_listings(); // Filter active listings let active_listings: Vec<&Listing> = listings.iter() .filter(|l| l.status == ListingStatus::Active) .collect(); context.insert("active_page", &"marketplace"); context.insert("listings", &active_listings); context.insert("listing_types", &[ ListingType::FixedPrice.as_str(), ListingType::Auction.as_str(), ListingType::Exchange.as_str(), ]); context.insert("asset_types", &[ AssetType::Token.as_str(), AssetType::Artwork.as_str(), AssetType::RealEstate.as_str(), AssetType::IntellectualProperty.as_str(), AssetType::Commodity.as_str(), AssetType::Share.as_str(), AssetType::Bond.as_str(), AssetType::Other.as_str(), ]); render_template(&tmpl, "marketplace/listings.html", &context) } // Display my listings pub async fn my_listings(tmpl: web::Data) -> Result { let mut context = Context::new(); let listings = Self::get_mock_listings(); // Filter by current user (mock user ID) let user_id = "user-123"; let my_listings: Vec<&Listing> = listings.iter() .filter(|l| l.seller_id == user_id) .collect(); context.insert("active_page", &"marketplace"); context.insert("listings", &my_listings); render_template(&tmpl, "marketplace/my_listings.html", &context) } // Display listing details pub async fn listing_detail(tmpl: web::Data, path: web::Path) -> Result { let listing_id = path.into_inner(); let mut context = Context::new(); let listings = Self::get_mock_listings(); // Find the listing let listing = listings.iter().find(|l| l.id == listing_id); if let Some(listing) = listing { // Get similar listings (same asset type, active) let similar_listings: Vec<&Listing> = listings.iter() .filter(|l| l.asset_type == listing.asset_type && l.status == ListingStatus::Active && l.id != listing.id) .take(4) .collect(); // Get highest bid amount and minimum bid for auction listings let (highest_bid_amount, minimum_bid) = if listing.listing_type == ListingType::Auction { if let Some(bid) = listing.highest_bid() { (Some(bid.amount), bid.amount + 1.0) } else { (None, listing.price + 1.0) } } else { (None, 0.0) }; context.insert("active_page", &"marketplace"); context.insert("listing", listing); context.insert("similar_listings", &similar_listings); context.insert("highest_bid_amount", &highest_bid_amount); context.insert("minimum_bid", &minimum_bid); // Add current user info for bid/purchase forms let user_id = "user-123"; let user_name = "Alice Hostly"; context.insert("user_id", &user_id); context.insert("user_name", &user_name); render_template(&tmpl, "marketplace/listing_detail.html", &context) } else { Ok(HttpResponse::NotFound().finish()) } } // Display create listing form pub async fn create_listing_form(tmpl: web::Data) -> Result { let mut context = Context::new(); // Get user's assets for selection let assets = AssetController::get_mock_assets(); let user_id = "user-123"; // Mock user ID let user_assets: Vec<&Asset> = assets.iter() .filter(|a| a.owner_id == user_id && a.status == AssetStatus::Active) .collect(); context.insert("active_page", &"marketplace"); context.insert("assets", &user_assets); context.insert("listing_types", &[ ListingType::FixedPrice.as_str(), ListingType::Auction.as_str(), ListingType::Exchange.as_str(), ]); render_template(&tmpl, "marketplace/create_listing.html", &context) } // Create a new listing pub async fn create_listing( tmpl: web::Data, form: web::Form, ) -> Result { let form = form.into_inner(); // Get the asset details let assets = AssetController::get_mock_assets(); let asset = assets.iter().find(|a| a.id == form.asset_id); if let Some(asset) = asset { // Process tags let tags = match form.tags { Some(tags_str) => tags_str.split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(), None => Vec::new(), }; // Calculate expiration date if provided let expires_at = form.duration_days.map(|days| { Utc::now() + Duration::days(days as i64) }); // Parse listing type let listing_type = match form.listing_type.as_str() { "Fixed Price" => ListingType::FixedPrice, "Auction" => ListingType::Auction, "Exchange" => ListingType::Exchange, _ => ListingType::FixedPrice, }; // Mock user data let user_id = "user-123"; let user_name = "Alice Hostly"; // Create the listing let _listing = Listing::new( form.title, form.description, asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), user_id.to_string(), user_name.to_string(), form.price, form.currency, listing_type, expires_at, tags, asset.image_url.clone(), ); // In a real application, we would save the listing to a database here // Redirect to the marketplace Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, "/marketplace")) .finish()) } else { // Asset not found let mut context = Context::new(); context.insert("active_page", &"marketplace"); context.insert("error", &"Asset not found"); render_template(&tmpl, "marketplace/create_listing.html", &context) } } // Submit a bid on an auction listing pub async fn submit_bid( tmpl: web::Data, path: web::Path, form: web::Form, ) -> Result { let listing_id = path.into_inner(); let form = form.into_inner(); // In a real application, we would: // 1. Find the listing in the database // 2. Validate the bid // 3. Create the bid // 4. Save it to the database // For now, we'll just redirect back to the listing Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id))) .finish()) } // Purchase a fixed-price listing pub async fn purchase_listing( tmpl: web::Data, path: web::Path, form: web::Form, ) -> Result { let listing_id = path.into_inner(); let form = form.into_inner(); if !form.agree_to_terms { // User must agree to terms return Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, format!("/marketplace/{}", listing_id))) .finish()); } // In a real application, we would: // 1. Find the listing in the database // 2. Validate the purchase // 3. Process the transaction // 4. Update the listing status // 5. Transfer the asset // For now, we'll just redirect to the marketplace Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, "/marketplace")) .finish()) } // Cancel a listing pub async fn cancel_listing( tmpl: web::Data, path: web::Path, ) -> Result { let _listing_id = path.into_inner(); // In a real application, we would: // 1. Find the listing in the database // 2. Validate that the current user is the seller // 3. Update the listing status // For now, we'll just redirect to my listings Ok(HttpResponse::SeeOther() .insert_header((http::header::LOCATION, "/marketplace/my")) .finish()) } // Generate mock listings for development pub fn get_mock_listings() -> Vec { let assets = AssetController::get_mock_assets(); let mut listings = Vec::new(); // Mock user data let user_ids = vec!["user-123", "user-456", "user-789"]; let user_names = vec!["Alice Hostly", "Ethan Cloudman", "Priya Servera"]; // Create some fixed price listings for i in 0..6 { let asset_index = i % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); let price = match asset.asset_type { AssetType::Token => 50.0 + (i as f64 * 10.0), AssetType::Artwork => 500.0 + (i as f64 * 100.0), AssetType::RealEstate => 50000.0 + (i as f64 * 10000.0), AssetType::IntellectualProperty => 2000.0 + (i as f64 * 500.0), AssetType::Commodity => 1000.0 + (i as f64 * 200.0), AssetType::Share => 300.0 + (i as f64 * 50.0), AssetType::Bond => 1500.0 + (i as f64 * 300.0), AssetType::Other => 800.0 + (i as f64 * 150.0), }; let mut listing = Listing::new( format!("{} for Sale", asset.name), format!("This is a great opportunity to own {}. {}", asset.name, asset.description), asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), user_ids[user_index].to_string(), user_names[user_index].to_string(), price, "USD".to_string(), ListingType::FixedPrice, Some(Utc::now() + Duration::days(30)), vec!["digital".to_string(), "asset".to_string()], asset.image_url.clone(), ); // Make some listings featured if i % 5 == 0 { listing.set_featured(true); } listings.push(listing); } // Create some auction listings for i in 0..4 { let asset_index = (i + 6) % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); let starting_price = match asset.asset_type { AssetType::Token => 40.0 + (i as f64 * 5.0), AssetType::Artwork => 400.0 + (i as f64 * 50.0), AssetType::RealEstate => 40000.0 + (i as f64 * 5000.0), AssetType::IntellectualProperty => 1500.0 + (i as f64 * 300.0), AssetType::Commodity => 800.0 + (i as f64 * 100.0), AssetType::Share => 250.0 + (i as f64 * 40.0), AssetType::Bond => 1200.0 + (i as f64 * 250.0), AssetType::Other => 600.0 + (i as f64 * 120.0), }; let mut listing = Listing::new( format!("Auction: {}", asset.name), format!("Bid on this amazing {}. {}", asset.name, asset.description), asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), user_ids[user_index].to_string(), user_names[user_index].to_string(), starting_price, "USD".to_string(), ListingType::Auction, Some(Utc::now() + Duration::days(7)), vec!["auction".to_string(), "bidding".to_string()], asset.image_url.clone(), ); // Add some bids to the auctions let num_bids = 2 + (i % 3); for j in 0..num_bids { let bidder_index = (j + 1) % user_ids.len(); if bidder_index != user_index { // Ensure seller isn't bidding let bid_amount = starting_price * (1.0 + (0.1 * (j + 1) as f64)); let _ = listing.add_bid( user_ids[bidder_index].to_string(), user_names[bidder_index].to_string(), bid_amount, "USD".to_string(), ); } } // Make some listings featured if i % 3 == 0 { listing.set_featured(true); } listings.push(listing); } // Create some exchange listings for i in 0..3 { let asset_index = (i + 10) % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); let value = match asset.asset_type { AssetType::Token => 60.0 + (i as f64 * 15.0), AssetType::Artwork => 600.0 + (i as f64 * 150.0), AssetType::RealEstate => 60000.0 + (i as f64 * 15000.0), AssetType::IntellectualProperty => 2500.0 + (i as f64 * 600.0), AssetType::Commodity => 1200.0 + (i as f64 * 300.0), AssetType::Share => 350.0 + (i as f64 * 70.0), AssetType::Bond => 1800.0 + (i as f64 * 350.0), AssetType::Other => 1000.0 + (i as f64 * 200.0), }; let listing = Listing::new( format!("Trade: {}", asset.name), format!("Looking to exchange {} for another asset of similar value. Interested in NFTs and tokens.", asset.name), asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), user_ids[user_index].to_string(), user_names[user_index].to_string(), value, // Estimated value for exchange "USD".to_string(), ListingType::Exchange, Some(Utc::now() + Duration::days(60)), vec!["exchange".to_string(), "trade".to_string()], asset.image_url.clone(), ); listings.push(listing); } // Create some sold listings for i in 0..5 { let asset_index = (i + 13) % assets.len(); let asset = &assets[asset_index]; let seller_index = i % user_ids.len(); let buyer_index = (i + 1) % user_ids.len(); let price = match asset.asset_type { AssetType::Token => 55.0 + (i as f64 * 12.0), AssetType::Artwork => 550.0 + (i as f64 * 120.0), AssetType::RealEstate => 55000.0 + (i as f64 * 12000.0), AssetType::IntellectualProperty => 2200.0 + (i as f64 * 550.0), AssetType::Commodity => 1100.0 + (i as f64 * 220.0), AssetType::Share => 320.0 + (i as f64 * 60.0), AssetType::Bond => 1650.0 + (i as f64 * 330.0), AssetType::Other => 900.0 + (i as f64 * 180.0), }; let sale_price = price * 0.95; // Slight discount on sale let mut listing = Listing::new( format!("{} - SOLD", asset.name), format!("This {} was sold recently.", asset.name), asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), user_ids[seller_index].to_string(), user_names[seller_index].to_string(), price, "USD".to_string(), ListingType::FixedPrice, None, vec!["sold".to_string()], asset.image_url.clone(), ); // Mark as sold let _ = listing.mark_as_sold( user_ids[buyer_index].to_string(), user_names[buyer_index].to_string(), sale_price, ); // Set sold date to be sometime in the past let days_ago = i as i64 + 1; listing.sold_at = Some(Utc::now() - Duration::days(days_ago)); listings.push(listing); } // Create a few cancelled listings for i in 0..2 { let asset_index = (i + 18) % assets.len(); let asset = &assets[asset_index]; let user_index = i % user_ids.len(); let price = match asset.asset_type { AssetType::Token => 45.0 + (i as f64 * 8.0), AssetType::Artwork => 450.0 + (i as f64 * 80.0), AssetType::RealEstate => 45000.0 + (i as f64 * 8000.0), AssetType::IntellectualProperty => 1800.0 + (i as f64 * 400.0), AssetType::Commodity => 900.0 + (i as f64 * 180.0), AssetType::Share => 280.0 + (i as f64 * 45.0), AssetType::Bond => 1350.0 + (i as f64 * 270.0), AssetType::Other => 750.0 + (i as f64 * 150.0), }; let mut listing = Listing::new( format!("{} - Cancelled", asset.name), format!("This listing for {} was cancelled.", asset.name), asset.id.clone(), asset.name.clone(), asset.asset_type.clone(), user_ids[user_index].to_string(), user_names[user_index].to_string(), price, "USD".to_string(), ListingType::FixedPrice, None, vec!["cancelled".to_string()], asset.image_url.clone(), ); // Cancel the listing let _ = listing.cancel(); listings.push(listing); } listings } }