← back to Handbag Auth Nextjs

arbitrage/server.js

404 lines

const express = require('express');
const helmet = require('helmet');
const Database = require('better-sqlite3');
const cors = require('cors');
const path = require('path');

const app = express();
// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
app.use(helmet({ contentSecurityPolicy: false }));
const PORT = 7600;

app.use(cors());
app.use(express.json());
app.use(express.static(__dirname));

// Database path
const DB_PATH = path.join(__dirname, '..', 'data', 'arbitrage.db');

// Live exchange rates (in production, fetch from API)
const EXCHANGE_RATES = {
  JPYUSD: 0.0069, // 1 JPY = $0.0069 (approx ¥145/$1)
  HKDUSD: 0.128,  // 1 HKD = $0.128 (approx HKD 7.8/$1)
  lastUpdated: new Date().toISOString()
};

// Cost assumptions
const COST_ASSUMPTIONS = {
  taxRefundPct: 10.0,     // Japan consumption tax refund
  shippingUSD: {
    economy: 50,
    express: 100
  },
  dutyPct: 5.0,           // US import duty on luxury goods
  paymentFeePct: 3.0,     // Credit card/PayPal fees
  authenticationUSD: 50,  // Authentication service
  sellingFeePct: {
    ebay: 13.0,
    fashionphile: 15.0,
    therealreal: 20.0,
    rebag: 18.0,
    vestiaire: 15.0
  }
};

// === ARBITRAGE CALCULATOR ===
function calculateArbitrage(params) {
  const {
    brand,
    model,
    japanPriceJPY,
    usaPriceUSD = null,
    hkPriceHKD = null,
    condition = 'excellent',
    shippingType = 'economy',
    includeAuth = true,
    sellingPlatform = 'fashionphile'
  } = params;

  // Step 1: Japan purchase cost
  const taxRefund = japanPriceJPY * (COST_ASSUMPTIONS.taxRefundPct / 100);
  const netPurchaseJPY = japanPriceJPY - taxRefund;
  const purchaseUSD = netPurchaseJPY * EXCHANGE_RATES.JPYUSD;

  // Step 2: Additional costs
  const shipping = COST_ASSUMPTIONS.shippingUSD[shippingType];
  const duties = purchaseUSD * (COST_ASSUMPTIONS.dutyPct / 100);
  const paymentFees = purchaseUSD * (COST_ASSUMPTIONS.paymentFeePct / 100);
  const authentication = includeAuth ? COST_ASSUMPTIONS.authenticationUSD : 0;

  const totalCostUSD = purchaseUSD + shipping + duties + paymentFees + authentication;

  // Step 3: USA revenue (if provided)
  let usaResult = null;
  if (usaPriceUSD) {
    const sellingFee = usaPriceUSD * (COST_ASSUMPTIONS.sellingFeePct[sellingPlatform] / 100);
    const netRevenueUSD = usaPriceUSD - sellingFee;
    const profitUSD = netRevenueUSD - totalCostUSD;
    const profitMarginPct = (profitUSD / totalCostUSD) * 100;

    usaResult = {
      sellPrice: usaPriceUSD,
      sellingFee,
      netRevenue: netRevenueUSD,
      profit: profitUSD,
      profitMargin: profitMarginPct,
      profitable: profitUSD > 0,
      recommendation: profitMarginPct > 30 ? 'EXCELLENT' : (profitMarginPct > 15 ? 'GOOD' : (profitMarginPct > 0 ? 'MARGINAL' : 'SKIP'))
    };
  }

  // Step 4: HK revenue (if provided)
  let hkResult = null;
  if (hkPriceHKD) {
    const hkPriceUSD = hkPriceHKD * EXCHANGE_RATES.HKDUSD;
    const sellingFee = hkPriceUSD * 0.15; // Assume 15% for HK platforms
    const netRevenueUSD = hkPriceUSD - sellingFee;
    const profitUSD = netRevenueUSD - totalCostUSD;
    const profitMarginPct = (profitUSD / totalCostUSD) * 100;

    hkResult = {
      sellPrice: hkPriceUSD,
      sellPriceHKD: hkPriceHKD,
      sellingFee,
      netRevenue: netRevenueUSD,
      profit: profitUSD,
      profitMargin: profitMarginPct,
      profitable: profitUSD > 0,
      recommendation: profitMarginPct > 30 ? 'EXCELLENT' : (profitMarginPct > 15 ? 'GOOD' : (profitMarginPct > 0 ? 'MARGINAL' : 'SKIP'))
    };
  }

  return {
    product: { brand, model, condition },
    costBreakdown: {
      japan: {
        purchaseJPY: japanPriceJPY,
        taxRefundJPY: taxRefund,
        netPurchaseJPY: netPurchaseJPY,
        purchaseUSD: Math.round(purchaseUSD * 100) / 100
      },
      additionalCosts: {
        shipping: shipping,
        duties: Math.round(duties * 100) / 100,
        paymentFees: Math.round(paymentFees * 100) / 100,
        authentication: authentication
      },
      totalCostUSD: Math.round(totalCostUSD * 100) / 100
    },
    usa: usaResult,
    hongkong: hkResult,
    exchangeRates: EXCHANGE_RATES,
    bestRoute: determineBestRoute(usaResult, hkResult)
  };
}

function determineBestRoute(usaResult, hkResult) {
  if (!usaResult && !hkResult) return null;
  if (!usaResult) return { market: 'Hong Kong', ...hkResult };
  if (!hkResult) return { market: 'USA', ...usaResult };

  if (usaResult.profit > hkResult.profit) {
    return { market: 'USA', ...usaResult };
  } else {
    return { market: 'Hong Kong', ...hkResult };
  }
}

// === API ROUTES ===

// GET /api/arbitrage/rates - Get current exchange rates
app.get('/api/arbitrage/rates', (req, res) => {
  res.json({
    success: true,
    rates: EXCHANGE_RATES
  });
});

// POST /api/arbitrage/calculate - Calculate arbitrage for a specific item
app.post('/api/arbitrage/calculate', (req, res) => {
  try {
    const result = calculateArbitrage(req.body);
    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    res.status(400).json({
      success: false,
      error: error.message
    });
  }
});

// GET /api/arbitrage/opportunities - Get saved arbitrage opportunities
app.get('/api/arbitrage/opportunities', (req, res) => {
  try {
    const db = new Database(DB_PATH, { readonly: true });

    const minMargin = parseFloat(req.query.minMargin) || 0;
    const limit = parseInt(req.query.limit) || 50;
    const brand = req.query.brand || null;

    let query = `
      SELECT *
      FROM arbitrage_opportunities
      WHERE is_active = 1 AND profit_margin_pct >= ?
    `;
    const params = [minMargin];

    if (brand) {
      query += ` AND brand = ?`;
      params.push(brand);
    }

    query += ` ORDER BY profit_margin_pct DESC LIMIT ?`;
    params.push(limit);

    const opportunities = db.prepare(query).all(...params);
    db.close();

    res.json({
      success: true,
      count: opportunities.length,
      data: opportunities
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message,
      data: []
    });
  }
});

// GET /api/arbitrage/stats - Get arbitrage statistics
app.get('/api/arbitrage/stats', (req, res) => {
  try {
    const db = new Database(DB_PATH, { readonly: true });

    const totalOpportunities = db.prepare(`
      SELECT COUNT(*) as count FROM arbitrage_opportunities WHERE is_active = 1
    `).get();

    const avgProfit = db.prepare(`
      SELECT AVG(potential_profit_usd) as avg FROM arbitrage_opportunities WHERE is_active = 1
    `).get();

    const avgMargin = db.prepare(`
      SELECT AVG(profit_margin_pct) as avg FROM arbitrage_opportunities WHERE is_active = 1
    `).get();

    const topBrand = db.prepare(`
      SELECT brand, AVG(profit_margin_pct) as avg_margin, COUNT(*) as count
      FROM arbitrage_opportunities
      WHERE is_active = 1
      GROUP BY brand
      ORDER BY avg_margin DESC
      LIMIT 1
    `).get();

    const profitDistribution = db.prepare(`
      SELECT
        SUM(CASE WHEN profit_margin_pct >= 40 THEN 1 ELSE 0 END) as excellent,
        SUM(CASE WHEN profit_margin_pct >= 25 AND profit_margin_pct < 40 THEN 1 ELSE 0 END) as good,
        SUM(CASE WHEN profit_margin_pct >= 10 AND profit_margin_pct < 25 THEN 1 ELSE 0 END) as marginal,
        SUM(CASE WHEN profit_margin_pct < 10 THEN 1 ELSE 0 END) as poor
      FROM arbitrage_opportunities
      WHERE is_active = 1
    `).get();

    db.close();

    res.json({
      success: true,
      stats: {
        totalOpportunities: totalOpportunities.count,
        avgProfit: Math.round((avgProfit.avg || 0) * 100) / 100,
        avgMargin: Math.round((avgMargin.avg || 0) * 10) / 10,
        topBrand: topBrand || { brand: 'N/A', avg_margin: 0 },
        profitDistribution: profitDistribution || { excellent: 0, good: 0, marginal: 0, poor: 0 }
      }
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// POST /api/arbitrage/save - Save a new arbitrage opportunity
app.post('/api/arbitrage/save', (req, res) => {
  try {
    const db = new Database(DB_PATH);

    const {
      brand, model, condition,
      japanPriceJPY, japanSource, japanUrl,
      usaPriceUSD, usaSource, usaUrl,
      hkPriceHKD, hkSource, hkUrl,
      costBreakdown
    } = req.body;

    // Calculate arbitrage
    const calc = calculateArbitrage({
      brand, model, japanPriceJPY, usaPriceUSD, hkPriceHKD, condition
    });

    const profit = calc.bestRoute ? calc.bestRoute.profit : 0;
    const margin = calc.bestRoute ? calc.bestRoute.profitMargin : 0;

    const stmt = db.prepare(`
      INSERT INTO arbitrage_opportunities (
        brand, model, condition,
        japan_price_jpy, japan_source, japan_url,
        usa_price_usd, usa_source, usa_url,
        hk_price_hkd, hk_source, hk_url,
        exchange_rate_jpy_usd, total_cost_usd,
        potential_profit_usd, profit_margin_pct,
        cost_breakdown
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);

    const result = stmt.run(
      brand, model, condition || 'excellent',
      japanPriceJPY, japanSource || 'Manual', japanUrl || '',
      usaPriceUSD || null, usaSource || 'Manual', usaUrl || '',
      hkPriceHKD || null, hkSource || null, hkUrl || null,
      EXCHANGE_RATES.JPYUSD, calc.costBreakdown.totalCostUSD,
      profit, margin,
      JSON.stringify(costBreakdown || calc.costBreakdown)
    );

    db.close();

    res.json({
      success: true,
      id: result.lastInsertRowid,
      calculation: calc
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// GET /api/arbitrage/compare - Compare prices across markets
app.get('/api/arbitrage/compare', (req, res) => {
  try {
    const brand = req.query.brand;
    const model = req.query.model;

    if (!brand || !model) {
      return res.status(400).json({
        success: false,
        error: 'brand and model parameters required'
      });
    }

    const db = new Database(DB_PATH, { readonly: true });

    const opportunities = db.prepare(`
      SELECT * FROM arbitrage_opportunities
      WHERE brand = ? AND model = ?
      ORDER BY created_at DESC
      LIMIT 5
    `).all(brand, model);

    db.close();

    if (opportunities.length === 0) {
      return res.json({
        success: true,
        found: false,
        message: 'No data found for this model'
      });
    }

    // Aggregate data
    const latest = opportunities[0];
    const avgJapanPrice = opportunities.reduce((sum, o) => sum + o.japan_price_jpy, 0) / opportunities.length;
    const avgUSAPrice = opportunities.reduce((sum, o) => sum + (o.usa_price_usd || 0), 0) / opportunities.filter(o => o.usa_price_usd).length;

    res.json({
      success: true,
      found: true,
      brand,
      model,
      comparison: {
        japan: {
          latest: latest.japan_price_jpy,
          average: Math.round(avgJapanPrice),
          latestUSD: Math.round(latest.japan_price_jpy * EXCHANGE_RATES.JPYUSD),
          source: latest.japan_source
        },
        usa: {
          latest: latest.usa_price_usd,
          average: Math.round(avgUSAPrice),
          source: latest.usa_source
        },
        arbitrage: {
          profit: latest.potential_profit_usd,
          margin: latest.profit_margin_pct,
          recommendation: latest.profit_margin_pct > 30 ? 'EXCELLENT' : (latest.profit_margin_pct > 15 ? 'GOOD' : 'MARGINAL')
        }
      },
      history: opportunities
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

app.listen(PORT, '0.0.0.0', () => {
  console.log(`💰 Arbitrage Calculator running on http://45.61.58.125:${PORT}`);
  console.log(`📊 Dashboard: http://45.61.58.125:${PORT}/arbitrage.html`);
  console.log(`🔌 API: http://45.61.58.125:${PORT}/api/arbitrage/opportunities`);
});