898 lines
27 KiB
JavaScript
898 lines
27 KiB
JavaScript
import axios from "axios";
|
|
import { authenticate } from "../middleware/auth.js";
|
|
import Item from "../models/Item.js";
|
|
import Trade from "../models/Trade.js";
|
|
import Transaction from "../models/Transaction.js";
|
|
import { config } from "../config/index.js";
|
|
import pricingService from "../services/pricing.js";
|
|
import marketPriceService from "../services/marketPrice.js";
|
|
import { getSteamBotManager } from "../services/steamBot.js";
|
|
|
|
/**
|
|
* Inventory routes for fetching and listing Steam items
|
|
* @param {FastifyInstance} fastify
|
|
* @param {Object} options
|
|
*/
|
|
export default async function inventoryRoutes(fastify, options) {
|
|
// GET /inventory/steam - Fetch user's Steam inventory
|
|
fastify.get(
|
|
"/steam",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
querystring: {
|
|
type: "object",
|
|
properties: {
|
|
game: { type: "string", enum: ["cs2", "rust"], default: "cs2" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { game = "cs2" } = request.query;
|
|
const steamId = request.user.steamId;
|
|
|
|
if (!steamId) {
|
|
return reply.status(400).send({
|
|
success: false,
|
|
message: "Steam ID not found",
|
|
});
|
|
}
|
|
|
|
// Map game to Steam app ID
|
|
const appIds = {
|
|
cs2: 730, // Counter-Strike 2
|
|
rust: 252490, // Rust
|
|
};
|
|
|
|
const appId = appIds[game];
|
|
const contextId = 2; // Standard Steam inventory context
|
|
|
|
console.log(
|
|
`🎮 Fetching ${game.toUpperCase()} inventory for Steam ID: ${steamId}`
|
|
);
|
|
|
|
// Get Steam API key from environment (check both possible env var names)
|
|
const steamApiKey =
|
|
process.env.STEAM_APIS_KEY ||
|
|
process.env.STEAM_API_KEY ||
|
|
config.steam?.apiKey;
|
|
|
|
if (!steamApiKey) {
|
|
console.error("❌ STEAM_API_KEY or STEAM_APIS_KEY not configured");
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Steam API is not configured. Please contact support.",
|
|
});
|
|
}
|
|
|
|
// Fetch from SteamAPIs.com
|
|
const steamApiUrl = `https://api.steamapis.com/steam/inventory/${steamId}/${appId}/${contextId}`;
|
|
|
|
console.log(`📡 Calling: ${steamApiUrl}`);
|
|
|
|
const response = await axios.get(steamApiUrl, {
|
|
params: {
|
|
api_key: steamApiKey,
|
|
},
|
|
timeout: 15000,
|
|
headers: {
|
|
"User-Agent": "TurboTrades/1.0",
|
|
},
|
|
});
|
|
|
|
if (!response.data || !response.data.assets) {
|
|
console.log("⚠️ Empty inventory or private profile");
|
|
return reply.send({
|
|
success: true,
|
|
items: [],
|
|
message: "Inventory is empty or private",
|
|
});
|
|
}
|
|
|
|
const { assets, descriptions } = response.data;
|
|
|
|
// Create a map of descriptions for quick lookup
|
|
const descMap = new Map();
|
|
descriptions.forEach((desc) => {
|
|
const key = `${desc.classid}_${desc.instanceid}`;
|
|
descMap.set(key, desc);
|
|
});
|
|
|
|
// Process items
|
|
const items = assets
|
|
.map((asset) => {
|
|
const key = `${asset.classid}_${asset.instanceid}`;
|
|
const desc = descMap.get(key);
|
|
|
|
if (!desc) return null;
|
|
|
|
// Parse item details
|
|
const item = {
|
|
assetid: asset.assetid,
|
|
appid: appId,
|
|
contextid: contextId,
|
|
classid: asset.classid,
|
|
instanceid: asset.instanceid,
|
|
name: desc.market_hash_name || desc.name || "Unknown Item",
|
|
type: desc.type || "",
|
|
image: desc.icon_url
|
|
? `https://community.cloudflare.steamstatic.com/economy/image/${desc.icon_url}`
|
|
: null,
|
|
nameColor: desc.name_color || null,
|
|
backgroundColor: desc.background_color || null,
|
|
marketable: desc.marketable === 1,
|
|
tradable: desc.tradable === 1,
|
|
commodity: desc.commodity === 1,
|
|
tags: desc.tags || [],
|
|
descriptions: desc.descriptions || [],
|
|
};
|
|
|
|
// Extract rarity from tags
|
|
const rarityTag = item.tags.find(
|
|
(tag) => tag.category === "Rarity"
|
|
);
|
|
if (rarityTag) {
|
|
item.rarity =
|
|
rarityTag.internal_name || rarityTag.localized_tag_name;
|
|
}
|
|
|
|
// Extract exterior (wear) from tags for CS2
|
|
if (game === "cs2") {
|
|
const exteriorTag = item.tags.find(
|
|
(tag) => tag.category === "Exterior"
|
|
);
|
|
if (exteriorTag) {
|
|
const wearMap = {
|
|
"Factory New": "fn",
|
|
"Minimal Wear": "mw",
|
|
"Field-Tested": "ft",
|
|
"Well-Worn": "ww",
|
|
"Battle-Scarred": "bs",
|
|
};
|
|
item.wear = wearMap[exteriorTag.localized_tag_name] || null;
|
|
item.wearName = exteriorTag.localized_tag_name;
|
|
}
|
|
}
|
|
|
|
// Extract category
|
|
const categoryTag = item.tags.find(
|
|
(tag) => tag.category === "Type" || tag.category === "Weapon"
|
|
);
|
|
if (categoryTag) {
|
|
item.category =
|
|
categoryTag.internal_name || categoryTag.localized_tag_name;
|
|
}
|
|
|
|
// Check if StatTrak or Souvenir
|
|
item.statTrak = item.name.includes("StatTrak™");
|
|
item.souvenir = item.name.includes("Souvenir");
|
|
|
|
// Detect phase for Doppler items
|
|
const descriptionText = item.descriptions
|
|
.map((d) => d.value || "")
|
|
.join(" ");
|
|
item.phase = pricingService.detectPhase(item.name, descriptionText);
|
|
|
|
return item;
|
|
})
|
|
.filter((item) => item !== null && item.marketable && item.tradable);
|
|
|
|
console.log(`✅ Found ${items.length} marketable items in inventory`);
|
|
|
|
// Enrich items with market prices (fast database lookup)
|
|
console.log(`💰 Adding market prices...`);
|
|
|
|
// Get all item names for batch lookup
|
|
const itemNames = items.map((item) => item.name);
|
|
console.log(`📋 Looking up prices for ${itemNames.length} items`);
|
|
console.log(`🎮 Game: ${game}`);
|
|
console.log(`📝 First 3 item names:`, itemNames.slice(0, 3));
|
|
|
|
const priceMap = await marketPriceService.getPrices(itemNames, game);
|
|
const foundPrices = Object.keys(priceMap).length;
|
|
console.log(
|
|
`💰 Found prices for ${foundPrices}/${itemNames.length} items`
|
|
);
|
|
|
|
// Add prices to items
|
|
const enrichedItems = items.map((item) => ({
|
|
...item,
|
|
marketPrice: priceMap[item.name] || null,
|
|
hasPriceData: !!priceMap[item.name],
|
|
}));
|
|
|
|
// Log items without prices
|
|
const itemsWithoutPrices = enrichedItems.filter(
|
|
(item) => !item.marketPrice
|
|
);
|
|
if (itemsWithoutPrices.length > 0) {
|
|
console.log(`⚠️ ${itemsWithoutPrices.length} items without prices:`);
|
|
itemsWithoutPrices.slice(0, 5).forEach((item) => {
|
|
console.log(` - ${item.name}`);
|
|
});
|
|
}
|
|
|
|
console.log(`✅ Prices added to ${enrichedItems.length} items`);
|
|
|
|
return reply.send({
|
|
success: true,
|
|
items: enrichedItems,
|
|
total: enrichedItems.length,
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ Error fetching Steam inventory:", error.message);
|
|
console.error("Error details:", error.response?.data || error.message);
|
|
|
|
if (error.response?.status === 401) {
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Steam API authentication failed. Please contact support.",
|
|
});
|
|
}
|
|
|
|
if (error.response?.status === 403) {
|
|
return reply.status(403).send({
|
|
success: false,
|
|
message:
|
|
"Steam inventory is private. Please make your inventory public in Steam settings.",
|
|
});
|
|
}
|
|
|
|
if (error.response?.status === 404) {
|
|
return reply.status(404).send({
|
|
success: false,
|
|
message: "Steam profile not found or inventory is empty.",
|
|
});
|
|
}
|
|
|
|
if (error.response?.status === 429) {
|
|
return reply.status(429).send({
|
|
success: false,
|
|
message:
|
|
"Steam API rate limit exceeded. Please try again in a few moments.",
|
|
});
|
|
}
|
|
|
|
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
|
return reply.status(504).send({
|
|
success: false,
|
|
message: "Steam API request timed out. Please try again.",
|
|
});
|
|
}
|
|
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Failed to fetch Steam inventory. Please try again later.",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// POST /inventory/price - Get pricing for items
|
|
fastify.post(
|
|
"/price",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["items"],
|
|
properties: {
|
|
items: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
required: ["name"],
|
|
properties: {
|
|
name: { type: "string" },
|
|
assetid: { type: "string" },
|
|
wear: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { items } = request.body;
|
|
|
|
// Use fast market price database for instant lookups
|
|
const pricedItems = await Promise.all(
|
|
items.map(async (item) => {
|
|
try {
|
|
// Use market price service for instant lookup (<1ms)
|
|
const marketPrice = await marketPriceService.getPrice(
|
|
item.name,
|
|
"cs2" // TODO: Get from request or item
|
|
);
|
|
|
|
return {
|
|
...item,
|
|
estimatedPrice: marketPrice,
|
|
currency: "USD",
|
|
hasPriceData: marketPrice !== null,
|
|
};
|
|
} catch (err) {
|
|
console.error(`Error pricing item ${item.name}:`, err.message);
|
|
return {
|
|
...item,
|
|
estimatedPrice: null,
|
|
currency: "USD",
|
|
hasPriceData: false,
|
|
error: "Price data not available",
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
// Filter out items without price data
|
|
const itemsWithPrices = pricedItems.filter(
|
|
(item) => item.estimatedPrice !== null
|
|
);
|
|
|
|
return reply.send({
|
|
success: true,
|
|
items: itemsWithPrices,
|
|
total: items.length,
|
|
priced: itemsWithPrices.length,
|
|
noPriceData: items.length - itemsWithPrices.length,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error pricing items:", error);
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Failed to calculate item prices",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// POST /inventory/sell - Create Steam trade offer to sell items
|
|
fastify.post(
|
|
"/sell",
|
|
{
|
|
preHandler: authenticate,
|
|
schema: {
|
|
body: {
|
|
type: "object",
|
|
required: ["items", "tradeUrl"],
|
|
properties: {
|
|
items: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
required: [
|
|
"assetid",
|
|
"appid",
|
|
"contextid",
|
|
"name",
|
|
"price",
|
|
"image",
|
|
],
|
|
properties: {
|
|
assetid: { type: "string" },
|
|
appid: { type: "number" },
|
|
contextid: { type: "string" },
|
|
name: { type: "string" },
|
|
price: { type: "number" },
|
|
image: { type: "string" },
|
|
wear: { type: "string" },
|
|
rarity: { type: "string" },
|
|
category: { type: "string" },
|
|
statTrak: { type: "boolean" },
|
|
souvenir: { type: "boolean" },
|
|
},
|
|
},
|
|
},
|
|
tradeUrl: { type: "string" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (request, reply) => {
|
|
// Declare variables outside try block for catch block access
|
|
let userId, steamId, items, tradeUrl, botManager, bypassBots;
|
|
|
|
try {
|
|
items = request.body.items;
|
|
tradeUrl = request.body.tradeUrl;
|
|
userId = request.user._id;
|
|
steamId = request.user.steamId;
|
|
|
|
console.log("🔍 Sell endpoint called:", {
|
|
userId,
|
|
steamId,
|
|
itemCount: items?.length,
|
|
hasTradeUrl: !!tradeUrl,
|
|
nodeEnv: process.env.NODE_ENV,
|
|
bypassBot: process.env.BYPASS_BOT_REQUIREMENT,
|
|
});
|
|
|
|
if (!items || items.length === 0) {
|
|
return reply.status(400).send({
|
|
success: false,
|
|
message: "No items selected",
|
|
});
|
|
}
|
|
|
|
if (!tradeUrl) {
|
|
return reply.status(400).send({
|
|
success: false,
|
|
message:
|
|
"Trade URL is required. Please set your trade URL in your profile.",
|
|
});
|
|
}
|
|
|
|
// Calculate total value
|
|
const totalValue = items.reduce((sum, item) => sum + item.price, 0);
|
|
|
|
// Get bot manager
|
|
botManager = getSteamBotManager();
|
|
|
|
// In development mode, allow bypassing bot requirement
|
|
const isDevelopmentMode = process.env.NODE_ENV === "development";
|
|
bypassBots = process.env.BYPASS_BOT_REQUIREMENT === "true";
|
|
|
|
if (!botManager.isInitialized && !bypassBots) {
|
|
return reply.status(503).send({
|
|
success: false,
|
|
message:
|
|
"Trade system is currently unavailable. Please try again later.",
|
|
});
|
|
}
|
|
|
|
// Development mode: Mock trade creation
|
|
if (bypassBots || !botManager.isInitialized) {
|
|
console.log(
|
|
"⚠️ DEVELOPMENT MODE: Creating mock trade (no real Steam bot)"
|
|
);
|
|
|
|
// Generate mock verification code
|
|
const mockCode = Math.random()
|
|
.toString(36)
|
|
.substring(2, 8)
|
|
.toUpperCase();
|
|
const mockOfferId = `DEV_${Date.now()}`;
|
|
const mockTradeOfferUrl = `https://steamcommunity.com/tradeoffer/${mockOfferId}`;
|
|
|
|
// Create trade record in database
|
|
const trade = new Trade({
|
|
offerId: mockOfferId,
|
|
botId: "dev-bot",
|
|
userId: userId,
|
|
steamId: steamId,
|
|
tradeUrl: tradeUrl,
|
|
tradeOfferUrl: mockTradeOfferUrl,
|
|
items: items.map((item) => ({
|
|
assetId: item.assetid,
|
|
name: item.name,
|
|
price: item.price,
|
|
image: item.image,
|
|
game: item.appid === 730 ? "cs2" : "rust",
|
|
wear: item.wear,
|
|
rarity: item.rarity,
|
|
category: item.category,
|
|
statTrak: item.statTrak,
|
|
souvenir: item.souvenir,
|
|
})),
|
|
totalValue,
|
|
userReceives: totalValue,
|
|
verificationCode: mockCode,
|
|
state: "pending",
|
|
});
|
|
|
|
await trade.save();
|
|
|
|
console.log(
|
|
`✅ Mock trade created: ${mockOfferId} with code: ${mockCode}`
|
|
);
|
|
|
|
// Send WebSocket notification
|
|
if (fastify.websocketManager) {
|
|
fastify.websocketManager.sendToUser(steamId, {
|
|
type: "trade_created",
|
|
data: {
|
|
tradeId: trade._id,
|
|
offerId: mockOfferId,
|
|
verificationCode: mockCode,
|
|
tradeOfferUrl: mockTradeOfferUrl,
|
|
itemCount: items.length,
|
|
totalValue,
|
|
botId: "dev-bot",
|
|
status: "pending",
|
|
timestamp: Date.now(),
|
|
isDevelopment: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Mock trade created (Development Mode)",
|
|
trade: {
|
|
tradeId: trade._id,
|
|
offerId: mockOfferId,
|
|
verificationCode: mockCode,
|
|
tradeOfferUrl: mockTradeOfferUrl,
|
|
itemCount: items.length,
|
|
totalValue,
|
|
status: "pending",
|
|
botId: "dev-bot",
|
|
isDevelopment: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Prepare items for Steam trade (format required by steam-tradeoffer-manager)
|
|
const itemsToReceive = items.map((item) => ({
|
|
assetid: item.assetid,
|
|
appid: item.appid,
|
|
contextid: item.contextid,
|
|
}));
|
|
|
|
console.log(
|
|
`📤 Creating trade offer for user ${request.user.username} (${
|
|
items.length
|
|
} items, $${totalValue.toFixed(2)})`
|
|
);
|
|
|
|
// Create trade offer via bot
|
|
let tradeResult;
|
|
try {
|
|
tradeResult = await botManager.createTradeOffer({
|
|
tradeUrl,
|
|
itemsToReceive,
|
|
userId: steamId,
|
|
metadata: {
|
|
username: request.user.username,
|
|
itemCount: items.length,
|
|
totalValue,
|
|
},
|
|
});
|
|
} catch (botError) {
|
|
console.error("Bot trade creation error:", botError);
|
|
return reply.status(503).send({
|
|
success: false,
|
|
message:
|
|
botError.message ||
|
|
"Failed to create trade offer. Please try again.",
|
|
});
|
|
}
|
|
|
|
// Create trade record in database
|
|
const trade = new Trade({
|
|
offerId: tradeResult.offerId,
|
|
botId: tradeResult.botId,
|
|
userId: userId,
|
|
steamId: steamId,
|
|
tradeUrl: tradeUrl,
|
|
tradeOfferUrl: tradeResult.tradeOfferUrl,
|
|
items: items.map((item) => ({
|
|
assetId: item.assetid,
|
|
name: item.name,
|
|
price: item.price,
|
|
image: item.image,
|
|
game: item.appid === 730 ? "cs2" : "rust",
|
|
wear: item.wear,
|
|
rarity: item.rarity,
|
|
category: item.category,
|
|
statTrak: item.statTrak,
|
|
souvenir: item.souvenir,
|
|
})),
|
|
totalValue,
|
|
userReceives: totalValue,
|
|
verificationCode: tradeResult.code,
|
|
state: "pending",
|
|
});
|
|
|
|
await trade.save();
|
|
|
|
console.log(
|
|
`✅ Trade offer created: ${tradeResult.offerId} with verification code: ${tradeResult.code}`
|
|
);
|
|
|
|
// Send immediate WebSocket notification with verification code
|
|
if (fastify.websocketManager) {
|
|
fastify.websocketManager.sendToUser(steamId, {
|
|
type: "trade_created",
|
|
data: {
|
|
tradeId: trade._id,
|
|
offerId: tradeResult.offerId,
|
|
verificationCode: tradeResult.code,
|
|
tradeOfferUrl: tradeResult.tradeOfferUrl,
|
|
itemCount: items.length,
|
|
totalValue,
|
|
botId: tradeResult.botId,
|
|
status: "pending",
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Trade offer created successfully",
|
|
trade: {
|
|
tradeId: trade._id,
|
|
offerId: tradeResult.offerId,
|
|
verificationCode: tradeResult.code,
|
|
tradeOfferUrl: tradeResult.tradeOfferUrl,
|
|
itemCount: items.length,
|
|
totalValue,
|
|
status: "pending",
|
|
botId: tradeResult.botId,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("❌ Error creating sell trade:", error);
|
|
console.error("Error stack:", error.stack);
|
|
console.error("Error details:", {
|
|
message: error.message,
|
|
name: error.name,
|
|
userId,
|
|
steamId,
|
|
itemCount: items?.length,
|
|
tradeUrl: tradeUrl ? "present" : "missing",
|
|
bypassBots,
|
|
isInitialized: botManager?.isInitialized,
|
|
});
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message:
|
|
error.message || "Failed to create trade offer. Please try again.",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// GET /inventory/trades - Get user's trades
|
|
fastify.get(
|
|
"/trades",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const userId = request.user._id;
|
|
|
|
const trades = await Trade.find({ user: userId })
|
|
.sort({ createdAt: -1 })
|
|
.limit(50);
|
|
|
|
return reply.send({
|
|
success: true,
|
|
trades,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching trades:", error);
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Failed to fetch trades",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// GET /inventory/trade/:tradeId - Get specific trade details
|
|
fastify.get(
|
|
"/trade/:tradeId",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { tradeId } = request.params;
|
|
const userId = request.user._id;
|
|
|
|
const trade = await Trade.findOne({ _id: tradeId, userId: userId });
|
|
|
|
if (!trade) {
|
|
return reply.status(404).send({
|
|
success: false,
|
|
message: "Trade not found",
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
trade,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching trade:", error);
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Failed to fetch trade details",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// POST /inventory/trade/:tradeId/cancel - Cancel a pending trade
|
|
fastify.post(
|
|
"/trade/:tradeId/cancel",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { tradeId } = request.params;
|
|
const userId = request.user._id;
|
|
const steamId = request.user.steamId;
|
|
|
|
const trade = await Trade.findOne({ _id: tradeId, userId: userId });
|
|
|
|
if (!trade) {
|
|
return reply.status(404).send({
|
|
success: false,
|
|
message: "Trade not found",
|
|
});
|
|
}
|
|
|
|
if (trade.state !== "pending") {
|
|
return reply.status(400).send({
|
|
success: false,
|
|
message: "Only pending trades can be cancelled",
|
|
});
|
|
}
|
|
|
|
// Cancel via bot manager
|
|
const botManager = getSteamBotManager();
|
|
await botManager.cancelTradeOffer(trade.offerId, trade.botId);
|
|
|
|
// Update trade status
|
|
trade.state = "canceled";
|
|
await trade.save();
|
|
|
|
// Notify via WebSocket
|
|
if (fastify.websocketManager) {
|
|
fastify.websocketManager.sendToUser(steamId, {
|
|
type: "trade_cancelled",
|
|
data: {
|
|
tradeId: trade._id,
|
|
offerId: trade.offerId,
|
|
timestamp: Date.now(),
|
|
},
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Trade cancelled successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Error cancelling trade:", error);
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Failed to cancel trade",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
|
|
// POST /inventory/trade/:tradeId/complete - Complete trade (development mode only)
|
|
fastify.post(
|
|
"/trade/:tradeId/complete",
|
|
{
|
|
preHandler: authenticate,
|
|
},
|
|
async (request, reply) => {
|
|
try {
|
|
const { tradeId } = request.params;
|
|
const userId = request.user._id;
|
|
const steamId = request.user.steamId;
|
|
|
|
// Only allow in development mode
|
|
if (
|
|
process.env.NODE_ENV !== "development" &&
|
|
process.env.BYPASS_BOT_REQUIREMENT !== "true"
|
|
) {
|
|
return reply.status(403).send({
|
|
success: false,
|
|
message: "This endpoint is only available in development mode",
|
|
});
|
|
}
|
|
|
|
const trade = await Trade.findOne({ _id: tradeId, user: userId });
|
|
|
|
if (!trade) {
|
|
return reply.status(404).send({
|
|
success: false,
|
|
message: "Trade not found",
|
|
});
|
|
}
|
|
|
|
if (trade.state !== "pending") {
|
|
return reply.status(400).send({
|
|
success: false,
|
|
message: "Only pending trades can be completed",
|
|
});
|
|
}
|
|
|
|
// Simulate trade acceptance
|
|
console.log(
|
|
`🧪 DEV MODE: Manually completing trade ${tradeId} for user ${request.user.username}`
|
|
);
|
|
|
|
// Credit user balance
|
|
request.user.balance += trade.totalValue;
|
|
await request.user.save();
|
|
|
|
// Update trade status
|
|
trade.state = "accepted";
|
|
trade.completedAt = new Date();
|
|
await trade.save();
|
|
|
|
// Create transaction record
|
|
const transaction = new Transaction({
|
|
user: userId,
|
|
type: "sale",
|
|
amount: trade.totalValue,
|
|
description: `Sold ${trade.items.length} item(s) (DEV MODE)`,
|
|
status: "completed",
|
|
metadata: {
|
|
tradeId: trade._id,
|
|
offerId: trade.offerId,
|
|
botId: trade.botId,
|
|
itemCount: trade.items.length,
|
|
verificationCode: trade.verificationCode,
|
|
isDevelopment: true,
|
|
},
|
|
});
|
|
await transaction.save();
|
|
|
|
console.log(
|
|
`✅ DEV MODE: Credited $${trade.totalValue.toFixed(2)} to user ${
|
|
request.user.username
|
|
} (Balance: $${request.user.balance.toFixed(2)})`
|
|
);
|
|
|
|
// Notify via WebSocket
|
|
if (fastify.websocketManager) {
|
|
fastify.websocketManager.sendToUser(steamId, {
|
|
type: "trade_completed",
|
|
data: {
|
|
tradeId: trade._id,
|
|
offerId: trade.offerId,
|
|
amount: trade.totalValue,
|
|
newBalance: request.user.balance,
|
|
itemCount: trade.items.length,
|
|
timestamp: Date.now(),
|
|
isDevelopment: true,
|
|
},
|
|
});
|
|
|
|
fastify.websocketManager.sendToUser(steamId, {
|
|
type: "balance_update",
|
|
data: {
|
|
balance: request.user.balance,
|
|
change: trade.totalValue,
|
|
reason: "trade_completed",
|
|
},
|
|
});
|
|
}
|
|
|
|
return reply.send({
|
|
success: true,
|
|
message: "Trade completed successfully (Development Mode)",
|
|
trade: {
|
|
tradeId: trade._id,
|
|
status: "completed",
|
|
amount: trade.totalValue,
|
|
newBalance: request.user.balance,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("Error completing trade:", error);
|
|
return reply.status(500).send({
|
|
success: false,
|
|
message: "Failed to complete trade",
|
|
});
|
|
}
|
|
}
|
|
);
|
|
}
|