Files
TurboTrades/IMPLEMENTATION_PLAN.md
iDefineHD 63c578b0ae feat: Complete admin panel implementation
- Add user management system with all CRUD operations
- Add promotion statistics dashboard with export
- Simplify Trading & Market settings UI
- Fix promotion schema (dates now optional)
- Add missing API endpoints and PATCH support
- Add comprehensive documentation
- Fix critical bugs (deletePromotion, duplicate endpoints)

All features tested and production-ready.
2026-01-10 21:57:55 +00:00

23 KiB

Complete Admin Features Implementation Plan

🎯 Project Goal

Fully implement all admin panel features so they actually work and enforce the configured settings across the entire TurboTrades platform.


📋 Phase 1: Maintenance Mode & Core Infrastructure (Priority: CRITICAL)

1.1 Maintenance Mode Enforcement

Files to Create/Modify:

  • middleware/maintenance.js (exists, needs enhancement)
  • index.js (register middleware globally)
  • frontend/src/views/MaintenancePage.vue (new)

Implementation:

// Global hook in index.js
fastify.addHook('preHandler', async (request, reply) => {
  // Skip for certain routes (health, auth callback)
  const skipRoutes = ['/health', '/api/health', '/auth/steam/callback'];
  if (skipRoutes.includes(request.url)) return;

  const config = await SiteConfig.getConfig();
  
  if (config.maintenance.enabled) {
    // Check scheduled times
    if (config.maintenance.scheduledStart && config.maintenance.scheduledEnd) {
      const now = new Date();
      if (now < config.maintenance.scheduledStart || now > config.maintenance.scheduledEnd) {
        return; // Outside maintenance window
      }
    }

    // Allow admins and whitelisted users
    if (request.user?.isAdmin) return;
    if (config.maintenance.allowedSteamIds?.includes(request.user?.steamId)) return;

    // Block everyone else
    return reply.status(503).send({
      success: false,
      error: 'Maintenance Mode',
      message: config.maintenance.message || 'Site is under maintenance',
      scheduledEnd: config.maintenance.scheduledEnd
    });
  }
});

Frontend:

  • Create maintenance page component
  • Show maintenance message
  • Show countdown if scheduled end time exists
  • Admin bypass indicator

Testing:

  • Toggle maintenance ON → non-admin users blocked ✓
  • Admin users can still access ✓
  • Whitelisted Steam IDs can access ✓
  • Scheduled maintenance activates/deactivates ✓

📋 Phase 2: Market Settings Integration (Priority: HIGH)

2.1 Market Enable/Disable

Files to Modify:

  • routes/market.js
  • frontend/src/views/MarketPage.vue

Implementation:

// Add to ALL market routes
const checkMarketEnabled = async (request, reply) => {
  const config = await SiteConfig.getConfig();
  if (!config.market.enabled) {
    return reply.status(503).send({
      success: false,
      message: 'Marketplace is currently disabled'
    });
  }
};

// Apply to routes
fastify.get('/listings', { preHandler: [checkMarketEnabled] }, ...);
fastify.post('/listings', { preHandler: [authenticate, checkMarketEnabled] }, ...);

Frontend:

  • Show "Market Disabled" message when trying to access
  • Disable market navigation when disabled
  • Admin can still access for configuration

2.2 Price Limits Enforcement

Implementation:

// In listing creation
const config = await SiteConfig.getConfig();

if (price < config.market.minListingPrice) {
  return reply.status(400).send({
    success: false,
    message: `Price must be at least $${config.market.minListingPrice}`
  });
}

if (price > config.market.maxListingPrice) {
  return reply.status(400).send({
    success: false,
    message: `Price cannot exceed $${config.market.maxListingPrice}`
  });
}

Frontend Validation:

  • Add min/max attributes to price inputs
  • Show real-time validation
  • Fetch limits from /api/config/status

2.3 Commission Application

Implementation:

// When item is sold
const config = await SiteConfig.getConfig();
const commission = salePrice * config.market.commission;
const sellerProceeds = salePrice - commission;

// Update seller balance
seller.balance += sellerProceeds;
await seller.save();

// Record commission
await Transaction.create({
  user: seller._id,
  type: 'market_sale',
  amount: sellerProceeds,
  description: `Sold ${item.name}`,
  metadata: {
    itemId: item._id,
    salePrice: salePrice,
    commission: commission,
    commissionRate: config.market.commission
  }
});

// Record platform revenue
await Transaction.create({
  type: 'platform_commission',
  amount: commission,
  description: `Commission from sale of ${item.name}`,
  metadata: {
    itemId: item._id,
    salePrice: salePrice,
    seller: seller._id
  }
});

2.4 Auto Price Updates

Files to Create:

  • services/priceUpdater.js
  • jobs/updatePrices.js

Implementation:

// services/priceUpdater.js
class PriceUpdater {
  async updateAllPrices() {
    const config = await SiteConfig.getConfig();
    
    if (!config.market.autoUpdatePrices) return;
    
    // Fetch latest prices from external API (Steam, CSGOFloat, etc)
    const items = await Item.find({ status: 'listed' });
    
    for (const item of items) {
      const newPrice = await this.fetchLatestPrice(item);
      if (newPrice && Math.abs(newPrice - item.price) > 0.01) {
        item.marketPrice = newPrice;
        // Optionally update listing price within bounds
        await item.save();
      }
    }
  }

  async start() {
    const config = await SiteConfig.getConfig();
    const interval = config.market.priceUpdateInterval || 3600000; // 1 hour default
    
    setInterval(() => this.updateAllPrices(), interval);
  }
}

📋 Phase 3: Trading System Implementation (Priority: HIGH)

3.1 Deposit System

Files to Create:

  • routes/trading.js (new)
  • models/Deposit.js (new)
  • services/steamBot.js (enhance existing)

Database Schema:

// models/Deposit.js
const depositSchema = new Schema({
  user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  items: [{
    assetId: String,
    name: String,
    marketHashName: String,
    iconUrl: String,
    rarity: String,
    value: Number
  }],
  totalValue: { type: Number, required: true },
  tradeOfferId: String,
  botId: String,
  status: {
    type: String,
    enum: ['pending', 'accepted', 'declined', 'cancelled', 'completed', 'failed'],
    default: 'pending'
  },
  promotion: {
    id: String,
    code: String,
    bonusAmount: Number,
    bonusPercentage: Number
  },
  bonusApplied: { type: Number, default: 0 },
  finalAmount: Number,
  createdAt: { type: Date, default: Date.now },
  completedAt: Date
});

API Endpoints:

// POST /api/trading/deposit/initiate
// - Check if deposits enabled
// - Check minimum deposit
// - Validate items
// - Check for active promotions
// - Create trade offer via Steam bot
// - Return trade offer URL

// POST /api/trading/deposit/cancel
// - Cancel pending deposit
// - Cancel trade offer

// GET /api/trading/deposit/history
// - Get user's deposit history

Implementation:

// POST /api/trading/deposit/initiate
fastify.post('/deposit/initiate', {
  preHandler: [authenticate],
  schema: {
    body: {
      type: 'object',
      required: ['items'],
      properties: {
        items: { type: 'array' },
        promoCode: { type: 'string' }
      }
    }
  }
}, async (request, reply) => {
  const config = await SiteConfig.getConfig();
  
  // Check if deposits enabled
  if (!config.trading.enabled || !config.trading.depositEnabled) {
    return reply.status(503).send({
      success: false,
      message: 'Deposits are currently disabled'
    });
  }

  const { items, promoCode } = request.body;
  
  // Calculate total value
  let totalValue = items.reduce((sum, item) => sum + item.value, 0);
  
  // Check minimum deposit
  if (totalValue < config.trading.minDeposit) {
    return reply.status(400).send({
      success: false,
      message: `Minimum deposit is $${config.trading.minDeposit}`
    });
  }

  // Check max items
  if (items.length > config.trading.maxItemsPerTrade) {
    return reply.status(400).send({
      success: false,
      message: `Maximum ${config.trading.maxItemsPerTrade} items per trade`
    });
  }

  // Check for active promotion
  let promotion = null;
  let bonusAmount = 0;

  if (promoCode) {
    const promoResult = await validateAndApplyPromotion(
      request.user,
      promoCode,
      totalValue,
      config
    );
    
    if (promoResult.valid) {
      promotion = promoResult.promotion;
      bonusAmount = promoResult.bonusAmount;
    }
  }

  const finalAmount = totalValue + bonusAmount;

  // Create deposit record
  const deposit = await Deposit.create({
    user: request.user._id,
    items,
    totalValue,
    bonusApplied: bonusAmount,
    finalAmount,
    promotion: promotion ? {
      id: promotion.id,
      code: promotion.code,
      bonusAmount: promotion.bonusAmount,
      bonusPercentage: promotion.bonusPercentage
    } : null,
    status: 'pending'
  });

  // Create trade offer via Steam bot
  const tradeOffer = await steamBot.createDepositOffer(
    request.user.tradeUrl,
    items,
    deposit._id
  );

  deposit.tradeOfferId = tradeOffer.id;
  deposit.botId = tradeOffer.botId;
  await deposit.save();

  return reply.send({
    success: true,
    deposit: {
      id: deposit._id,
      totalValue,
      bonusAmount,
      finalAmount,
      tradeUrl: tradeOffer.url
    }
  });
});

3.2 Withdrawal System

Files to Create:

  • models/Withdrawal.js
  • Routes in routes/trading.js

Database Schema:

const withdrawalSchema = new Schema({
  user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
  items: [{
    itemId: { type: Schema.Types.ObjectId, ref: 'Item' },
    name: String,
    value: Number
  }],
  totalValue: { type: Number, required: true },
  fee: { type: Number, required: true },
  feePercentage: Number,
  amountDeducted: Number,
  tradeOfferId: String,
  botId: String,
  status: {
    type: String,
    enum: ['pending', 'processing', 'sent', 'accepted', 'declined', 'cancelled'],
    default: 'pending'
  },
  createdAt: { type: Date, default: Date.now },
  processedAt: Date
});

Implementation:

// POST /api/trading/withdraw/request
fastify.post('/withdraw/request', {
  preHandler: [authenticate]
}, async (request, reply) => {
  const config = await SiteConfig.getConfig();
  
  // Check if withdrawals enabled
  if (!config.trading.enabled || !config.trading.withdrawEnabled) {
    return reply.status(503).send({
      success: false,
      message: 'Withdrawals are currently disabled'
    });
  }

  // Check trade URL
  if (!request.user.tradeUrl) {
    return reply.status(400).send({
      success: false,
      message: 'Please set your trade URL first'
    });
  }

  const { itemIds } = request.body;
  
  // Fetch items from user's inventory
  const items = await Item.find({
    _id: { $in: itemIds },
    owner: request.user._id,
    status: 'owned'
  });

  if (items.length !== itemIds.length) {
    return reply.status(400).send({
      success: false,
      message: 'Some items not found or not owned by you'
    });
  }

  // Calculate total value
  const totalValue = items.reduce((sum, item) => sum + item.value, 0);
  
  // Check minimum withdrawal
  if (totalValue < config.trading.minWithdraw) {
    return reply.status(400).send({
      success: false,
      message: `Minimum withdrawal is $${config.trading.minWithdraw}`
    });
  }

  // Calculate fee
  const feePercentage = config.trading.withdrawFee;
  const fee = totalValue * feePercentage;
  const amountDeducted = totalValue + fee;

  // Check user balance (if paying fee from balance)
  if (request.user.balance < fee) {
    return reply.status(400).send({
      success: false,
      message: `Insufficient balance for withdrawal fee ($${fee.toFixed(2)})`
    });
  }

  // Create withdrawal
  const withdrawal = await Withdrawal.create({
    user: request.user._id,
    items: items.map(item => ({
      itemId: item._id,
      name: item.name,
      value: item.value
    })),
    totalValue,
    fee,
    feePercentage,
    amountDeducted,
    status: 'pending'
  });

  // Deduct fee from balance
  request.user.balance -= fee;
  await request.user.save();

  // Mark items as withdrawing
  await Item.updateMany(
    { _id: { $in: itemIds } },
    { status: 'withdrawing', withdrawalId: withdrawal._id }
  );

  // Create transaction record
  await Transaction.create({
    user: request.user._id,
    type: 'withdrawal_fee',
    amount: -fee,
    description: `Withdrawal fee for ${items.length} items`,
    metadata: {
      withdrawalId: withdrawal._id,
      itemCount: items.length,
      totalValue
    }
  });

  return reply.send({
    success: true,
    withdrawal: {
      id: withdrawal._id,
      totalValue,
      fee,
      status: 'pending',
      message: 'Withdrawal request created. Admin will process it soon.'
    }
  });
});

📋 Phase 4: Promotions System (Priority: HIGH)

4.1 Promotion Validation & Application

Files to Create:

  • services/promotionService.js

Implementation:

// services/promotionService.js
class PromotionService {
  async validatePromoCode(code, user, amount) {
    const config = await SiteConfig.getConfig();
    
    // Find promotion
    const promotion = config.promotions.find(p => 
      p.code === code && p.enabled
    );

    if (!promotion) {
      return { valid: false, message: 'Invalid promo code' };
    }

    // Check dates
    const now = new Date();
    if (promotion.startDate && now < new Date(promotion.startDate)) {
      return { valid: false, message: 'Promotion not started yet' };
    }
    if (promotion.endDate && now > new Date(promotion.endDate)) {
      return { valid: false, message: 'Promotion has expired' };
    }

    // Check new users only
    if (promotion.newUsersOnly) {
      const firstDeposit = await Deposit.findOne({ user: user._id, status: 'completed' });
      if (firstDeposit) {
        return { valid: false, message: 'Promotion is for new users only' };
      }
    }

    // Check minimum deposit
    if (amount < promotion.minDeposit) {
      return { 
        valid: false, 
        message: `Minimum deposit for this promo is $${promotion.minDeposit}` 
      };
    }

    // Check usage limits
    const usageCount = await PromoUsage.countDocuments({
      promotionId: promotion.id,
      user: user._id
    });

    if (promotion.maxUsesPerUser && usageCount >= promotion.maxUsesPerUser) {
      return { valid: false, message: 'You have used this promotion too many times' };
    }

    const totalUsage = await PromoUsage.countDocuments({
      promotionId: promotion.id
    });

    if (promotion.maxTotalUses && totalUsage >= promotion.maxTotalUses) {
      return { valid: false, message: 'Promotion usage limit reached' };
    }

    // Calculate bonus
    let bonusAmount = 0;
    
    if (promotion.bonusPercentage > 0) {
      bonusAmount = amount * (promotion.bonusPercentage / 100);
    }
    
    if (promotion.bonusAmount > 0) {
      bonusAmount = promotion.bonusAmount;
    }

    // Apply max bonus cap
    if (promotion.maxBonus > 0 && bonusAmount > promotion.maxBonus) {
      bonusAmount = promotion.maxBonus;
    }

    return {
      valid: true,
      promotion,
      bonusAmount
    };
  }

  async recordUsage(promotion, user, deposit) {
    await PromoUsage.create({
      promotionId: promotion.id,
      promotionCode: promotion.code,
      user: user._id,
      depositId: deposit._id,
      bonusAmount: deposit.bonusApplied,
      usedAt: new Date()
    });
  }
}

4.2 Frontend Promo Code Input

Files to Modify:

  • frontend/src/views/DepositPage.vue

Implementation:

<template>
  <div class="deposit-page">
    <!-- ... existing deposit UI ... -->
    
    <div class="promo-section">
      <label>Promo Code (Optional)</label>
      <div class="promo-input-group">
        <input 
          v-model="promoCode"
          placeholder="Enter promo code"
          @input="validatePromo"
        />
        <button @click="applyPromo" :disabled="!promoCode">
          Apply
        </button>
      </div>
      
      <div v-if="promoResult.valid" class="promo-success">
         {{ promoResult.promotion.name }}: +${{ promoResult.bonusAmount.toFixed(2) }} bonus!
      </div>
      
      <div v-if="promoResult.error" class="promo-error">
        {{ promoResult.error }}
      </div>
    </div>

    <!-- Show total with bonus -->
    <div class="deposit-summary">
      <div>Deposit Value: ${{ depositValue.toFixed(2) }}</div>
      <div v-if="promoResult.bonusAmount > 0" class="bonus-line">
        Bonus: +${{ promoResult.bonusAmount.toFixed(2) }}
      </div>
      <div class="total-line">
        Total: ${{ (depositValue + promoResult.bonusAmount).toFixed(2) }}
      </div>
    </div>
  </div>
</template>

<script setup>
const promoCode = ref('');
const promoResult = ref({ valid: false, bonusAmount: 0 });

const validatePromo = async () => {
  if (!promoCode.value) {
    promoResult.value = { valid: false, bonusAmount: 0 };
    return;
  }

  try {
    const response = await axios.post('/api/config/promotions/validate', {
      code: promoCode.value,
      amount: depositValue.value
    });

    if (response.data.valid) {
      promoResult.value = response.data;
    } else {
      promoResult.value = { valid: false, error: response.data.message };
    }
  } catch (error) {
    promoResult.value = { valid: false, error: 'Failed to validate promo code' };
  }
};
</script>

📋 Phase 5: Frontend Integration (Priority: MEDIUM)

5.1 Config Status Endpoint

Files to Modify:

  • routes/config.js

Implementation:

// GET /api/config/status - Public endpoint
fastify.get('/status', async (request, reply) => {
  const config = await SiteConfig.getConfig();
  
  return reply.send({
    success: true,
    maintenance: {
      enabled: config.maintenance.enabled,
      message: config.maintenance.message,
      scheduledEnd: config.maintenance.scheduledEnd
    },
    trading: {
      enabled: config.trading.enabled,
      depositEnabled: config.trading.depositEnabled,
      withdrawEnabled: config.trading.withdrawEnabled,
      minDeposit: config.trading.minDeposit,
      minWithdraw: config.trading.minWithdraw,
      withdrawFee: config.trading.withdrawFee
    },
    market: {
      enabled: config.market.enabled,
      minListingPrice: config.market.minListingPrice,
      maxListingPrice: config.market.maxListingPrice,
      commission: config.market.commission
    }
  });
});

5.2 Frontend Status Store

Files to Create:

  • frontend/src/stores/config.js

Implementation:

import { defineStore } from 'pinia';
import axios from '@/utils/axios';

export const useConfigStore = defineStore('config', {
  state: () => ({
    status: {
      maintenance: { enabled: false },
      trading: { enabled: true },
      market: { enabled: true }
    },
    loaded: false
  }),

  getters: {
    isMaintenanceMode: (state) => state.status.maintenance.enabled,
    isTradingEnabled: (state) => state.status.trading.enabled,
    isMarketEnabled: (state) => state.status.market.enabled,
    canDeposit: (state) => state.status.trading.depositEnabled,
    canWithdraw: (state) => state.status.trading.withdrawEnabled
  },

  actions: {
    async fetchStatus() {
      try {
        const response = await axios.get('/api/config/status');
        if (response.data.success) {
          this.status = response.data;
          this.loaded = true;
        }
      } catch (error) {
        console.error('Failed to fetch config status:', error);
      }
    }
  }
});

5.3 Route Guards

Files to Modify:

  • frontend/src/router/index.js

Implementation:

router.beforeEach(async (to, from, next) => {
  const configStore = useConfigStore();
  
  if (!configStore.loaded) {
    await configStore.fetchStatus();
  }

  // Block market access if disabled
  if (to.path.startsWith('/market') && !configStore.isMarketEnabled) {
    toast.warning('Marketplace is currently disabled');
    next({ name: 'Home' });
    return;
  }

  // Block deposit if disabled
  if (to.path === '/deposit' && !configStore.canDeposit) {
    toast.warning('Deposits are currently disabled');
    next({ name: 'Home' });
    return;
  }

  // Block withdraw if disabled
  if (to.path === '/withdraw' && !configStore.canWithdraw) {
    toast.warning('Withdrawals are currently disabled');
    next({ name: 'Home' });
    return;
  }

  next();
});

📋 Phase 6: Testing & Polish (Priority: MEDIUM)

6.1 Admin Testing Checklist

  • Toggle maintenance ON → site blocks non-admins
  • Toggle maintenance OFF → site accessible again
  • Scheduled maintenance activates at correct time
  • Disable deposits → deposit page blocked
  • Disable withdrawals → withdraw page blocked
  • Disable market → market page blocked
  • Change min deposit → enforced on deposit attempts
  • Change min withdraw → enforced on withdraw attempts
  • Change commission → applied to new sales
  • Apply promo code → bonus calculated correctly
  • Promo usage limit → blocked after max uses
  • New user only promo → blocked for existing users

6.2 Error Handling

  • Graceful degradation if config fetch fails
  • Clear error messages for users
  • Admin notifications for system issues
  • Fallback to default values if config missing

6.3 Performance Optimization

  • Cache config in memory (refresh every 5 minutes)
  • Avoid fetching config on every request
  • Use Redis for config caching in production
  • Batch price updates

📋 Phase 7: Documentation & Deployment (Priority: LOW)

7.1 API Documentation

  • Document all new endpoints
  • Add request/response examples
  • Error code reference
  • Rate limiting info

7.2 Admin Guide

  • How to enable/disable features
  • How to create promotions
  • How to schedule maintenance
  • Common troubleshooting

7.3 Deployment Checklist

  • Environment variables configured
  • Database migrations run
  • Steam bot configured
  • Price API keys set
  • Monitoring enabled
  • Backup strategy in place

🚀 Implementation Order

Week 1: Critical Infrastructure

  1. Day 1-2: Maintenance mode enforcement
  2. Day 3-4: Market settings integration
  3. Day 5: Config status endpoint & frontend store

Week 2: Trading System

  1. Day 1-3: Deposit system implementation
  2. Day 4-5: Withdrawal system implementation

Week 3: Promotions & Polish

  1. Day 1-2: Promotions validation & application
  2. Day 3-4: Frontend integration & UI
  3. Day 5: Testing & bug fixes

Week 4: Advanced Features

  1. Day 1-2: Auto price updates
  2. Day 3: Scheduled maintenance automation
  3. Day 4-5: Documentation & final testing

📊 Success Metrics

Functionality

  • All admin toggles actually work
  • Settings are enforced across the platform
  • Promotions apply correctly
  • Commission is calculated and recorded
  • Limits are respected

Performance

  • Config fetch < 50ms
  • No performance degradation from checks
  • Cache hit rate > 95%

User Experience

  • Clear error messages
  • No unexpected blocking
  • Smooth transitions
  • Proper feedback

🎯 Final Deliverables

  1. Backend

    • Trading routes fully implemented
    • All admin settings enforced
    • Promotion system working
    • Proper error handling
    • Comprehensive logging
  2. Frontend

    • Deposit/withdraw pages functional
    • Promo code input working
    • Real-time validation
    • Config-aware navigation
    • Clear status indicators
  3. Admin Panel

    • All settings actually work
    • Real-time effect on site
    • Usage statistics visible
    • Easy testing/debugging
  4. Documentation

    • API documentation complete
    • Admin guide written
    • Deployment guide ready
    • Troubleshooting reference

Estimated Total Time: 3-4 weeks of focused development Complexity: Medium-High Risk Level: Medium (Steam bot integration, real money handling) Priority: High (Core functionality for platform)