// heromodels/src/models/finance/marketplace.rs use serde::{Deserialize, Serialize}; use rhai::{CustomType, TypeBuilder}; use chrono::{DateTime, Utc}; use heromodels_core::BaseModelData; use heromodels_derive::model; use super::asset::AssetType; /// ListingStatus defines the status of a marketplace listing #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ListingStatus { Active, // Listing is active and available Sold, // Listing has been sold Cancelled, // Listing was cancelled by the seller Expired, // Listing has expired } impl Default for ListingStatus { fn default() -> Self { ListingStatus::Active } } /// ListingType defines the type of marketplace listing #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum ListingType { FixedPrice, // Fixed price sale Auction, // Auction with bids Exchange, // Exchange for other assets } impl Default for ListingType { fn default() -> Self { ListingType::FixedPrice } } /// BidStatus defines the status of a bid on an auction listing #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum BidStatus { Active, // Bid is active Accepted, // Bid was accepted Rejected, // Bid was rejected Cancelled, // Bid was cancelled by the bidder } impl Default for BidStatus { fn default() -> Self { BidStatus::Active } } /// Bid represents a bid on an auction listing #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Bid { pub listing_id: String, // ID of the listing this bid belongs to pub bidder_id: u32, // ID of the user who placed the bid pub amount: f64, // Bid amount pub currency: String, // Currency of the bid pub status: BidStatus, // Status of the bid pub created_at: DateTime, // When the bid was created } impl Bid { /// Create a new bid pub fn new( listing_id: impl ToString, bidder_id: u32, amount: f64, currency: impl ToString, ) -> Self { Self { listing_id: listing_id.to_string(), bidder_id, amount, currency: currency.to_string(), status: BidStatus::default(), created_at: Utc::now(), } } /// Update the status of the bid pub fn update_status(mut self, status: BidStatus) -> Self { self.status = status; self } } /// Listing represents a marketplace listing for an asset #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] #[model] // Has base.Base in V spec pub struct Listing { pub base_data: BaseModelData, pub title: String, pub description: String, pub asset_id: String, pub asset_type: AssetType, pub seller_id: String, pub price: f64, // Initial price for fixed price, or starting price for auction pub currency: String, pub listing_type: ListingType, pub status: ListingStatus, pub expires_at: Option>, // Optional expiration date pub sold_at: Option>, // Optional date when the item was sold pub buyer_id: Option, // Optional buyer ID pub sale_price: Option, // Optional final sale price pub bids: Vec, // List of bids for auction type listings pub tags: Vec, // Tags for the listing pub image_url: Option, // Optional image URL } impl Listing { /// Create a new listing with auto-generated ID /// /// # Arguments /// * `id` - Optional ID for the listing (use None for auto-generated ID) /// * `title` - Title of the listing /// * `description` - Description of the listing /// * `asset_id` - ID of the asset being listed /// * `asset_type` - Type of the asset /// * `seller_id` - ID of the seller /// * `price` - Initial price for fixed price, or starting price for auction /// * `currency` - Currency of the price /// * `listing_type` - Type of the listing /// * `expires_at` - Optional expiration date /// * `tags` - Tags for the listing /// * `image_url` - Optional image URL pub fn new( id: Option, title: impl ToString, description: impl ToString, asset_id: impl ToString, asset_type: AssetType, seller_id: impl ToString, price: f64, currency: impl ToString, listing_type: ListingType, expires_at: Option>, tags: Vec, image_url: Option, ) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { base_data.update_id(id); } Self { base_data, title: title.to_string(), description: description.to_string(), asset_id: asset_id.to_string(), asset_type, seller_id: seller_id.to_string(), price, currency: currency.to_string(), listing_type, status: ListingStatus::default(), expires_at, sold_at: None, buyer_id: None, sale_price: None, bids: Vec::new(), tags, image_url: image_url.map(|url| url.to_string()), } } /// Add a bid to an auction listing pub fn add_bid(mut self, bid: Bid) -> Result { // Check if listing is an auction if self.listing_type != ListingType::Auction { return Err("Bids can only be placed on auction listings"); } // Check if listing is active if self.status != ListingStatus::Active { return Err("Cannot place bid on inactive listing"); } // Check if bid amount is higher than current price if bid.amount <= self.price { return Err("Bid amount must be higher than current price"); } // Check if there are existing bids and if the new bid is higher if let Some(highest_bid) = self.highest_bid() { if bid.amount <= highest_bid.amount { return Err("Bid amount must be higher than current highest bid"); } } // Add the bid self.bids.push(bid); // Update the current price to the new highest bid if let Some(highest_bid) = self.highest_bid() { self.price = highest_bid.amount; } Ok(self) } /// Get the highest active bid pub fn highest_bid(&self) -> Option<&Bid> { self.bids .iter() .filter(|bid| bid.status == BidStatus::Active) .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap()) } /// Complete a sale (fixed price or auction) pub fn complete_sale( mut self, buyer_id: impl ToString, sale_price: f64, ) -> Result { if self.status != ListingStatus::Active { return Err("Cannot complete sale for inactive listing"); } self.status = ListingStatus::Sold; self.buyer_id = Some(buyer_id.to_string()); self.sale_price = Some(sale_price); self.sold_at = Some(Utc::now()); // If this was an auction, accept the winning bid and reject others if self.listing_type == ListingType::Auction { for bid in &mut self.bids { if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string() && bid.amount == sale_price { bid.status = BidStatus::Accepted; } else { bid.status = BidStatus::Rejected; } } } Ok(self) } /// Cancel the listing pub fn cancel(mut self) -> Result { if self.status != ListingStatus::Active { return Err("Cannot cancel inactive listing"); } self.status = ListingStatus::Cancelled; // Cancel all active bids for bid in &mut self.bids { if bid.status == BidStatus::Active { bid.status = BidStatus::Cancelled; } } Ok(self) } /// Check if the listing has expired and update status if needed pub fn check_expiration(mut self) -> Self { if self.status == ListingStatus::Active { if let Some(expires_at) = self.expires_at { if Utc::now() > expires_at { self.status = ListingStatus::Expired; // Cancel all active bids for bid in &mut self.bids { if bid.status == BidStatus::Active { bid.status = BidStatus::Cancelled; } } } } } self } /// Add tags to the listing pub fn add_tags(mut self, tags: Vec) -> Self { for tag in tags { self.tags.push(tag.to_string()); } self } /// Update the listing details pub fn update_details( mut self, title: Option, description: Option, price: Option, image_url: Option, ) -> Self { if let Some(title) = title { self.title = title.to_string(); } if let Some(description) = description { self.description = description.to_string(); } if let Some(price) = price { self.price = price; } if let Some(image_url) = image_url { self.image_url = Some(image_url.to_string()); } self } }