""" Marketplace and economy data models. This module defines the marketplace-related dataclasses including MarketplaceListing, Bid, Transaction, and ShopItem for the player economy. """ from dataclasses import dataclass, field, asdict from typing import Dict, Any, List, Optional from datetime import datetime from app.models.items import Item from app.models.enums import ListingType, ListingStatus @dataclass class Bid: """ Represents a bid on an auction listing. Attributes: bidder_id: User ID of the bidder bidder_name: Character name of the bidder amount: Bid amount in gold timestamp: ISO timestamp of when bid was placed """ bidder_id: str bidder_name: str amount: int timestamp: str = "" def __post_init__(self): """Initialize timestamp if not provided.""" if not self.timestamp: self.timestamp = datetime.utcnow().isoformat() def to_dict(self) -> Dict[str, Any]: """Serialize bid to dictionary.""" return asdict(self) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Bid': """Deserialize bid from dictionary.""" return cls( bidder_id=data["bidder_id"], bidder_name=data["bidder_name"], amount=data["amount"], timestamp=data.get("timestamp", ""), ) @dataclass class MarketplaceListing: """ Represents an item listing on the player marketplace. Supports both fixed-price and auction-style listings. Attributes: listing_id: Unique identifier seller_id: User ID of the seller character_id: Character ID of the seller item_data: Full item details being sold listing_type: "auction" or "fixed_price" price: For fixed_price listings starting_bid: Minimum bid for auction listings current_bid: Current highest bid for auction listings buyout_price: Optional instant-buy price for auctions bids: Bid history for auction listings auction_end: ISO timestamp when auction ends status: Listing status (active, sold, expired, removed) created_at: ISO timestamp of listing creation """ listing_id: str seller_id: str character_id: str item_data: Item listing_type: ListingType status: ListingStatus = ListingStatus.ACTIVE created_at: str = "" # Fixed price fields price: int = 0 # Auction fields starting_bid: int = 0 current_bid: int = 0 buyout_price: int = 0 bids: List[Bid] = field(default_factory=list) auction_end: str = "" def __post_init__(self): """Initialize timestamps if not provided.""" if not self.created_at: self.created_at = datetime.utcnow().isoformat() def is_auction(self) -> bool: """Check if this is an auction listing.""" return self.listing_type == ListingType.AUCTION def is_fixed_price(self) -> bool: """Check if this is a fixed-price listing.""" return self.listing_type == ListingType.FIXED_PRICE def is_active(self) -> bool: """Check if listing is active.""" return self.status == ListingStatus.ACTIVE def has_ended(self) -> bool: """Check if auction has ended (for auction listings).""" if not self.is_auction() or not self.auction_end: return False end_time = datetime.fromisoformat(self.auction_end) return datetime.utcnow() >= end_time def can_bid(self, bid_amount: int) -> bool: """ Check if a bid amount is valid. Args: bid_amount: Proposed bid amount Returns: True if bid is valid, False otherwise """ if not self.is_auction() or not self.is_active(): return False if self.has_ended(): return False # First bid must meet starting bid if not self.bids and bid_amount < self.starting_bid: return False # Subsequent bids must exceed current bid if self.bids and bid_amount <= self.current_bid: return False return True def place_bid(self, bidder_id: str, bidder_name: str, amount: int) -> bool: """ Place a bid on this auction. Args: bidder_id: User ID of bidder bidder_name: Character name of bidder amount: Bid amount Returns: True if bid was accepted, False otherwise """ if not self.can_bid(amount): return False bid = Bid( bidder_id=bidder_id, bidder_name=bidder_name, amount=amount, ) self.bids.append(bid) self.current_bid = amount return True def buyout(self) -> bool: """ Attempt to buy out the auction immediately. Returns: True if buyout is available and successful, False otherwise """ if not self.is_auction() or not self.buyout_price: return False if not self.is_active() or self.has_ended(): return False self.current_bid = self.buyout_price self.status = ListingStatus.SOLD return True def get_winning_bidder(self) -> Optional[Bid]: """ Get the current winning bid. Returns: Winning Bid or None if no bids """ if not self.bids: return None # Bids are added chronologically, last one is highest return self.bids[-1] def cancel_listing(self) -> bool: """ Cancel this listing (seller action). Returns: True if successfully cancelled, False if cannot be cancelled """ if not self.is_active(): return False # Cannot cancel auction with bids if self.is_auction() and self.bids: return False self.status = ListingStatus.REMOVED return True def complete_sale(self) -> None: """Mark listing as sold.""" self.status = ListingStatus.SOLD def expire_listing(self) -> None: """Mark listing as expired.""" self.status = ListingStatus.EXPIRED def to_dict(self) -> Dict[str, Any]: """Serialize listing to dictionary.""" return { "listing_id": self.listing_id, "seller_id": self.seller_id, "character_id": self.character_id, "item_data": self.item_data.to_dict(), "listing_type": self.listing_type.value, "status": self.status.value, "created_at": self.created_at, "price": self.price, "starting_bid": self.starting_bid, "current_bid": self.current_bid, "buyout_price": self.buyout_price, "bids": [bid.to_dict() for bid in self.bids], "auction_end": self.auction_end, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'MarketplaceListing': """Deserialize listing from dictionary.""" item_data = Item.from_dict(data["item_data"]) listing_type = ListingType(data["listing_type"]) status = ListingStatus(data.get("status", "active")) bids = [Bid.from_dict(b) for b in data.get("bids", [])] return cls( listing_id=data["listing_id"], seller_id=data["seller_id"], character_id=data["character_id"], item_data=item_data, listing_type=listing_type, status=status, created_at=data.get("created_at", ""), price=data.get("price", 0), starting_bid=data.get("starting_bid", 0), current_bid=data.get("current_bid", 0), buyout_price=data.get("buyout_price", 0), bids=bids, auction_end=data.get("auction_end", ""), ) @dataclass class Transaction: """ Record of a completed transaction. Tracks all sales for auditing and analytics. Attributes: transaction_id: Unique identifier buyer_id: User ID of buyer seller_id: User ID of seller listing_id: Marketplace listing ID (if from marketplace) item_data: Item that was sold price: Final sale price in gold timestamp: ISO timestamp of transaction transaction_type: "marketplace_sale", "shop_purchase", etc. """ transaction_id: str buyer_id: str seller_id: str item_data: Item price: int transaction_type: str listing_id: str = "" timestamp: str = "" def __post_init__(self): """Initialize timestamp if not provided.""" if not self.timestamp: self.timestamp = datetime.utcnow().isoformat() def to_dict(self) -> Dict[str, Any]: """Serialize transaction to dictionary.""" return { "transaction_id": self.transaction_id, "buyer_id": self.buyer_id, "seller_id": self.seller_id, "listing_id": self.listing_id, "item_data": self.item_data.to_dict(), "price": self.price, "timestamp": self.timestamp, "transaction_type": self.transaction_type, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Transaction': """Deserialize transaction from dictionary.""" item_data = Item.from_dict(data["item_data"]) return cls( transaction_id=data["transaction_id"], buyer_id=data["buyer_id"], seller_id=data["seller_id"], listing_id=data.get("listing_id", ""), item_data=item_data, price=data["price"], timestamp=data.get("timestamp", ""), transaction_type=data["transaction_type"], ) @dataclass class ShopItem: """ Item sold by NPC shops. Attributes: item_id: Item identifier item: Item details stock: Available quantity (-1 = unlimited) price: Fixed gold price """ item_id: str item: Item stock: int = -1 # -1 = unlimited price: int = 0 def is_in_stock(self) -> bool: """Check if item is available for purchase.""" return self.stock != 0 def purchase(self, quantity: int = 1) -> bool: """ Attempt to purchase from stock. Args: quantity: Number of items to purchase Returns: True if purchase successful, False if insufficient stock """ if self.stock == -1: # Unlimited stock return True if self.stock < quantity: return False self.stock -= quantity return True def restock(self, quantity: int) -> None: """ Add stock to this shop item. Args: quantity: Amount to add to stock """ if self.stock == -1: # Unlimited, no need to restock return self.stock += quantity def to_dict(self) -> Dict[str, Any]: """Serialize shop item to dictionary.""" return { "item_id": self.item_id, "item": self.item.to_dict(), "stock": self.stock, "price": self.price, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'ShopItem': """Deserialize shop item from dictionary.""" item = Item.from_dict(data["item"]) return cls( item_id=data["item_id"], item=item, stock=data.get("stock", -1), price=data.get("price", 0), )