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, } } }