443 lines
11 KiB
JavaScript
443 lines
11 KiB
JavaScript
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',
|
|
});
|
|
}
|
|
});
|
|
}
|