system now uses seperate pricing.
All checks were successful
Build Frontend / Build Frontend (push) Successful in 24s
All checks were successful
Build Frontend / Build Frontend (push) Successful in 24s
This commit is contained in:
395
PRICING_SYSTEM_COMPLETE.md
Normal file
395
PRICING_SYSTEM_COMPLETE.md
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
# TurboTrades Pricing & Payout System
|
||||||
|
|
||||||
|
## 📊 System Overview
|
||||||
|
|
||||||
|
TurboTrades has **two separate pricing systems** for different purposes:
|
||||||
|
|
||||||
|
### 1. **Marketplace** (User-to-User)
|
||||||
|
- Users list items at their own prices
|
||||||
|
- Other users buy from listings
|
||||||
|
- Site takes a commission (configurable in admin panel)
|
||||||
|
- Prices set by sellers
|
||||||
|
|
||||||
|
### 2. **Instant Sell** (User-to-Site)
|
||||||
|
- Site **buys items directly** from users
|
||||||
|
- Instant payout to user balance
|
||||||
|
- Price = Market Price × Payout Rate (e.g., 60%)
|
||||||
|
- Admin configures payout percentage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗄️ Database Structure
|
||||||
|
|
||||||
|
### MarketPrice Collection (Reference Database)
|
||||||
|
- **Purpose**: Reference prices for ALL Steam market items
|
||||||
|
- **Updated**: Every 1 hour automatically
|
||||||
|
- **Source**: SteamAPIs.com market data
|
||||||
|
- **Used for**: Instant sell page pricing
|
||||||
|
- **Contents**: ~30,000+ items (CS2 + Rust)
|
||||||
|
|
||||||
|
### Item Collection (Marketplace Listings)
|
||||||
|
- **Purpose**: User-listed items for sale
|
||||||
|
- **Updated**: Every 1 hour (optional, for listed items)
|
||||||
|
- **Source**: User-set prices or suggested from MarketPrice
|
||||||
|
- **Used for**: Marketplace page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ How Instant Sell Works
|
||||||
|
|
||||||
|
### User Experience:
|
||||||
|
1. User goes to `/sell` page
|
||||||
|
2. Selects items from their Steam inventory
|
||||||
|
3. Sees **instant buy price** = Market Price × Payout Rate
|
||||||
|
4. Accepts offer
|
||||||
|
5. Site buys items, credits balance immediately
|
||||||
|
|
||||||
|
### Backend Flow:
|
||||||
|
1. **Fetch Inventory** (`GET /api/inventory/steam`)
|
||||||
|
- Gets user's Steam inventory
|
||||||
|
- Looks up each item in `MarketPrice` database
|
||||||
|
- Applies payout rate (e.g., 60%)
|
||||||
|
- Returns items with `marketPrice` (already discounted)
|
||||||
|
|
||||||
|
2. **Create Trade** (`POST /api/inventory/trade`)
|
||||||
|
- Validates items and prices
|
||||||
|
- Creates trade offer via Steam bot
|
||||||
|
- User accepts in Steam
|
||||||
|
- Balance credited on completion
|
||||||
|
|
||||||
|
### Example Calculation:
|
||||||
|
```
|
||||||
|
Market Price (MarketPrice DB): $10.00
|
||||||
|
Payout Rate (Admin Config): 60%
|
||||||
|
User Receives: $6.00
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Automatic Price Updates
|
||||||
|
|
||||||
|
### On Server Startup:
|
||||||
|
```javascript
|
||||||
|
// Runs immediately when server starts
|
||||||
|
pricingService.updateAllPrices()
|
||||||
|
→ Updates MarketPrice database (CS2 + Rust)
|
||||||
|
→ Updates Item prices (marketplace listings)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Every Hour:
|
||||||
|
```javascript
|
||||||
|
// Scheduled via setInterval (60 minutes)
|
||||||
|
pricingService.scheduleUpdates(60 * 60 * 1000)
|
||||||
|
→ updateMarketPriceDatabase('cs2') // ~20,000 items
|
||||||
|
→ updateMarketPriceDatabase('rust') // ~10,000 items
|
||||||
|
→ updateDatabasePrices('cs2') // Marketplace items
|
||||||
|
→ updateDatabasePrices('rust') // Marketplace items
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Gets Updated:
|
||||||
|
- ✅ **MarketPrice**: ALL Steam market items
|
||||||
|
- ✅ **Item**: Only items listed on marketplace
|
||||||
|
- ⏱️ **Duration**: ~2-5 minutes per update
|
||||||
|
- 🔑 **Requires**: `STEAM_APIS_KEY` in `.env`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎛️ Admin Configuration
|
||||||
|
|
||||||
|
### Instant Sell Settings
|
||||||
|
|
||||||
|
**Endpoint:** `PATCH /api/admin/config/instantsell`
|
||||||
|
|
||||||
|
**Settings Available:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true, // Enable/disable instant sell
|
||||||
|
"payoutRate": 0.6, // 60% default payout
|
||||||
|
"minItemValue": 0.1, // Min $0.10
|
||||||
|
"maxItemValue": 10000, // Max $10,000
|
||||||
|
"cs2": {
|
||||||
|
"enabled": true,
|
||||||
|
"payoutRate": 0.65 // CS2-specific override (65%)
|
||||||
|
},
|
||||||
|
"rust": {
|
||||||
|
"enabled": true,
|
||||||
|
"payoutRate": 0.55 // Rust-specific override (55%)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setting Payout Rates
|
||||||
|
|
||||||
|
**Example: Set 65% payout for CS2, 55% for Rust**
|
||||||
|
```bash
|
||||||
|
curl -X PATCH https://api.turbotrades.dev/api/admin/config/instantsell \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: accessToken=YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"cs2": {
|
||||||
|
"payoutRate": 0.65
|
||||||
|
},
|
||||||
|
"rust": {
|
||||||
|
"payoutRate": 0.55
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example: Disable instant sell for Rust**
|
||||||
|
```bash
|
||||||
|
curl -X PATCH https://api.turbotrades.dev/api/admin/config/instantsell \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Cookie: accessToken=YOUR_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"rust": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Initial Setup
|
||||||
|
|
||||||
|
### 1. Set API Key
|
||||||
|
```bash
|
||||||
|
# In your .env file
|
||||||
|
STEAM_APIS_KEY=your_api_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
Get API key from: https://steamapis.com/
|
||||||
|
|
||||||
|
### 2. Import Initial Prices (One-Time)
|
||||||
|
```bash
|
||||||
|
# Populates MarketPrice database
|
||||||
|
node import-market-prices.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output:**
|
||||||
|
```
|
||||||
|
📡 Fetching CS2 market data...
|
||||||
|
✅ Received 20,147 items from API
|
||||||
|
💾 Importing CS2 items to database...
|
||||||
|
✅ CS2 import complete: 20,147 inserted
|
||||||
|
|
||||||
|
📡 Fetching Rust market data...
|
||||||
|
✅ Received 9,823 items from API
|
||||||
|
💾 Importing Rust items to database...
|
||||||
|
✅ Rust import complete: 9,823 inserted
|
||||||
|
|
||||||
|
🎉 Total: 29,970 items imported
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure Payout Rate
|
||||||
|
Use admin panel or API to set your desired payout percentage.
|
||||||
|
|
||||||
|
### 4. Start Server
|
||||||
|
```bash
|
||||||
|
pm2 start ecosystem.config.js --env production
|
||||||
|
```
|
||||||
|
|
||||||
|
Server will automatically:
|
||||||
|
- ✅ Update prices on startup
|
||||||
|
- ✅ Schedule hourly updates
|
||||||
|
- ✅ Keep prices fresh
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Price Update Logs
|
||||||
|
|
||||||
|
### Successful Update:
|
||||||
|
```
|
||||||
|
🔄 Starting price update for all games...
|
||||||
|
|
||||||
|
🔄 Updating MarketPrice reference database for CS2...
|
||||||
|
📡 Fetching market data from Steam API...
|
||||||
|
✅ Received 20,147 items from API
|
||||||
|
📦 Batch: 10,000 inserted, 10,147 updated
|
||||||
|
✅ MarketPrice update complete for CS2:
|
||||||
|
📥 Inserted: 234
|
||||||
|
🔄 Updated: 19,913
|
||||||
|
⏭️ Skipped: 0
|
||||||
|
|
||||||
|
🔄 Updating MarketPrice reference database for RUST...
|
||||||
|
✅ Received 9,823 items from API
|
||||||
|
✅ MarketPrice update complete for RUST:
|
||||||
|
📥 Inserted: 89
|
||||||
|
🔄 Updated: 9,734
|
||||||
|
|
||||||
|
✅ All price updates complete!
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs:
|
||||||
|
```bash
|
||||||
|
pm2 logs turbotrades-backend --lines 100 | grep "price update"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Manual Price Updates
|
||||||
|
|
||||||
|
### Update All Prices Now:
|
||||||
|
```bash
|
||||||
|
node update-prices-now.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Specific Game:
|
||||||
|
```bash
|
||||||
|
node update-prices-now.js cs2
|
||||||
|
node update-prices-now.js rust
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Price Coverage:
|
||||||
|
```bash
|
||||||
|
node check-prices.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```
|
||||||
|
📊 Price Coverage Report
|
||||||
|
|
||||||
|
🎮 Counter-Strike 2:
|
||||||
|
Active Items: 1,245
|
||||||
|
With Prices: 1,198
|
||||||
|
Coverage: 96.2%
|
||||||
|
|
||||||
|
🔧 Rust:
|
||||||
|
Active Items: 387
|
||||||
|
With Prices: 375
|
||||||
|
Coverage: 96.9%
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Troubleshooting
|
||||||
|
|
||||||
|
### Items Showing "Price unavailable" on Sell Page
|
||||||
|
|
||||||
|
**Cause:** MarketPrice database is empty or outdated
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Import prices
|
||||||
|
node import-market-prices.js
|
||||||
|
|
||||||
|
# OR restart server (auto-updates on startup)
|
||||||
|
pm2 restart turbotrades-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prices Not Updating
|
||||||
|
|
||||||
|
**Check 1:** Verify API key
|
||||||
|
```bash
|
||||||
|
# Check if set
|
||||||
|
echo $STEAM_APIS_KEY
|
||||||
|
|
||||||
|
# Test API key
|
||||||
|
curl "https://api.steamapis.com/market/items/730?api_key=$STEAM_APIS_KEY" | head
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check 2:** Check logs
|
||||||
|
```bash
|
||||||
|
pm2 logs turbotrades-backend --lines 200 | grep -E "price|update"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check 3:** Verify scheduler is running
|
||||||
|
```bash
|
||||||
|
# Should see this on startup:
|
||||||
|
⏰ Starting automatic price update scheduler...
|
||||||
|
🔄 Running initial price update on startup...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wrong Payout Rate Applied
|
||||||
|
|
||||||
|
**Check config:**
|
||||||
|
```bash
|
||||||
|
# Via API
|
||||||
|
curl https://api.turbotrades.dev/api/config/public | jq '.config.instantSell'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update config:**
|
||||||
|
```bash
|
||||||
|
# Via admin panel at /admin
|
||||||
|
# Or via API (requires admin auth)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Database Queries
|
||||||
|
|
||||||
|
### Check MarketPrice Count:
|
||||||
|
```javascript
|
||||||
|
db.marketprices.countDocuments({ game: "cs2" })
|
||||||
|
db.marketprices.countDocuments({ game: "rust" })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find Item Price:
|
||||||
|
```javascript
|
||||||
|
db.marketprices.findOne({
|
||||||
|
marketHashName: "AK-47 | Redline (Field-Tested)",
|
||||||
|
game: "cs2"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Most Expensive Items:
|
||||||
|
```javascript
|
||||||
|
db.marketprices.find({ game: "cs2" })
|
||||||
|
.sort({ price: -1 })
|
||||||
|
.limit(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Last Update Time:
|
||||||
|
```javascript
|
||||||
|
db.marketprices.findOne({ game: "cs2" })
|
||||||
|
.sort({ lastUpdated: -1 })
|
||||||
|
.limit(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Best Practices
|
||||||
|
|
||||||
|
### Payout Rates:
|
||||||
|
- **Too High** (>80%): Site loses money
|
||||||
|
- **Too Low** (<40%): Users won't sell
|
||||||
|
- **Recommended**: 55-65% depending on competition
|
||||||
|
|
||||||
|
### Update Frequency:
|
||||||
|
- **Every Hour**: Good for active trading
|
||||||
|
- **Every 2-4 Hours**: Sufficient for most sites
|
||||||
|
- **Daily**: Only for low-volume sites
|
||||||
|
|
||||||
|
### Monitoring:
|
||||||
|
- Set up alerts for failed price updates
|
||||||
|
- Monitor price coverage percentage
|
||||||
|
- Track user complaints about prices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Notes
|
||||||
|
|
||||||
|
- ✅ API key stored in `.env` (never committed)
|
||||||
|
- ✅ Payout rate changes logged with admin username
|
||||||
|
- ✅ Rate limits on price update endpoints
|
||||||
|
- ✅ Validate prices before accepting trades
|
||||||
|
- ✅ Maximum item value limits prevent abuse
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Summary
|
||||||
|
|
||||||
|
| Feature | Status | Update Frequency |
|
||||||
|
|---------|--------|------------------|
|
||||||
|
| MarketPrice DB | ✅ Automatic | Every 1 hour |
|
||||||
|
| Item Prices | ✅ Automatic | Every 1 hour |
|
||||||
|
| Payout Rate | ⚙️ Admin Config | Manual |
|
||||||
|
| Initial Import | 🔧 Manual | One-time |
|
||||||
|
| Sell Page | ✅ Live | Real-time |
|
||||||
|
|
||||||
|
**Your instant sell page will now:**
|
||||||
|
- ✅ Show accurate prices from your database
|
||||||
|
- ✅ Apply your configured payout rate
|
||||||
|
- ✅ Update prices automatically every hour
|
||||||
|
- ✅ Allow per-game payout customization
|
||||||
|
|
||||||
|
**No more manual price updates needed!** 🎉
|
||||||
20
index.js
20
index.js
@@ -601,8 +601,24 @@ const start = async () => {
|
|||||||
.updateAllPrices()
|
.updateAllPrices()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
console.log("✅ Initial price update completed successfully");
|
console.log("✅ Initial price update completed successfully");
|
||||||
console.log(` CS2: ${result.cs2.updated || 0} items updated`);
|
console.log(" 📊 MarketPrice Reference Database:");
|
||||||
console.log(` Rust: ${result.rust.updated || 0} items updated`);
|
console.log(
|
||||||
|
` CS2: ${
|
||||||
|
result.marketPrices.cs2.updated || 0
|
||||||
|
} prices updated, ${result.marketPrices.cs2.inserted || 0} new`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` Rust: ${
|
||||||
|
result.marketPrices.rust.updated || 0
|
||||||
|
} prices updated, ${result.marketPrices.rust.inserted || 0} new`
|
||||||
|
);
|
||||||
|
console.log(" 📦 Marketplace Items:");
|
||||||
|
console.log(
|
||||||
|
` CS2: ${result.itemPrices.cs2.updated || 0} items updated`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` Rust: ${result.itemPrices.rust.updated || 0} items updated`
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("❌ Initial price update failed:", error.message);
|
console.error("❌ Initial price update failed:", error.message);
|
||||||
|
|||||||
@@ -95,6 +95,22 @@ const SiteConfigSchema = new mongoose.Schema(
|
|||||||
priceUpdateInterval: { type: Number, default: 3600000 }, // 1 hour in ms
|
priceUpdateInterval: { type: Number, default: 3600000 }, // 1 hour in ms
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Instant sell / buyback settings (site buying items from users)
|
||||||
|
instantSell: {
|
||||||
|
enabled: { type: Boolean, default: true },
|
||||||
|
payoutRate: { type: Number, default: 0.6 }, // 60% of market price
|
||||||
|
minItemValue: { type: Number, default: 0.1 }, // Min $0.10
|
||||||
|
maxItemValue: { type: Number, default: 10000 }, // Max $10,000
|
||||||
|
cs2: {
|
||||||
|
enabled: { type: Boolean, default: true },
|
||||||
|
payoutRate: { type: Number, default: 0.6 }, // Game-specific override
|
||||||
|
},
|
||||||
|
rust: {
|
||||||
|
enabled: { type: Boolean, default: true },
|
||||||
|
payoutRate: { type: Number, default: 0.6 }, // Game-specific override
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// Features toggles
|
// Features toggles
|
||||||
features: {
|
features: {
|
||||||
twoFactorAuth: { type: Boolean, default: true },
|
twoFactorAuth: { type: Boolean, default: true },
|
||||||
|
|||||||
@@ -786,6 +786,98 @@ export default async function adminManagementRoutes(fastify, options) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PATCH /admin/config/instantsell - Update instant sell settings
|
||||||
|
fastify.patch(
|
||||||
|
"/config/instantsell",
|
||||||
|
{
|
||||||
|
preHandler: [authenticate, isAdmin],
|
||||||
|
schema: {
|
||||||
|
body: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
enabled: { type: "boolean" },
|
||||||
|
payoutRate: { type: "number", minimum: 0, maximum: 1 },
|
||||||
|
minItemValue: { type: "number", minimum: 0 },
|
||||||
|
maxItemValue: { type: "number", minimum: 0 },
|
||||||
|
cs2: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
enabled: { type: "boolean" },
|
||||||
|
payoutRate: { type: "number", minimum: 0, maximum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rust: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
enabled: { type: "boolean" },
|
||||||
|
payoutRate: { type: "number", minimum: 0, maximum: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const config = await SiteConfig.getConfig();
|
||||||
|
|
||||||
|
// Update top-level instant sell settings
|
||||||
|
["enabled", "payoutRate", "minItemValue", "maxItemValue"].forEach(
|
||||||
|
(key) => {
|
||||||
|
if (request.body[key] !== undefined) {
|
||||||
|
config.instantSell[key] = request.body[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update CS2 settings
|
||||||
|
if (request.body.cs2) {
|
||||||
|
Object.keys(request.body.cs2).forEach((key) => {
|
||||||
|
if (request.body.cs2[key] !== undefined) {
|
||||||
|
config.instantSell.cs2[key] = request.body.cs2[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Rust settings
|
||||||
|
if (request.body.rust) {
|
||||||
|
Object.keys(request.body.rust).forEach((key) => {
|
||||||
|
if (request.body.rust[key] !== undefined) {
|
||||||
|
config.instantSell.rust[key] = request.body.rust[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
config.lastUpdatedBy = request.user.username;
|
||||||
|
config.lastUpdatedAt = new Date();
|
||||||
|
|
||||||
|
await config.save();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`⚙️ Admin ${request.user.username} updated instant sell settings:`,
|
||||||
|
{
|
||||||
|
payoutRate: config.instantSell.payoutRate,
|
||||||
|
cs2PayoutRate: config.instantSell.cs2?.payoutRate,
|
||||||
|
rustPayoutRate: config.instantSell.rust?.payoutRate,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return reply.send({
|
||||||
|
success: true,
|
||||||
|
message: "Instant sell settings updated",
|
||||||
|
instantSell: config.instantSell,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to update instant sell settings:", error);
|
||||||
|
return reply.status(500).send({
|
||||||
|
success: false,
|
||||||
|
message: "Failed to update instant sell settings",
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// ANNOUNCEMENTS
|
// ANNOUNCEMENTS
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { authenticate } from "../middleware/auth.js";
|
|||||||
import Item from "../models/Item.js";
|
import Item from "../models/Item.js";
|
||||||
import Trade from "../models/Trade.js";
|
import Trade from "../models/Trade.js";
|
||||||
import Transaction from "../models/Transaction.js";
|
import Transaction from "../models/Transaction.js";
|
||||||
|
import SiteConfig from "../models/SiteConfig.js";
|
||||||
import { config } from "../config/index.js";
|
import { config } from "../config/index.js";
|
||||||
import pricingService from "../services/pricing.js";
|
import pricingService from "../services/pricing.js";
|
||||||
import marketPriceService from "../services/marketPrice.js";
|
import marketPriceService from "../services/marketPrice.js";
|
||||||
@@ -184,6 +185,23 @@ export default async function inventoryRoutes(fastify, options) {
|
|||||||
// Enrich items with market prices (fast database lookup)
|
// Enrich items with market prices (fast database lookup)
|
||||||
console.log(`💰 Adding market prices...`);
|
console.log(`💰 Adding market prices...`);
|
||||||
|
|
||||||
|
// Get site config for payout rate
|
||||||
|
const siteConfig = await SiteConfig.getConfig();
|
||||||
|
let payoutRate = siteConfig.instantSell.payoutRate || 0.6;
|
||||||
|
|
||||||
|
// Check for game-specific payout rate
|
||||||
|
if (game === "cs2" && siteConfig.instantSell.cs2?.payoutRate) {
|
||||||
|
payoutRate = siteConfig.instantSell.cs2.payoutRate;
|
||||||
|
} else if (game === "rust" && siteConfig.instantSell.rust?.payoutRate) {
|
||||||
|
payoutRate = siteConfig.instantSell.rust.payoutRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`💵 Instant sell payout rate: ${(payoutRate * 100).toFixed(
|
||||||
|
0
|
||||||
|
)}% of market price`
|
||||||
|
);
|
||||||
|
|
||||||
// Get all item names for batch lookup
|
// Get all item names for batch lookup
|
||||||
const itemNames = items.map((item) => item.name);
|
const itemNames = items.map((item) => item.name);
|
||||||
console.log(`📋 Looking up prices for ${itemNames.length} items`);
|
console.log(`📋 Looking up prices for ${itemNames.length} items`);
|
||||||
@@ -196,12 +214,19 @@ export default async function inventoryRoutes(fastify, options) {
|
|||||||
`💰 Found prices for ${foundPrices}/${itemNames.length} items`
|
`💰 Found prices for ${foundPrices}/${itemNames.length} items`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add prices to items
|
// Add prices to items (applying payout rate for instant sell)
|
||||||
const enrichedItems = items.map((item) => ({
|
const enrichedItems = items.map((item) => {
|
||||||
...item,
|
const marketPrice = priceMap[item.name] || null;
|
||||||
marketPrice: priceMap[item.name] || null,
|
return {
|
||||||
hasPriceData: !!priceMap[item.name],
|
...item,
|
||||||
}));
|
marketPrice: marketPrice
|
||||||
|
? parseFloat((marketPrice * payoutRate).toFixed(2))
|
||||||
|
: null,
|
||||||
|
fullMarketPrice: marketPrice, // Keep original for reference
|
||||||
|
payoutRate: payoutRate,
|
||||||
|
hasPriceData: !!marketPrice,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Log items without prices
|
// Log items without prices
|
||||||
const itemsWithoutPrices = enrichedItems.filter(
|
const itemsWithoutPrices = enrichedItems.filter(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Item from "../models/Item.js";
|
import Item from "../models/Item.js";
|
||||||
|
import MarketPrice from "../models/MarketPrice.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pricing Service
|
* Pricing Service
|
||||||
@@ -275,15 +276,177 @@ class PricingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update prices for all games
|
* Update MarketPrice reference database for a specific game
|
||||||
|
* Fetches all items from Steam market and updates the reference database
|
||||||
|
* @param {string} game - Game identifier ('cs2' or 'rust')
|
||||||
|
* @returns {Promise<Object>} - Update statistics
|
||||||
|
*/
|
||||||
|
async updateMarketPriceDatabase(game) {
|
||||||
|
console.log(
|
||||||
|
`\n🔄 Updating MarketPrice reference database for ${game.toUpperCase()}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const appId = this.appIds[game];
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error(`Invalid game: ${game}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.apiKey) {
|
||||||
|
throw new Error("Steam API key not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all market items from Steam API
|
||||||
|
console.log(`📡 Fetching market data from Steam API...`);
|
||||||
|
const response = await axios.get(
|
||||||
|
`${this.baseUrl}/market/items/${appId}`,
|
||||||
|
{
|
||||||
|
params: { api_key: this.apiKey },
|
||||||
|
timeout: 60000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.data || !response.data.data) {
|
||||||
|
throw new Error("No data returned from Steam API");
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = response.data.data;
|
||||||
|
const itemCount = Object.keys(items).length;
|
||||||
|
console.log(`✅ Received ${itemCount} items from API`);
|
||||||
|
|
||||||
|
let inserted = 0;
|
||||||
|
let updated = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let errors = 0;
|
||||||
|
|
||||||
|
// Process items in batches
|
||||||
|
const bulkOps = [];
|
||||||
|
|
||||||
|
for (const item of Object.values(items)) {
|
||||||
|
try {
|
||||||
|
// Get the best available price
|
||||||
|
const price =
|
||||||
|
item.prices?.safe ||
|
||||||
|
item.prices?.median ||
|
||||||
|
item.prices?.mean ||
|
||||||
|
item.prices?.avg ||
|
||||||
|
item.prices?.latest;
|
||||||
|
|
||||||
|
if (!price || price <= 0) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketHashName = item.market_hash_name || item.market_name;
|
||||||
|
const marketName = item.market_name || item.market_hash_name;
|
||||||
|
|
||||||
|
if (!marketHashName || !marketName) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which price type was used
|
||||||
|
let priceType = "safe";
|
||||||
|
if (item.prices?.safe) priceType = "safe";
|
||||||
|
else if (item.prices?.median) priceType = "median";
|
||||||
|
else if (item.prices?.mean) priceType = "mean";
|
||||||
|
else if (item.prices?.avg) priceType = "avg";
|
||||||
|
else if (item.prices?.latest) priceType = "latest";
|
||||||
|
|
||||||
|
bulkOps.push({
|
||||||
|
updateOne: {
|
||||||
|
filter: { marketHashName: marketHashName },
|
||||||
|
update: {
|
||||||
|
$set: {
|
||||||
|
name: marketName,
|
||||||
|
game: game,
|
||||||
|
appId: appId,
|
||||||
|
marketHashName: marketHashName,
|
||||||
|
price: price,
|
||||||
|
priceType: priceType,
|
||||||
|
image: item.image || null,
|
||||||
|
borderColor: item.border_color || null,
|
||||||
|
nameId: item.nameID || null,
|
||||||
|
lastUpdated: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
upsert: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute in batches of 1000
|
||||||
|
if (bulkOps.length >= 1000) {
|
||||||
|
const result = await MarketPrice.bulkWrite(bulkOps);
|
||||||
|
inserted += result.upsertedCount;
|
||||||
|
updated += result.modifiedCount;
|
||||||
|
console.log(
|
||||||
|
` 📦 Batch: ${inserted} inserted, ${updated} updated`
|
||||||
|
);
|
||||||
|
bulkOps.length = 0;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
errors++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute remaining items
|
||||||
|
if (bulkOps.length > 0) {
|
||||||
|
const result = await MarketPrice.bulkWrite(bulkOps);
|
||||||
|
inserted += result.upsertedCount;
|
||||||
|
updated += result.modifiedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ MarketPrice update complete for ${game.toUpperCase()}:`);
|
||||||
|
console.log(` 📥 Inserted: ${inserted}`);
|
||||||
|
console.log(` 🔄 Updated: ${updated}`);
|
||||||
|
console.log(` ⏭️ Skipped: ${skipped}`);
|
||||||
|
if (errors > 0) {
|
||||||
|
console.log(` ❌ Errors: ${errors}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
game,
|
||||||
|
total: itemCount,
|
||||||
|
inserted,
|
||||||
|
updated,
|
||||||
|
skipped,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ Error updating MarketPrice for ${game}:`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
game,
|
||||||
|
error: error.message,
|
||||||
|
total: 0,
|
||||||
|
inserted: 0,
|
||||||
|
updated: 0,
|
||||||
|
skipped: 0,
|
||||||
|
errors: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update prices for all games (both Item and MarketPrice databases)
|
||||||
* @returns {Promise<Object>} - Combined update statistics
|
* @returns {Promise<Object>} - Combined update statistics
|
||||||
*/
|
*/
|
||||||
async updateAllPrices() {
|
async updateAllPrices() {
|
||||||
console.log("🔄 Starting price update for all games...");
|
console.log("🔄 Starting price update for all games...");
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
cs2: await this.updateDatabasePrices("cs2"),
|
marketPrices: {
|
||||||
rust: await this.updateDatabasePrices("rust"),
|
cs2: await this.updateMarketPriceDatabase("cs2"),
|
||||||
|
rust: await this.updateMarketPriceDatabase("rust"),
|
||||||
|
},
|
||||||
|
itemPrices: {
|
||||||
|
cs2: await this.updateDatabasePrices("cs2"),
|
||||||
|
rust: await this.updateDatabasePrices("rust"),
|
||||||
|
},
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user