183 lines
6.2 KiB
Rust
183 lines
6.2 KiB
Rust
//! Listing models with type-safe enum-based structure
|
|
//!
|
|
//! This module provides a type-safe representation of different listing types:
|
|
//! - `BasicAuction`: Traditional time-based auction with bidding
|
|
//! - `MultiSlotAuction`: Auction with multiple winners/slots available
|
|
//! - `FixedPriceListing`: Fixed price sale with no bidding
|
|
//! - `BlindAuction`: Blind auction where seller chooses winner
|
|
//!
|
|
//! The main `Listing` enum ensures that only valid fields are accessible for each type.
|
|
//! Database mapping is handled through `ListingRow` with conversion traits.
|
|
|
|
use crate::db::{ListingDbId, ListingDuration, MoneyAmount, UserDbId};
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::fmt::Debug;
|
|
|
|
pub type NewListing = Listing<NewListingFields>;
|
|
pub type PersistedListing = Listing<PersistedListingFields>;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
|
pub struct PersistedListingFields {
|
|
pub id: ListingDbId,
|
|
pub start_at: DateTime<Utc>,
|
|
pub end_at: DateTime<Utc>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Default)]
|
|
pub struct NewListingFields {
|
|
pub start_delay: ListingDuration,
|
|
pub end_delay: ListingDuration,
|
|
}
|
|
|
|
/// Main listing/auction entity
|
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
#[allow(unused)]
|
|
pub struct Listing<P: Debug + Clone> {
|
|
pub persisted: P,
|
|
pub base: ListingBase,
|
|
pub fields: ListingFields,
|
|
}
|
|
|
|
/// Common fields shared by all listing types
|
|
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
|
#[allow(unused)]
|
|
pub struct ListingBase {
|
|
pub seller_id: UserDbId,
|
|
pub title: String,
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
impl ListingBase {
|
|
#[cfg(test)]
|
|
pub fn with_fields(self, fields: ListingFields) -> NewListing {
|
|
Listing {
|
|
persisted: NewListingFields::default(),
|
|
base: self,
|
|
fields,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[allow(unused)]
|
|
pub enum ListingFields {
|
|
BasicAuction {
|
|
starting_bid: MoneyAmount,
|
|
buy_now_price: Option<MoneyAmount>,
|
|
min_increment: MoneyAmount,
|
|
anti_snipe_minutes: Option<i32>,
|
|
},
|
|
MultiSlotAuction {
|
|
starting_bid: MoneyAmount,
|
|
buy_now_price: MoneyAmount,
|
|
min_increment: Option<MoneyAmount>,
|
|
slots_available: i32,
|
|
anti_snipe_minutes: i32,
|
|
},
|
|
FixedPriceListing {
|
|
buy_now_price: MoneyAmount,
|
|
slots_available: i32,
|
|
},
|
|
BlindAuction {
|
|
starting_bid: MoneyAmount,
|
|
},
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::db::ListingDbId;
|
|
use crate::db::{ListingDAO, TelegramUserDbId};
|
|
use rstest::rstest;
|
|
use sqlx::SqlitePool;
|
|
|
|
/// Test utilities for creating an in-memory database with migrations
|
|
async fn create_test_pool() -> SqlitePool {
|
|
// Create an in-memory SQLite database for testing
|
|
let pool = SqlitePool::connect("sqlite::memory:")
|
|
.await
|
|
.expect("Failed to create in-memory database");
|
|
|
|
// Run the migration
|
|
apply_test_migrations(&pool).await;
|
|
|
|
pool
|
|
}
|
|
|
|
/// Apply the database migrations for testing
|
|
async fn apply_test_migrations(pool: &SqlitePool) {
|
|
// Run the actual migrations from the migrations directory
|
|
sqlx::migrate!("./migrations")
|
|
.run(pool)
|
|
.await
|
|
.expect("Failed to run database migrations");
|
|
}
|
|
|
|
/// Create a test user using UserDAO and return their ID
|
|
async fn create_test_user(
|
|
pool: &SqlitePool,
|
|
telegram_id: TelegramUserDbId,
|
|
username: Option<&str>,
|
|
) -> UserDbId {
|
|
use crate::db::{models::user::NewUser, UserDAO};
|
|
|
|
let new_user = NewUser {
|
|
persisted: (),
|
|
telegram_id,
|
|
first_name: "Test User".to_string(),
|
|
last_name: None,
|
|
username: username.map(|s| s.to_string()),
|
|
is_banned: false,
|
|
};
|
|
|
|
let user = UserDAO::insert_user(pool, &new_user)
|
|
.await
|
|
.expect("Failed to create test user");
|
|
user.persisted.id
|
|
}
|
|
|
|
fn build_base_listing(
|
|
seller_id: UserDbId,
|
|
title: impl Into<String>,
|
|
description: Option<&str>,
|
|
) -> ListingBase {
|
|
ListingBase {
|
|
seller_id,
|
|
title: title.into(),
|
|
description: description.map(|s| s.to_string()),
|
|
}
|
|
}
|
|
|
|
#[rstest]
|
|
#[case(ListingFields::BlindAuction { starting_bid: MoneyAmount::from_str("100.00").unwrap() })]
|
|
#[case(ListingFields::BasicAuction { starting_bid: MoneyAmount::from_str("100.00").unwrap(), buy_now_price: Some(MoneyAmount::from_str("100.00").unwrap()), min_increment: MoneyAmount::from_str("1.00").unwrap(), anti_snipe_minutes: Some(5) })]
|
|
#[case(ListingFields::MultiSlotAuction { starting_bid: MoneyAmount::from_str("100.00").unwrap(), buy_now_price: MoneyAmount::from_str("100.00").unwrap(), min_increment: Some(MoneyAmount::from_str("1.00").unwrap()), slots_available: 10, anti_snipe_minutes: 5 })]
|
|
#[case(ListingFields::FixedPriceListing { buy_now_price: MoneyAmount::from_str("100.00").unwrap(), slots_available: 10 })]
|
|
#[case(ListingFields::BlindAuction { starting_bid: MoneyAmount::from_str("100.00").unwrap() })]
|
|
#[tokio::test]
|
|
async fn test_blind_auction_crud(#[case] fields: ListingFields) {
|
|
let pool = create_test_pool().await;
|
|
let seller_id = create_test_user(&pool, 99999.into(), Some("testuser")).await;
|
|
let new_listing = build_base_listing(seller_id, "Test Auction", Some("Test description"))
|
|
.with_fields(fields);
|
|
|
|
// Insert using DAO
|
|
let created_listing = ListingDAO::insert_listing(&pool, new_listing.clone())
|
|
.await
|
|
.expect("Failed to insert listing");
|
|
|
|
assert_eq!(created_listing.base, new_listing.base);
|
|
assert_eq!(created_listing.fields, new_listing.fields);
|
|
|
|
let read_listing = ListingDAO::find_by_id(&pool, created_listing.persisted.id)
|
|
.await
|
|
.expect("Failed to find listing")
|
|
.expect("Listing should exist");
|
|
|
|
assert_eq!(read_listing, created_listing);
|
|
}
|
|
}
|