/** * routes/payments.js — GSO Premium Gem Store (GDD Phase 3 Monetisation) * * Endpoints: * POST /api/payments/create-checkout — create Stripe Checkout session * POST /api/payments/webhook — Stripe webhook (payment confirmation) * GET /api/payments/products — list purchasable gem packages * * Requires ENV vars: * STRIPE_SECRET_KEY — sk_live_... (or sk_test_... for development) * STRIPE_WEBHOOK_SECRET — whsec_... (from Stripe Dashboard → Webhooks) * CLIENT_URL — https://galaxystrike.online (for redirect URLs) * * Stripe is loaded lazily so the server starts even if the package isn't installed yet. * Run: npm install stripe in the API directory before going live. */ const express = require('express'); const router = express.Router(); // ── Gem packages available for purchase ──────────────────────────────────────── const GEM_PACKAGES = [ { id: 'gems_80', gems: 80, priceUSD: 0.99, label: 'Starter Pack', bonus: 0, popular: false }, { id: 'gems_200', gems: 200, priceUSD: 1.99, label: 'Pilot Pack', bonus: 0, popular: false }, { id: 'gems_500', gems: 500, priceUSD: 4.99, label: 'Commander Pack', bonus: 50, popular: true }, { id: 'gems_1200', gems: 1200, priceUSD: 9.99, label: 'Admiral Pack', bonus: 200,popular: false }, { id: 'gems_2500', gems: 2500, priceUSD: 19.99, label: 'Fleet Admiral', bonus: 500,popular: false }, { id: 'gems_6500', gems: 6500, priceUSD: 49.99, label: 'Grand Admiral', bonus: 1500,popular: false}, ]; // ── Helper: load Stripe lazily ──────────────────────────────────────────────── function getStripe() { if (!process.env.STRIPE_SECRET_KEY) throw new Error('STRIPE_SECRET_KEY not configured'); // eslint-disable-next-line global-require return require('stripe')(process.env.STRIPE_SECRET_KEY); } // ── GET /api/payments/products ──────────────────────────────────────────────── router.get('/products', (req, res) => { res.json({ packages: GEM_PACKAGES }); }); // ── POST /api/payments/create-checkout ──────────────────────────────────────── // Body: { packageId, userId, username } router.post('/create-checkout', async (req, res) => { try { const { packageId, userId, username } = req.body; if (!packageId || !userId) return res.status(400).json({ error: 'packageId and userId required' }); const pkg = GEM_PACKAGES.find(p => p.id === packageId); if (!pkg) return res.status(404).json({ error: 'Unknown package' }); const stripe = getStripe(); const totalGems = pkg.gems + pkg.bonus; const clientUrl = process.env.CLIENT_URL || 'http://localhost:3000'; const session = await stripe.checkout.sessions.create({ mode: 'payment', payment_method_types: ['card'], line_items: [{ price_data: { currency: 'usd', unit_amount: Math.round(pkg.priceUSD * 100), // cents product_data: { name: `${pkg.label} — ${totalGems} 💎 Gems`, description: pkg.bonus > 0 ? `${pkg.gems} gems + ${pkg.bonus} bonus gems (${totalGems} total)` : `${pkg.gems} Galaxy Strike Online gems`, metadata: { packageId: pkg.id }, }, }, quantity: 1, }], client_reference_id: userId, customer_email: req.body.email || undefined, metadata: { userId, username: username || 'unknown', packageId: pkg.id, gems: String(totalGems), }, success_url: `${clientUrl}/payment-success?session={CHECKOUT_SESSION_ID}`, cancel_url: `${clientUrl}/payment-cancel`, }); res.json({ sessionId: session.id, url: session.url }); } catch (err) { console.error('[PAYMENTS] create-checkout error:', err.message); res.status(500).json({ error: err.message }); } }); // ── POST /api/payments/webhook ──────────────────────────────────────────────── // Stripe sends raw body — must use express.raw() middleware for this route. router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature']; const secret = process.env.STRIPE_WEBHOOK_SECRET; if (!secret) { console.error('[PAYMENTS] STRIPE_WEBHOOK_SECRET not set — webhook rejected'); return res.status(400).send('Webhook secret not configured'); } let event; try { const stripe = getStripe(); event = stripe.webhooks.constructEvent(req.body, sig, secret); } catch (err) { console.error('[PAYMENTS] Webhook signature verification failed:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'checkout.session.completed') { const session = event.data.object; const { userId, username, packageId, gems } = session.metadata || {}; if (!userId || !gems) { console.error('[PAYMENTS] Webhook missing metadata', session.metadata); return res.json({ received: true }); } const gemCount = parseInt(gems, 10); const amountCents = session.amount_total || 0; console.log(`[PAYMENTS] ✅ Payment confirmed: ${username} (${userId}) — ${gemCount} gems — $${(amountCents/100).toFixed(2)}`); // Credit gems in the database directly try { const PlayerData = require('../models/PlayerData'); const player = await PlayerData.findOne({ userId }); if (player) { player.stats.gems = (player.stats.gems || 0) + gemCount; player.markModified('stats'); await player.save(); console.log(`[PAYMENTS] Credited ${gemCount} gems to ${username}. New balance: ${player.stats.gems}`); } else { console.warn(`[PAYMENTS] Player not found for userId: ${userId}`); } } catch (dbErr) { console.error('[PAYMENTS] DB credit error:', dbErr.message); // Return 200 to Stripe regardless — Stripe will retry on 5xx } // Attempt to push live notification to game server via internal HTTP // (Game server will push 'gems_credited' to the player's socket if online) const gameServerUrl = process.env.GAME_SERVER_URL || 'http://localhost:3002'; try { const http = require('http'); const body = JSON.stringify({ userId, gems: gemCount, packageId, amountCents }); const options = { hostname: new URL(gameServerUrl).hostname, port: new URL(gameServerUrl).port || 3002, path: '/internal/credit-gems', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'X-Internal-Key': process.env.INTERNAL_API_KEY || 'gso-internal', }, }; const req2 = http.request(options); req2.write(body); req2.end(); } catch (notifyErr) { // Non-fatal — player will see updated gems on next login } } res.json({ received: true }); }); module.exports = router; module.exports.GEM_PACKAGES = GEM_PACKAGES;