174 lines
7.3 KiB
JavaScript
174 lines
7.3 KiB
JavaScript
/**
|
|
* 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;
|