import axios from "axios"; import { authenticate } from "../middleware/auth.js"; import Item from "../models/Item.js"; import { config } from "../config/index.js"; import pricingService from "../services/pricing.js"; import marketPriceService from "../services/marketPrice.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, 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 - Sell items to the site fastify.post( "/sell", { preHandler: authenticate, schema: { body: { type: "object", required: ["items"], properties: { items: { type: "array", items: { type: "object", required: ["assetid", "name", "price", "image"], properties: { assetid: { 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" }, }, }, }, }, }, }, }, async (request, reply) => { try { const { items } = request.body; const userId = request.user._id; const steamId = request.user.steamId; if (!items || items.length === 0) { return reply.status(400).send({ success: false, message: "No items selected", }); } // Calculate total value const totalValue = items.reduce((sum, item) => sum + item.price, 0); // Add items to marketplace const createdItems = []; for (const item of items) { // Determine game based on item characteristics const game = item.category?.includes("Rust") ? "rust" : "cs2"; // Map category let categoryMapped = "other"; if (item.category) { const cat = item.category.toLowerCase(); if (cat.includes("rifle")) categoryMapped = "rifles"; else if (cat.includes("pistol")) categoryMapped = "pistols"; else if (cat.includes("knife")) categoryMapped = "knives"; else if (cat.includes("glove")) categoryMapped = "gloves"; else if (cat.includes("smg")) categoryMapped = "smgs"; else if (cat.includes("sticker")) categoryMapped = "stickers"; } // Map rarity let rarityMapped = "common"; if (item.rarity) { const rar = item.rarity.toLowerCase(); if (rar.includes("contraband") || rar.includes("ancient")) rarityMapped = "exceedingly"; else if (rar.includes("covert") || rar.includes("legendary")) rarityMapped = "legendary"; else if (rar.includes("classified") || rar.includes("mythical")) rarityMapped = "mythical"; else if (rar.includes("restricted") || rar.includes("rare")) rarityMapped = "rare"; else if (rar.includes("mil-spec") || rar.includes("uncommon")) rarityMapped = "uncommon"; } const newItem = new Item({ name: item.name, description: `Listed from Steam inventory`, image: item.image, game: game, category: categoryMapped, rarity: rarityMapped, wear: item.wear || null, statTrak: item.statTrak || false, souvenir: item.souvenir || false, price: item.price, seller: userId, status: "active", featured: false, }); await newItem.save(); createdItems.push(newItem); } // Update user balance request.user.balance += totalValue; await request.user.save(); console.log( `โœ… User ${request.user.username} sold ${ items.length } items for $${totalValue.toFixed(2)}` ); // Broadcast to WebSocket if available if (fastify.websocketManager) { // Update user's balance fastify.websocketManager.sendToUser(steamId, { type: "balance_update", data: { balance: request.user.balance, }, }); // Broadcast new items to marketplace fastify.websocketManager.broadcastPublic("new_items", { count: createdItems.length, }); } return reply.send({ success: true, message: `Successfully sold ${items.length} item${ items.length > 1 ? "s" : "" } for $${totalValue.toFixed(2)}`, itemsListed: createdItems.length, totalEarned: totalValue, newBalance: request.user.balance, }); } catch (error) { console.error("Error selling items:", error); return reply.status(500).send({ success: false, message: "Failed to process sale. Please try again.", }); } } ); }