diff --git a/actix_mvc_app/src/controllers/asset.rs b/actix_mvc_app/src/controllers/asset.rs index 7faed45..aae8100 100644 --- a/actix_mvc_app/src/controllers/asset.rs +++ b/actix_mvc_app/src/controllers/asset.rs @@ -443,7 +443,7 @@ impl AssetController { } // Generate mock assets for testing - fn get_mock_assets() -> Vec { + pub fn get_mock_assets() -> Vec { let now = Utc::now(); let mut assets = Vec::new(); diff --git a/actix_mvc_app/src/controllers/marketplace.rs b/actix_mvc_app/src/controllers/marketplace.rs new file mode 100644 index 0000000..ebe48d5 --- /dev/null +++ b/actix_mvc_app/src/controllers/marketplace.rs @@ -0,0 +1,545 @@ +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::NFT.as_str(), + AssetType::RealEstate.as_str(), + AssetType::IntellectualProperty.as_str(), + AssetType::PhysicalAsset.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(); + + context.insert("active_page", &"marketplace"); + context.insert("listing", listing); + context.insert("similar_listings", &similar_listings); + + // Add current user info for bid/purchase forms + let user_id = "user-123"; + let user_name = "John Doe"; + 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 = "John Doe"; + + // 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!["John Doe", "Jane Smith", "Bob Johnson"]; + + // 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::NFT => 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::PhysicalAsset => 1000.0 + (i as f64 * 200.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::NFT => 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::PhysicalAsset => 800.0 + (i as f64 * 100.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::NFT => 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::PhysicalAsset => 1200.0 + (i as f64 * 300.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::NFT => 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::PhysicalAsset => 1100.0 + (i as f64 * 220.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::NFT => 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::PhysicalAsset => 900.0 + (i as f64 * 180.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 + } +} diff --git a/actix_mvc_app/src/controllers/mod.rs b/actix_mvc_app/src/controllers/mod.rs index 22cf289..18054d0 100644 --- a/actix_mvc_app/src/controllers/mod.rs +++ b/actix_mvc_app/src/controllers/mod.rs @@ -7,5 +7,6 @@ pub mod governance; pub mod flow; pub mod contract; pub mod asset; +pub mod marketplace; // Re-export controllers for easier imports diff --git a/actix_mvc_app/src/models/marketplace.rs b/actix_mvc_app/src/models/marketplace.rs new file mode 100644 index 0000000..784a53b --- /dev/null +++ b/actix_mvc_app/src/models/marketplace.rs @@ -0,0 +1,295 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use crate::models::asset::{Asset, AssetType}; + +/// Status of a marketplace listing +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ListingStatus { + Active, + Sold, + Cancelled, + Expired, +} + +impl ListingStatus { + pub fn as_str(&self) -> &str { + match self { + ListingStatus::Active => "Active", + ListingStatus::Sold => "Sold", + ListingStatus::Cancelled => "Cancelled", + ListingStatus::Expired => "Expired", + } + } +} + +/// Type of marketplace listing +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ListingType { + FixedPrice, + Auction, + Exchange, +} + +impl ListingType { + pub fn as_str(&self) -> &str { + match self { + ListingType::FixedPrice => "Fixed Price", + ListingType::Auction => "Auction", + ListingType::Exchange => "Exchange", + } + } +} + +/// Represents a bid on an auction listing +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Bid { + pub id: String, + pub listing_id: String, + pub bidder_id: String, + pub bidder_name: String, + pub amount: f64, + pub currency: String, + pub created_at: DateTime, + pub status: BidStatus, +} + +/// Status of a bid +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum BidStatus { + Active, + Accepted, + Rejected, + Cancelled, +} + +impl BidStatus { + pub fn as_str(&self) -> &str { + match self { + BidStatus::Active => "Active", + BidStatus::Accepted => "Accepted", + BidStatus::Rejected => "Rejected", + BidStatus::Cancelled => "Cancelled", + } + } +} + +/// Represents a marketplace listing +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Listing { + pub id: String, + pub title: String, + pub description: String, + pub asset_id: String, + pub asset_name: String, + pub asset_type: AssetType, + pub seller_id: String, + pub seller_name: String, + pub price: f64, + pub currency: String, + pub listing_type: ListingType, + pub status: ListingStatus, + pub created_at: DateTime, + pub updated_at: DateTime, + pub expires_at: Option>, + pub sold_at: Option>, + pub buyer_id: Option, + pub buyer_name: Option, + pub sale_price: Option, + pub bids: Vec, + pub views: u32, + pub featured: bool, + pub tags: Vec, + pub image_url: Option, +} + +impl Listing { + /// Creates a new listing + pub fn new( + title: String, + description: String, + asset_id: String, + asset_name: String, + asset_type: AssetType, + seller_id: String, + seller_name: String, + price: f64, + currency: String, + listing_type: ListingType, + expires_at: Option>, + tags: Vec, + image_url: Option, + ) -> Self { + let now = Utc::now(); + Self { + id: format!("listing-{}", Uuid::new_v4().to_string()), + title, + description, + asset_id, + asset_name, + asset_type, + seller_id, + seller_name, + price, + currency, + listing_type, + status: ListingStatus::Active, + created_at: now, + updated_at: now, + expires_at, + sold_at: None, + buyer_id: None, + buyer_name: None, + sale_price: None, + bids: Vec::new(), + views: 0, + featured: false, + tags, + image_url, + } + } + + /// Adds a bid to the listing + pub fn add_bid(&mut self, bidder_id: String, bidder_name: String, amount: f64, currency: String) -> Result<(), String> { + if self.status != ListingStatus::Active { + return Err("Listing is not active".to_string()); + } + + if self.listing_type != ListingType::Auction { + return Err("Listing is not an auction".to_string()); + } + + if currency != self.currency { + return Err(format!("Currency mismatch: expected {}, got {}", self.currency, currency)); + } + + // Check if bid amount is higher than current highest bid or starting price + let highest_bid = self.highest_bid(); + let min_bid = match highest_bid { + Some(bid) => bid.amount, + None => self.price, + }; + + if amount <= min_bid { + return Err(format!("Bid amount must be higher than {}", min_bid)); + } + + let bid = Bid { + id: format!("bid-{}", Uuid::new_v4().to_string()), + listing_id: self.id.clone(), + bidder_id, + bidder_name, + amount, + currency, + created_at: Utc::now(), + status: BidStatus::Active, + }; + + self.bids.push(bid); + self.updated_at = Utc::now(); + + Ok(()) + } + + /// Gets the highest bid on the listing + pub fn highest_bid(&self) -> Option<&Bid> { + self.bids.iter() + .filter(|b| b.status == BidStatus::Active) + .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap()) + } + + /// Marks the listing as sold + pub fn mark_as_sold(&mut self, buyer_id: String, buyer_name: String, sale_price: f64) -> Result<(), String> { + if self.status != ListingStatus::Active { + return Err("Listing is not active".to_string()); + } + + self.status = ListingStatus::Sold; + self.sold_at = Some(Utc::now()); + self.buyer_id = Some(buyer_id); + self.buyer_name = Some(buyer_name); + self.sale_price = Some(sale_price); + self.updated_at = Utc::now(); + + Ok(()) + } + + /// Cancels the listing + pub fn cancel(&mut self) -> Result<(), String> { + if self.status != ListingStatus::Active { + return Err("Listing is not active".to_string()); + } + + self.status = ListingStatus::Cancelled; + self.updated_at = Utc::now(); + + Ok(()) + } + + /// Increments the view count + pub fn increment_views(&mut self) { + self.views += 1; + } + + /// Sets the listing as featured + pub fn set_featured(&mut self, featured: bool) { + self.featured = featured; + self.updated_at = Utc::now(); + } +} + +/// Statistics for marketplace +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarketplaceStatistics { + pub total_listings: usize, + pub active_listings: usize, + pub sold_listings: usize, + pub total_value: f64, + pub total_sales: f64, + pub listings_by_type: std::collections::HashMap, + pub sales_by_asset_type: std::collections::HashMap, +} + +impl MarketplaceStatistics { + pub fn new(listings: &[Listing]) -> Self { + let mut total_value = 0.0; + let mut total_sales = 0.0; + let mut listings_by_type = std::collections::HashMap::new(); + let mut sales_by_asset_type = std::collections::HashMap::new(); + + let active_listings = listings.iter() + .filter(|l| l.status == ListingStatus::Active) + .count(); + + let sold_listings = listings.iter() + .filter(|l| l.status == ListingStatus::Sold) + .count(); + + for listing in listings { + if listing.status == ListingStatus::Active { + total_value += listing.price; + } + + if listing.status == ListingStatus::Sold { + if let Some(sale_price) = listing.sale_price { + total_sales += sale_price; + let asset_type = listing.asset_type.as_str().to_string(); + *sales_by_asset_type.entry(asset_type).or_insert(0.0) += sale_price; + } + } + + let listing_type = listing.listing_type.as_str().to_string(); + *listings_by_type.entry(listing_type).or_insert(0) += 1; + } + + Self { + total_listings: listings.len(), + active_listings, + sold_listings, + total_value, + total_sales, + listings_by_type, + sales_by_asset_type, + } + } +} diff --git a/actix_mvc_app/src/models/mod.rs b/actix_mvc_app/src/models/mod.rs index b2cb5d7..8d7fff0 100644 --- a/actix_mvc_app/src/models/mod.rs +++ b/actix_mvc_app/src/models/mod.rs @@ -6,8 +6,10 @@ pub mod governance; pub mod flow; pub mod contract; pub mod asset; +pub mod marketplace; // Re-export models for easier imports pub use user::User; pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority}; pub use calendar::{CalendarEvent, CalendarViewMode}; +pub use marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus, MarketplaceStatistics}; diff --git a/actix_mvc_app/src/routes/mod.rs b/actix_mvc_app/src/routes/mod.rs index 42595b9..10981da 100644 --- a/actix_mvc_app/src/routes/mod.rs +++ b/actix_mvc_app/src/routes/mod.rs @@ -8,6 +8,7 @@ use crate::controllers::governance::GovernanceController; use crate::controllers::flow::FlowController; use crate::controllers::contract::ContractController; use crate::controllers::asset::AssetController; +use crate::controllers::marketplace::MarketplaceController; use crate::middleware::JwtAuth; use crate::SESSION_KEY; @@ -105,6 +106,20 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) { .route("/{id}/transaction", web::post().to(AssetController::add_transaction)) .route("/{id}/status/{status}", web::post().to(AssetController::update_status)) ) + + // Marketplace routes + .service( + web::scope("/marketplace") + .route("", web::get().to(MarketplaceController::index)) + .route("/listings", web::get().to(MarketplaceController::list_listings)) + .route("/my", web::get().to(MarketplaceController::my_listings)) + .route("/create", web::get().to(MarketplaceController::create_listing_form)) + .route("/create", web::post().to(MarketplaceController::create_listing)) + .route("/{id}", web::get().to(MarketplaceController::listing_detail)) + .route("/{id}/bid", web::post().to(MarketplaceController::submit_bid)) + .route("/{id}/purchase", web::post().to(MarketplaceController::purchase_listing)) + .route("/{id}/cancel", web::post().to(MarketplaceController::cancel_listing)) + ) ); // Keep the /protected scope for any future routes that should be under that path diff --git a/actix_mvc_app/src/views/base.html b/actix_mvc_app/src/views/base.html index 689e87e..9eeb986 100644 --- a/actix_mvc_app/src/views/base.html +++ b/actix_mvc_app/src/views/base.html @@ -91,6 +91,7 @@
  • New Ticket
  • My Tickets
  • My Assets
  • +
  • My Listings
  • My Votes
  • {% if user.role == "Admin" %}
  • Admin Panel
  • @@ -149,6 +150,11 @@ Digital Assets + +
    +
    + + Asset Preview +
    +
    +
    +
    + +
    +

    Select an asset to preview

    +
    +
    +
    + + +
    +
    + + Listing Tips +
    +
    +
      +
    • + + Use a clear, descriptive title +
    • +
    • + + Include detailed information about your asset +
    • +
    • + + Set a competitive price +
    • +
    • + + Add relevant tags to improve discoverability +
    • +
    • + + Choose the right listing type for your asset +
    • +
    +
    +
    + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/actix_mvc_app/src/views/marketplace/index.html b/actix_mvc_app/src/views/marketplace/index.html new file mode 100644 index 0000000..d83f392 --- /dev/null +++ b/actix_mvc_app/src/views/marketplace/index.html @@ -0,0 +1,293 @@ +{% extends "base.html" %} + +{% block title %}Digital Assets Marketplace{% endblock %} + +{% block content %} +
    +

    Digital Assets Marketplace

    + + + +
    +
    +
    +
    +

    {{ stats.active_listings }}

    +

    Active Listings

    +
    + +
    +
    +
    +
    +
    +

    ${{ stats.total_value }}

    +

    Total Market Value

    +
    + +
    +
    +
    +
    +
    +

    {{ stats.total_listings }}

    +

    Total Listings

    +
    + +
    +
    +
    +
    +
    +

    ${{ stats.total_sales }}

    +

    Total Sales

    +
    + +
    +
    +
    + + + + + +
    +
    +
    +
    + + Featured Listings +
    +
    +
    + {% if featured_listings|length > 0 %} + {% for listing in featured_listings %} +
    +
    +
    Featured
    + {% if listing.image_url %} + {{ listing.title }} + {% else %} +
    + +
    + {% endif %} +
    +
    {{ listing.title }}
    +

    {{ listing.description }}

    +
    + {{ listing.listing_type.as_str() }} + {{ listing.asset_type.as_str() }} +
    +
    + +
    +
    + {% endfor %} + {% else %} +
    +

    No featured listings available at this time.

    +
    + {% endif %} +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    + + Recent Listings +
    +
    +
    + + + + + + + + + + + + + + {% if recent_listings|length > 0 %} + {% for listing in recent_listings %} + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
    AssetTypePriceListing TypeSellerListedAction
    +
    + {% if listing.image_url %} + {{ listing.asset_name }} + {% else %} + + {% endif %} + {{ listing.asset_name }} +
    +
    + {% if listing.asset_type.as_str() == "Token" %} + {{ listing.asset_type.as_str() }} + {% elif listing.asset_type.as_str() == "NFT" %} + {{ listing.asset_type.as_str() }} + {% elif listing.asset_type.as_str() == "RealEstate" %} + Real Estate + {% elif listing.asset_type.as_str() == "IntellectualProperty" %} + IP + {% else %} + {{ listing.asset_type.as_str() }} + {% endif %} + ${{ listing.price }}{{ listing.listing_type.as_str() }}{{ listing.seller_name }}{{ listing.created_at|date }} + View +
    No recent listings available.
    +
    +
    + +
    +
    + + +
    +
    +
    + + Recent Sales +
    +
    +
    + + + + + + + + + + {% if recent_sales|length > 0 %} + {% for listing in recent_sales %} + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
    AssetPriceDate
    +
    + {% if listing.image_url %} + {{ listing.asset_name }} + {% else %} + + {% endif %} + {{ listing.asset_name }} +
    +
    ${{ listing.sale_price }}{{ listing.sold_at|date }}
    No recent sales available.
    +
    +
    +
    + + +
    +
    + + Listing Types +
    +
    +
    + + + + + + + + + {% for type, count in stats.listings_by_type %} + + + + + {% endfor %} + +
    TypeCount
    {{ type }}{{ count }}
    +
    +
    +
    +
    +
    +
    +{% endblock %} diff --git a/actix_mvc_app/src/views/marketplace/listing_detail.html b/actix_mvc_app/src/views/marketplace/listing_detail.html new file mode 100644 index 0000000..39d1d44 --- /dev/null +++ b/actix_mvc_app/src/views/marketplace/listing_detail.html @@ -0,0 +1,350 @@ +{% extends "base.html" %} + +{% block title %}{{ listing.title }} | Marketplace{% endblock %} + +{% block content %} +
    +

    Listing Details

    + + + +
    + +
    +
    +
    + {% if listing.image_url %} + {{ listing.title }} + {% else %} +
    + +
    + {% endif %} + +
    + {% if listing.listing_type.as_str() == "Fixed Price" %} + + {% elif listing.listing_type.as_str() == "Auction" %} + + {% elif listing.listing_type.as_str() == "Exchange" %} + + {% endif %} + + {% if listing.seller_id == user_id %} + + {% endif %} +
    +
    +
    + + +
    +
    + + Asset Information +
    +
    +

    Asset Name: {{ listing.asset_name }}

    +

    Asset Type: + {% if listing.asset_type.as_str() == "Token" %} + {{ listing.asset_type.as_str() }} + {% elif listing.asset_type.as_str() == "NFT" %} + {{ listing.asset_type.as_str() }} + {% elif listing.asset_type.as_str() == "RealEstate" %} + Real Estate + {% elif listing.asset_type.as_str() == "IntellectualProperty" %} + Intellectual Property + {% else %} + {{ listing.asset_type.as_str() }} + {% endif %} +

    +

    Asset ID: {{ listing.asset_id }}

    + + View Asset Details + +
    +
    +
    + + +
    +
    +
    +
    +
    + + Listing Details +
    +
    + + {{ listing.status.as_str() }} + +
    +
    +
    +
    +

    {{ listing.title }}

    + +
    +
    + {{ listing.listing_type.as_str() }} + {% if listing.featured %} + Featured + {% endif %} +
    +
    +

    ${{ listing.price }}

    +
    +
    + +

    {{ listing.description }}

    + +
    + +
    +
    +

    Seller: {{ listing.seller_name }}

    +

    Listed: {{ listing.created_at|date }}

    +
    +
    +

    Currency: {{ listing.currency }}

    +

    Expires: + {% if listing.expires_at %} + {{ listing.expires_at|date }} + {% else %} + No expiration + {% endif %} +

    +
    +
    + + {% if listing.tags|length > 0 %} +
    + Tags: + {% for tag in listing.tags %} + {{ tag }} + {% endfor %} +
    + {% endif %} +
    +
    + + + {% if listing.listing_type.as_str() == "Auction" %} +
    +
    + + Bids +
    +
    + {% if listing.bids|length > 0 %} +
    + + + + + + + + + + + {% for bid in listing.bids %} + + + + + + + {% endfor %} + +
    BidderAmountTimeStatus
    {{ bid.bidder_name }}${{ bid.amount }}{{ bid.created_at|date }} + + {{ bid.status.as_str() }} + +
    +
    +

    Current Highest Bid: ${{ listing.highest_bid().amount }}

    + {% else %} +

    No bids yet. Be the first to bid!

    +

    Starting Price: ${{ listing.price }}

    + {% endif %} +
    +
    + {% endif %} +
    +
    + + +
    +
    +
    +
    + + Similar Listings +
    +
    +
    + {% if similar_listings|length > 0 %} + {% for similar in similar_listings %} +
    +
    + {% if similar.image_url %} + {{ similar.title }} + {% else %} +
    + +
    + {% endif %} +
    +
    {{ similar.title }}
    +
    + {{ similar.listing_type.as_str() }} + {{ similar.asset_type.as_str() }} +
    +
    + +
    +
    + {% endfor %} + {% else %} +
    +

    No similar listings found.

    +
    + {% endif %} +
    +
    +
    +
    +
    +
    + + + + + + + + + +{% endblock %} diff --git a/actix_mvc_app/src/views/marketplace/listings.html b/actix_mvc_app/src/views/marketplace/listings.html new file mode 100644 index 0000000..aa50e32 --- /dev/null +++ b/actix_mvc_app/src/views/marketplace/listings.html @@ -0,0 +1,294 @@ +{% extends "base.html" %} + +{% block title %}Marketplace Listings{% endblock %} + +{% block content %} +
    +

    Marketplace Listings

    + + + +
    +
    + + Filter Listings +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    + + List New Asset + +
    + + +
    +
    + {% if listings|length > 0 %} + {% for listing in listings %} +
    +
    + {% if listing.featured %} +
    Featured
    + {% endif %} + {% if listing.image_url %} + {{ listing.title }} + {% else %} +
    + +
    + {% endif %} +
    +
    {{ listing.title }}
    +

    {{ listing.description }}

    +
    + {{ listing.listing_type.as_str() }} + {% if listing.asset_type.as_str() == "Token" %} + {{ listing.asset_type.as_str() }} + {% elif listing.asset_type.as_str() == "NFT" %} + {{ listing.asset_type.as_str() }} + {% elif listing.asset_type.as_str() == "RealEstate" %} + Real Estate + {% elif listing.asset_type.as_str() == "IntellectualProperty" %} + IP + {% else %} + {{ listing.asset_type.as_str() }} + {% endif %} +
    +
    + Listed by {{ listing.seller_name }} +
    +
    + +
    +
    + {% endfor %} + {% else %} +
    +
    + No listings found. Create a new listing +
    +
    + {% endif %} +
    +
    + + + +
    + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/actix_mvc_app/src/views/marketplace/my_listings.html b/actix_mvc_app/src/views/marketplace/my_listings.html new file mode 100644 index 0000000..6ff99bd --- /dev/null +++ b/actix_mvc_app/src/views/marketplace/my_listings.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}My Marketplace Listings{% endblock %} + +{% block content %} +
    +

    My Listings

    + + + + + +
    +
    + + My Listings +
    +
    +
    + + + + + + + + + + + + + + + + {% if listings|length > 0 %} + {% for listing in listings %} + + + + + + + + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
    AssetTitlePriceTypeStatusCreatedExpiresViewsActions
    +
    + {% if listing.image_url %} + {{ listing.asset_name }} + {% else %} + + {% endif %} + {{ listing.asset_name }} +
    +
    {{ listing.title }}${{ listing.price }} + {{ listing.listing_type.as_str() }} + + {% if listing.status.as_str() == "Active" %} + {{ listing.status.as_str() }} + {% elif listing.status.as_str() == "Sold" %} + {{ listing.status.as_str() }} + {% elif listing.status.as_str() == "Cancelled" %} + {{ listing.status.as_str() }} + {% elif listing.status.as_str() == "Expired" %} + {{ listing.status.as_str() }} + {% endif %} + {{ listing.created_at|date }} + {% if listing.expires_at %} + {{ listing.expires_at|date }} + {% else %} + N/A + {% endif %} + {{ listing.views }} +
    + + + + {% if listing.status.as_str() == "Active" %} +
    + +
    + {% endif %} +
    +
    + You don't have any listings yet. + Create your first listing +
    +
    +
    +
    + + +
    +
    +
    +
    + + Listings by Status +
    +
    + +
    +
    +
    +
    +
    +
    + + Listings by Type +
    +
    + +
    +
    +
    +
    +
    +{% endblock %} + +{% block scripts %} + + +{% endblock %}