implement marketplace feature wip
This commit is contained in:
@@ -443,7 +443,7 @@ impl AssetController {
|
||||
}
|
||||
|
||||
// Generate mock assets for testing
|
||||
fn get_mock_assets() -> Vec<Asset> {
|
||||
pub fn get_mock_assets() -> Vec<Asset> {
|
||||
let now = Utc::now();
|
||||
let mut assets = Vec::new();
|
||||
|
||||
|
545
actix_mvc_app/src/controllers/marketplace.rs
Normal file
545
actix_mvc_app/src/controllers/marketplace.rs
Normal file
@@ -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<u32>,
|
||||
pub tags: Option<String>,
|
||||
}
|
||||
|
||||
#[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<Tera>) -> Result<HttpResponse> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
// 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::<Vec<_>>();
|
||||
|
||||
// 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<Tera>) -> Result<HttpResponse> {
|
||||
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<Tera>) -> Result<HttpResponse> {
|
||||
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<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
|
||||
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<Tera>) -> Result<HttpResponse> {
|
||||
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<Tera>,
|
||||
form: web::Form<ListingForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
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<Tera>,
|
||||
path: web::Path<String>,
|
||||
form: web::Form<BidForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
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<Tera>,
|
||||
path: web::Path<String>,
|
||||
form: web::Form<PurchaseForm>,
|
||||
) -> Result<HttpResponse> {
|
||||
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<Tera>,
|
||||
path: web::Path<String>,
|
||||
) -> Result<HttpResponse> {
|
||||
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<Listing> {
|
||||
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
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
Reference in New Issue
Block a user