first commit
This commit is contained in:
442
routes/market.js
Normal file
442
routes/market.js
Normal file
@@ -0,0 +1,442 @@
|
||||
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',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user