import { authenticate, optionalAuthenticate } from "../middleware/auth.js"; import { wsManager } from "../utils/websocket.js"; /** * Example marketplace routes demonstrating WebSocket broadcasting * This shows how to integrate real-time updates for listings, prices, etc. * @param {FastifyInstance} fastify - Fastify instance */ export default async function marketplaceRoutes(fastify, options) { // Get all listings (public endpoint with optional auth for user-specific data) fastify.get( "/marketplace/listings", { preHandler: optionalAuthenticate, schema: { querystring: { type: "object", properties: { game: { type: "string", enum: ["cs2", "rust"] }, minPrice: { type: "number" }, maxPrice: { type: "number" }, search: { type: "string" }, page: { type: "number", default: 1 }, limit: { type: "number", default: 20, maximum: 100 }, }, }, }, }, async (request, reply) => { try { const { game, minPrice, maxPrice, search, page = 1, limit = 20 } = request.query; // TODO: Implement actual database query // This is a placeholder showing the structure const listings = { items: [ { id: "listing_123", itemName: "AK-47 | Redline", game: "cs2", price: 45.99, seller: { steamId: "76561198012345678", username: "TraderPro", }, condition: "Field-Tested", float: 0.23, createdAt: new Date(), }, ], pagination: { page, limit, total: 1, pages: 1, }, }; return reply.send({ success: true, listings: listings.items, pagination: listings.pagination, }); } catch (error) { console.error("Error fetching listings:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to fetch listings", }); } } ); // Create new listing (authenticated users only) fastify.post( "/marketplace/listings", { preHandler: authenticate, schema: { body: { type: "object", required: ["itemName", "game", "price"], properties: { itemName: { type: "string" }, game: { type: "string", enum: ["cs2", "rust"] }, price: { type: "number", minimum: 0.01 }, description: { type: "string", maxLength: 500 }, assetId: { type: "string" }, condition: { type: "string" }, float: { type: "number" }, }, }, }, }, async (request, reply) => { try { const { itemName, game, price, description, assetId, condition, float } = request.body; const user = request.user; // Verify user has verified email (optional security check) if (!user.email?.verified) { return reply.status(403).send({ error: "Forbidden", message: "Email verification required to create listings", }); } // Verify user has trade URL set if (!user.tradeUrl) { return reply.status(400).send({ error: "ValidationError", message: "Trade URL must be set before creating listings", }); } // TODO: Implement actual listing creation in database const newListing = { id: `listing_${Date.now()}`, itemName, game, price, description, assetId, condition, float, seller: { id: user._id, steamId: user.steamId, username: user.username, avatar: user.avatar, }, status: "active", createdAt: new Date(), }; // Broadcast new listing to all connected clients wsManager.broadcastPublic("new_listing", { listing: newListing, message: `New ${game.toUpperCase()} item listed: ${itemName}`, }); console.log(`📢 Broadcasted new listing: ${itemName} by ${user.username}`); return reply.status(201).send({ success: true, message: "Listing created successfully", listing: newListing, }); } catch (error) { console.error("Error creating listing:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to create listing", }); } } ); // Update listing price (seller only) fastify.patch( "/marketplace/listings/:listingId/price", { preHandler: authenticate, schema: { params: { type: "object", required: ["listingId"], properties: { listingId: { type: "string" }, }, }, body: { type: "object", required: ["price"], properties: { price: { type: "number", minimum: 0.01 }, }, }, }, }, async (request, reply) => { try { const { listingId } = request.params; const { price } = request.body; const user = request.user; // TODO: Fetch listing from database and verify ownership // This is a placeholder const listing = { id: listingId, itemName: "AK-47 | Redline", game: "cs2", oldPrice: 45.99, seller: { id: user._id.toString(), steamId: user.steamId, username: user.username, }, }; // Verify user owns the listing if (listing.seller.id !== user._id.toString()) { return reply.status(403).send({ error: "Forbidden", message: "You can only update your own listings", }); } // Update price (TODO: Update in database) const updatedListing = { ...listing, price, updatedAt: new Date(), }; // Broadcast price update to all clients wsManager.broadcastPublic("price_update", { listingId, itemName: listing.itemName, oldPrice: listing.oldPrice, newPrice: price, percentChange: ((price - listing.oldPrice) / listing.oldPrice * 100).toFixed(2), }); console.log(`📢 Broadcasted price update for listing ${listingId}`); return reply.send({ success: true, message: "Price updated successfully", listing: updatedListing, }); } catch (error) { console.error("Error updating listing price:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to update listing price", }); } } ); // Purchase item fastify.post( "/marketplace/listings/:listingId/purchase", { preHandler: authenticate, schema: { params: { type: "object", required: ["listingId"], properties: { listingId: { type: "string" }, }, }, }, }, async (request, reply) => { try { const { listingId } = request.params; const buyer = request.user; // TODO: Fetch listing from database const listing = { id: listingId, itemName: "AK-47 | Redline", game: "cs2", price: 45.99, seller: { id: "different_user_id", steamId: "76561198012345678", username: "TraderPro", }, status: "active", }; // Prevent self-purchase if (listing.seller.id === buyer._id.toString()) { return reply.status(400).send({ error: "ValidationError", message: "You cannot purchase your own listing", }); } // Check if listing is still active if (listing.status !== "active") { return reply.status(400).send({ error: "ValidationError", message: "This listing is no longer available", }); } // Check buyer balance if (buyer.balance < listing.price) { return reply.status(400).send({ error: "InsufficientFunds", message: `Insufficient balance. Required: $${listing.price}, Available: $${buyer.balance}`, }); } // TODO: Process transaction in database // - Deduct from buyer balance // - Add to seller balance // - Create transaction record // - Update listing status to "sold" // - Create trade offer via Steam API const transaction = { id: `tx_${Date.now()}`, listingId, itemName: listing.itemName, price: listing.price, buyer: { id: buyer._id, steamId: buyer.steamId, username: buyer.username, }, seller: listing.seller, status: "processing", createdAt: new Date(), }; // Notify seller via WebSocket wsManager.sendToUser(listing.seller.id, { type: "item_sold", data: { transaction, message: `Your ${listing.itemName} has been sold for $${listing.price}!`, }, }); // Notify buyer wsManager.sendToUser(buyer._id.toString(), { type: "purchase_confirmed", data: { transaction, message: `Purchase confirmed! Trade offer will be sent shortly.`, }, }); // Broadcast listing removal to all clients wsManager.broadcastPublic("listing_sold", { listingId, itemName: listing.itemName, price: listing.price, }); console.log(`💰 Item sold: ${listing.itemName} - Buyer: ${buyer.username}, Seller: ${listing.seller.username}`); return reply.send({ success: true, message: "Purchase successful! Trade offer will be sent to your Steam account.", transaction, }); } catch (error) { console.error("Error processing purchase:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to process purchase", }); } } ); // Delete listing (seller or admin) fastify.delete( "/marketplace/listings/:listingId", { preHandler: authenticate, schema: { params: { type: "object", required: ["listingId"], properties: { listingId: { type: "string" }, }, }, }, }, async (request, reply) => { try { const { listingId } = request.params; const user = request.user; // TODO: Fetch listing from database const listing = { id: listingId, itemName: "AK-47 | Redline", seller: { id: user._id.toString(), steamId: user.steamId, username: user.username, }, }; // Check permissions (owner or admin) if (listing.seller.id !== user._id.toString() && user.staffLevel < 3) { return reply.status(403).send({ error: "Forbidden", message: "You can only delete your own listings", }); } // TODO: Delete from database // Broadcast listing removal wsManager.broadcastPublic("listing_removed", { listingId, itemName: listing.itemName, reason: user.staffLevel >= 3 ? "Removed by admin" : "Removed by seller", }); console.log(`🗑️ Listing deleted: ${listingId} by ${user.username}`); return reply.send({ success: true, message: "Listing deleted successfully", }); } catch (error) { console.error("Error deleting listing:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to delete listing", }); } } ); // Get user's own listings fastify.get( "/marketplace/my-listings", { preHandler: authenticate, schema: { querystring: { type: "object", properties: { status: { type: "string", enum: ["active", "sold", "cancelled"] }, page: { type: "number", default: 1 }, limit: { type: "number", default: 20, maximum: 100 }, }, }, }, }, async (request, reply) => { try { const { status, page = 1, limit = 20 } = request.query; const user = request.user; // TODO: Fetch user's listings from database const listings = { items: [], pagination: { page, limit, total: 0, pages: 0, }, }; return reply.send({ success: true, listings: listings.items, pagination: listings.pagination, }); } catch (error) { console.error("Error fetching user listings:", error); return reply.status(500).send({ error: "InternalServerError", message: "Failed to fetch your listings", }); } } ); }