first commit

This commit is contained in:
2026-01-10 04:57:43 +00:00
parent 16a76a2cd6
commit 232968de1e
131 changed files with 43262 additions and 0 deletions

416
services/pricing.js Normal file
View File

@@ -0,0 +1,416 @@
import axios from "axios";
import Item from "../models/Item.js";
/**
* Pricing Service
* Fetches and updates item prices from SteamAPIs.com
*/
class PricingService {
constructor() {
this.apiKey = process.env.STEAM_APIS_KEY || process.env.STEAM_API_KEY;
this.baseUrl = "https://api.steamapis.com";
this.appIds = {
cs2: 730,
rust: 252490,
};
this.lastUpdate = {};
this.updateInterval = 60 * 60 * 1000; // 1 hour in milliseconds
}
/**
* Detect phase from item name and description
* @param {string} name - Item name
* @param {string} description - Item description
* @returns {string|null} - Phase name or null
*/
detectPhase(name, description = "") {
const combinedText = `${name} ${description}`.toLowerCase();
// Doppler phases
if (combinedText.includes("ruby")) return "Ruby";
if (combinedText.includes("sapphire")) return "Sapphire";
if (combinedText.includes("black pearl")) return "Black Pearl";
if (combinedText.includes("emerald")) return "Emerald";
if (combinedText.includes("phase 1")) return "Phase 1";
if (combinedText.includes("phase 2")) return "Phase 2";
if (combinedText.includes("phase 3")) return "Phase 3";
if (combinedText.includes("phase 4")) return "Phase 4";
return null;
}
/**
* Fetch market prices for a specific game
* @param {string} game - Game identifier ('cs2' or 'rust')
* @returns {Promise<Object>} - Map of item names to prices
*/
async fetchMarketPrices(game = "cs2") {
if (!this.apiKey) {
throw new Error("Steam API key not configured");
}
const appId = this.appIds[game];
if (!appId) {
throw new Error(`Invalid game: ${game}`);
}
try {
console.log(`📊 Fetching ${game.toUpperCase()} market prices...`);
const response = await axios.get(
`${this.baseUrl}/market/items/${appId}`,
{
params: {
api_key: this.apiKey,
},
timeout: 30000, // 30 second timeout
}
);
if (!response.data || !response.data.data) {
console.warn(`⚠️ No price data returned for ${game}`);
return {};
}
const priceMap = {};
const items = response.data.data;
// Process each item - API returns array with numeric indices
Object.keys(items).forEach((index) => {
const itemData = items[index];
if (itemData && itemData.prices) {
// Get item name from market_hash_name or market_name
const itemName = itemData.market_hash_name || itemData.market_name;
if (!itemName) {
return; // Skip items without names
}
// Get the most recent price - try different price fields
const price =
itemData.prices.safe ||
itemData.prices.median ||
itemData.prices.mean ||
itemData.prices.avg ||
itemData.prices.latest;
if (price && price > 0) {
priceMap[itemName] = {
price: price,
currency: "USD",
timestamp: new Date(),
};
}
}
});
console.log(
`✅ Fetched ${
Object.keys(priceMap).length
} prices for ${game.toUpperCase()}`
);
this.lastUpdate[game] = new Date();
return priceMap;
} catch (error) {
console.error(
`❌ Error fetching market prices for ${game}:`,
error.message
);
if (error.response?.status === 401) {
throw new Error("Steam API authentication failed. Check your API key.");
}
if (error.response?.status === 429) {
throw new Error("Steam API rate limit exceeded. Try again later.");
}
throw new Error(`Failed to fetch market prices: ${error.message}`);
}
}
/**
* Update database prices for all items of a specific game
* @param {string} game - Game identifier ('cs2' or 'rust')
* @returns {Promise<Object>} - Update statistics
*/
async updateDatabasePrices(game = "cs2") {
try {
console.log(`🔄 Updating database prices for ${game.toUpperCase()}...`);
// Fetch latest market prices
const priceMap = await this.fetchMarketPrices(game);
if (Object.keys(priceMap).length === 0) {
console.warn(`⚠️ No prices fetched for ${game}`);
return {
success: false,
game,
updated: 0,
notFound: 0,
errors: 0,
};
}
// Get all active items for this game
const items = await Item.find({
game,
status: "active",
});
let updated = 0;
let notFound = 0;
let errors = 0;
// Helper function to build full item name with wear
const buildFullName = (item) => {
const wearMap = {
fn: "Factory New",
mw: "Minimal Wear",
ft: "Field-Tested",
ww: "Well-Worn",
bs: "Battle-Scarred",
};
let fullName = item.name;
// Add StatTrak prefix if applicable
if (item.statTrak && !fullName.includes("StatTrak")) {
fullName = `StatTrak™ ${fullName}`;
}
// Add Souvenir prefix if applicable
if (item.souvenir && !fullName.includes("Souvenir")) {
fullName = `Souvenir ${fullName}`;
}
// Add wear condition suffix if applicable
if (item.wear && wearMap[item.wear]) {
fullName = `${fullName} (${wearMap[item.wear]})`;
}
return fullName;
};
// Update each item
for (const item of items) {
try {
// Try exact name match first
let priceData = priceMap[item.name];
// If not found, try with full name (including wear)
if (!priceData && item.wear) {
const fullName = buildFullName(item);
priceData = priceMap[fullName];
}
// If still not found, try partial matching
if (!priceData) {
const baseName = item.name
.replace(/^StatTrak™\s+/, "")
.replace(/^Souvenir\s+/, "");
// Try to find by partial match
const matchingKey = Object.keys(priceMap).find((key) => {
const apiBaseName = key
.replace(/^StatTrak™\s+/, "")
.replace(/^Souvenir\s+/, "")
.replace(/\s*\([^)]+\)\s*$/, ""); // Remove wear condition
return apiBaseName === baseName;
});
if (matchingKey) {
priceData = priceMap[matchingKey];
}
}
if (priceData) {
// Update market price
item.marketPrice = priceData.price;
item.priceUpdatedAt = new Date();
await item.save();
updated++;
} else {
notFound++;
}
} catch (err) {
console.error(`Error updating item ${item.name}:`, err.message);
errors++;
}
}
const stats = {
success: true,
game,
total: items.length,
updated,
notFound,
errors,
timestamp: new Date(),
};
console.log(`✅ Price update complete for ${game.toUpperCase()}:`);
console.log(` - Total items: ${stats.total}`);
console.log(` - Updated: ${stats.updated}`);
console.log(` - Not found: ${stats.notFound}`);
console.log(` - Errors: ${stats.errors}`);
return stats;
} catch (error) {
console.error(
`❌ Failed to update database prices for ${game}:`,
error.message
);
return {
success: false,
game,
error: error.message,
timestamp: new Date(),
};
}
}
/**
* Update prices for all games
* @returns {Promise<Object>} - Combined update statistics
*/
async updateAllPrices() {
console.log("🔄 Starting price update for all games...");
const results = {
cs2: await this.updateDatabasePrices("cs2"),
rust: await this.updateDatabasePrices("rust"),
timestamp: new Date(),
};
console.log("✅ All price updates complete!");
return results;
}
/**
* Get estimated price for an item based on characteristics
* @param {Object} itemData - Item data with name, wear, phase, etc.
* @returns {Promise<number>} - Estimated price in USD
*/
async estimatePrice(itemData) {
const { name, wear, phase, statTrak, souvenir } = itemData;
// Try to find in database first
const dbItem = await Item.findOne({
name,
status: "active",
}).sort({ marketPrice: -1 });
let basePrice = null;
if (dbItem && dbItem.marketPrice) {
// Use market price from database
basePrice = dbItem.marketPrice;
} else {
// No hardcoded fallback - return null if no market data
console.warn(`⚠️ No market price available for: ${name}`);
return null;
}
// Apply wear multiplier
if (wear) {
const wearMultipliers = {
fn: 1.0,
mw: 0.85,
ft: 0.7,
ww: 0.55,
bs: 0.4,
};
basePrice *= wearMultipliers[wear] || 1.0;
}
// Apply phase multiplier for Doppler knives
if (phase) {
const phaseMultipliers = {
Ruby: 3.5,
Sapphire: 3.8,
"Black Pearl": 2.5,
Emerald: 4.0,
"Phase 2": 1.3,
"Phase 4": 1.2,
"Phase 1": 1.0,
"Phase 3": 0.95,
};
basePrice *= phaseMultipliers[phase] || 1.0;
}
// Apply StatTrak multiplier
if (statTrak) {
basePrice *= 1.5;
}
// Apply Souvenir multiplier
if (souvenir) {
basePrice *= 1.3;
}
return parseFloat(basePrice.toFixed(2));
}
/**
* Estimate price from item name (fallback method)
* @param {string} name - Item name
* @returns {number|null} - Estimated base price or null if no data
*/
estimatePriceFromName(name) {
// No hardcoded prices - return null to indicate no market data available
console.warn(`⚠️ No market price data available for: ${name}`);
return null;
}
/**
* Schedule automatic price updates
* @param {number} intervalMs - Update interval in milliseconds (default: 1 hour)
*/
scheduleUpdates(intervalMs = this.updateInterval) {
console.log(
`⏰ Scheduling automatic price updates every ${
intervalMs / 60000
} minutes`
);
// Run on interval (initial update is handled on startup)
setInterval(() => {
this.updateAllPrices().catch((error) => {
console.error("Scheduled price update failed:", error.message);
});
}, intervalMs);
}
/**
* Check if prices need updating
* @param {string} game - Game identifier
* @returns {boolean} - True if update needed
*/
needsUpdate(game) {
if (!this.lastUpdate[game]) {
return true;
}
const timeSinceUpdate = Date.now() - this.lastUpdate[game].getTime();
return timeSinceUpdate >= this.updateInterval;
}
/**
* Get last update timestamp for a game
* @param {string} game - Game identifier
* @returns {Date|null} - Last update timestamp
*/
getLastUpdate(game) {
return this.lastUpdate[game] || null;
}
}
// Export singleton instance
const pricingService = new PricingService();
export default pricingService;