import Fastify from 'fastify'; import Item from '../models/Item.js'; import { authenticate, optionalAuthenticate } from '../middleware/auth.js'; /** * Market routes for browsing and purchasing items * @param {FastifyInstance} fastify * @param {Object} options */ export default async function marketRoutes(fastify, options) { // GET /market/items - Browse marketplace items fastify.get('/items', { preHandler: optionalAuthenticate, schema: { querystring: { type: 'object', properties: { page: { type: 'integer', minimum: 1, default: 1 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 24 }, search: { type: 'string' }, game: { type: 'string', enum: ['cs2', 'rust'] }, category: { type: 'string' }, rarity: { type: 'string' }, wear: { type: 'string' }, minPrice: { type: 'number', minimum: 0 }, maxPrice: { type: 'number', minimum: 0 }, sortBy: { type: 'string', enum: ['price_asc', 'price_desc', 'name_asc', 'name_desc', 'date_new', 'date_old'], default: 'date_new' }, statTrak: { type: 'boolean' }, souvenir: { type: 'boolean' }, } } } }, async (request, reply) => { try { const { page = 1, limit = 24, search, game, category, rarity, wear, minPrice, maxPrice, sortBy = 'date_new', statTrak, souvenir, } = request.query; // Build query const query = { status: 'active' }; if (search) { query.$or = [ { name: { $regex: search, $options: 'i' } }, { description: { $regex: search, $options: 'i' } } ]; } if (game) query.game = game; if (category && category !== 'all') query.category = category; if (rarity) query.rarity = rarity; if (wear) query.wear = wear; if (statTrak !== undefined) query.statTrak = statTrak; if (souvenir !== undefined) query.souvenir = souvenir; if (minPrice !== undefined || maxPrice !== undefined) { query.price = {}; if (minPrice !== undefined) query.price.$gte = minPrice; if (maxPrice !== undefined) query.price.$lte = maxPrice; } // Build sort let sort = {}; switch (sortBy) { case 'price_asc': sort = { price: 1 }; break; case 'price_desc': sort = { price: -1 }; break; case 'name_asc': sort = { name: 1 }; break; case 'name_desc': sort = { name: -1 }; break; case 'date_new': sort = { listedAt: -1 }; break; case 'date_old': sort = { listedAt: 1 }; break; default: sort = { listedAt: -1 }; } // Execute query with pagination const skip = (page - 1) * limit; const [items, total] = await Promise.all([ Item.find(query) .sort(sort) .skip(skip) .limit(limit) .populate('seller', 'username avatar steamId') .lean(), Item.countDocuments(query) ]); return reply.send({ success: true, items, page, limit, total, totalPages: Math.ceil(total / limit), }); } catch (error) { fastify.log.error(error); return reply.status(500).send({ success: false, message: 'Failed to fetch items', }); } }); // GET /market/featured - Get featured items fastify.get('/featured', { preHandler: optionalAuthenticate, }, async (request, reply) => { try { const items = await Item.find({ status: 'active', featured: true }) .sort({ listedAt: -1 }) .limit(12) .populate('seller', 'username avatar steamId') .lean(); return reply.send({ success: true, items, }); } catch (error) { fastify.log.error(error); return reply.status(500).send({ success: false, message: 'Failed to fetch featured items', }); } }); // GET /market/recent-sales - Get recent sales fastify.get('/recent-sales', { preHandler: optionalAuthenticate, schema: { querystring: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 50, default: 10 }, } } } }, async (request, reply) => { try { const { limit = 10 } = request.query; const sales = await Item.find({ status: 'sold' }) .sort({ soldAt: -1 }) .limit(limit) .populate('seller', 'username avatar steamId') .populate('buyer', 'username avatar steamId') .lean(); // Format for frontend const formattedSales = sales.map(sale => ({ id: sale._id, itemName: sale.name, itemImage: sale.image, wear: sale.wear, price: sale.price, soldAt: sale.soldAt, seller: sale.seller, buyer: sale.buyer, })); return reply.send({ success: true, sales: formattedSales, }); } catch (error) { fastify.log.error(error); return reply.status(500).send({ success: false, message: 'Failed to fetch recent sales', }); } }); // GET /market/items/:id - Get single item details fastify.get('/items/:id', { preHandler: optionalAuthenticate, schema: { params: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } } } }, async (request, reply) => { try { const { id } = request.params; const item = await Item.findById(id) .populate('seller', 'username avatar steamId') .lean(); if (!item) { return reply.status(404).send({ success: false, message: 'Item not found', }); } // Increment views (don't await) Item.findByIdAndUpdate(id, { $inc: { views: 1 } }).exec(); return reply.send({ success: true, item, }); } catch (error) { fastify.log.error(error); return reply.status(500).send({ success: false, message: 'Failed to fetch item', }); } }); // POST /market/purchase/:id - Purchase an item fastify.post('/purchase/:id', { preHandler: authenticate, schema: { params: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } } } }, async (request, reply) => { try { const { id } = request.params; const userId = request.user.userId; // Find item const item = await Item.findById(id); if (!item) { return reply.status(404).send({ success: false, message: 'Item not found', }); } if (item.status !== 'active') { return reply.status(400).send({ success: false, message: 'Item is not available for purchase', }); } // Check if user is trying to buy their own item if (item.seller.toString() === userId) { return reply.status(400).send({ success: false, message: 'You cannot purchase your own item', }); } // Get user to check balance const User = fastify.mongoose.model('User'); const user = await User.findById(userId); if (!user) { return reply.status(404).send({ success: false, message: 'User not found', }); } if (user.balance < item.price) { return reply.status(400).send({ success: false, message: 'Insufficient balance', }); } // Check if user has trade URL set if (!user.tradeUrl) { return reply.status(400).send({ success: false, message: 'Please set your trade URL in profile settings', }); } // Process purchase (in a transaction would be better) // Deduct from buyer user.balance -= item.price; await user.save(); // Add to seller const seller = await User.findById(item.seller); if (seller) { seller.balance += item.price; await seller.save(); } // Mark item as sold await item.markAsSold(userId); // TODO: Send trade offer via Steam bot // TODO: Send notifications via WebSocket // Broadcast to WebSocket clients if (fastify.websocketManager) { fastify.websocketManager.broadcastPublic('item_sold', { itemId: item._id, itemName: item.name, price: item.price, }); // Send notification to buyer fastify.websocketManager.sendToUser(user.steamId, { type: 'item_purchased', data: { itemId: item._id, itemName: item.name, price: item.price, } }); // Send notification to seller if (seller) { fastify.websocketManager.sendToUser(seller.steamId, { type: 'item_sold', data: { itemId: item._id, itemName: item.name, price: item.price, buyer: user.username, } }); } // Update balance for buyer fastify.websocketManager.sendToUser(user.steamId, { type: 'balance_update', data: { balance: user.balance, } }); // Update balance for seller if (seller) { fastify.websocketManager.sendToUser(seller.steamId, { type: 'balance_update', data: { balance: seller.balance, } }); } } return reply.send({ success: true, message: 'Purchase successful! You will receive a trade offer shortly.', item, newBalance: user.balance, }); } catch (error) { fastify.log.error(error); return reply.status(500).send({ success: false, message: 'Failed to process purchase', }); } }); // GET /market/stats - Get marketplace statistics fastify.get('/stats', { preHandler: optionalAuthenticate, }, async (request, reply) => { try { const [ totalActive, totalSold, totalValue, averagePrice, ] = await Promise.all([ Item.countDocuments({ status: 'active' }), Item.countDocuments({ status: 'sold' }), Item.aggregate([ { $match: { status: 'active' } }, { $group: { _id: null, total: { $sum: '$price' } } } ]), Item.aggregate([ { $match: { status: 'active' } }, { $group: { _id: null, avg: { $avg: '$price' } } } ]), ]); return reply.send({ success: true, stats: { totalActive, totalSold, totalValue: totalValue[0]?.total || 0, averagePrice: averagePrice[0]?.avg || 0, } }); } catch (error) { fastify.log.error(error); return reply.status(500).send({ success: false, message: 'Failed to fetch statistics', }); } }); }