diff --git a/package-lock.json b/package-lock.json index aa8bad4..8d48a81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "openai": "^4.104.0", "pokersolver": "^2.1.4", "socket.io": "^4.8.1", + "stripe": "^20.3.0", "unique-names-generator": "^4.7.1", "uuid": "^11.1.0" }, @@ -36,15 +37,15 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.3.tgz", - "integrity": "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.6.1", - "@discordjs/util": "^1.1.1", + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.16", + "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" @@ -66,12 +67,12 @@ } }, "node_modules/@discordjs/formatters": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", - "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", "license": "Apache-2.0", "dependencies": { - "discord-api-types": "^0.38.1" + "discord-api-types": "^0.38.33" }, "engines": { "node": ">=16.11.0" @@ -116,10 +117,13 @@ } }, "node_modules/@discordjs/util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, "engines": { "node": ">=18" }, @@ -865,29 +869,58 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1248,9 +1281,9 @@ } }, "node_modules/discord-api-types": { - "version": "0.38.23", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.23.tgz", - "integrity": "sha512-C8VjK0yxBUq1dakxGpUXQm4VSC7R+aaD2SIr3paj2a0bP/LRok1AqHiezp30GruK6Ba9FtQAKqYUMJPzsqv7IQ==", + "version": "0.38.38", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.38.tgz", + "integrity": "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q==", "license": "MIT", "workspaces": [ "scripts/actions/documentation" @@ -1266,19 +1299,19 @@ } }, "node_modules/discord.js": { - "version": "14.22.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.22.1.tgz", - "integrity": "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==", + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.11.2", + "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.1", + "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", - "@discordjs/util": "^1.1.1", + "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.16", + "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", @@ -1729,39 +1762,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -2601,12 +2634,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -2651,9 +2684,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -3290,12 +3323,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -3314,20 +3347,49 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -3910,6 +3972,23 @@ "node": ">=0.10.0" } }, + "node_modules/stripe": { + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.0.tgz", + "integrity": "sha512-DYzcmV1MfYhycr1GwjCjeQVYk9Gu8dpxyTlu7qeDCsuguug7oUTxPsUQuZeSf/OPzK7pofqobvOKVqAwlpgf/Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@types/node": ">=16" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index c2236fa..ec9da38 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "openai": "^4.104.0", "pokersolver": "^2.1.4", "socket.io": "^4.8.1", + "stripe": "^20.3.0", "unique-names-generator": "^4.7.1", "uuid": "^11.1.0" }, diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index 8230bc0..0fe47ac 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -1,4 +1,5 @@ import { sleep } from "openai/core"; +import { AttachmentBuilder } from "discord.js"; import { buildAiMessages, buildParticipantsMap, @@ -52,10 +53,10 @@ export async function handleMessageCreate(message, client, io) { // --- Main Guild Features (Points & Slowmode) --- if (message.guildId === process.env.GUILD_ID) { // Award points for activity - const pointsAwarded = channelPointsHandler(message); - if (pointsAwarded) { - io.emit("data-updated", { table: "users", action: "update" }); - } + // const pointsAwarded = channelPointsHandler(message); + // if (pointsAwarded) { + // io.emit("data-updated", { table: "users", action: "update" }); + // } // Enforce active slowmodes const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes); @@ -245,7 +246,10 @@ async function handleAdminCommands(message) { try { const stmt = flopoDB.prepare(sqlCommand); const result = sqlCommand.trim().toUpperCase().startsWith("SELECT") ? stmt.all() : stmt.run(); - message.reply("```json\n" + JSON.stringify(result, null, 2).substring(0, 1900) + "\n```"); + const jsonString = JSON.stringify(result, null, 2); + const buffer = Buffer.from(jsonString, "utf-8"); + const attachment = new AttachmentBuilder(buffer, { name: "sql-result.json" }); + message.reply({ content: "SQL query executed successfully:", files: [attachment] }); } catch (e) { message.reply(`SQL Error: ${e.message}`); } diff --git a/src/database/index.js b/src/database/index.js index 360011f..a3b96f5 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -269,6 +269,49 @@ flopoDB.exec(` score INTEGER ); + + CREATE TABLE IF NOT EXISTS transactions + ( + id + TEXT + PRIMARY + KEY, + session_id + TEXT + UNIQUE + NOT + NULL, + user_id + TEXT + REFERENCES + users + NOT + NULL, + coins_amount + INTEGER + NOT + NULL, + amount_cents + INTEGER + NOT + NULL, + currency + TEXT + DEFAULT + 'eur', + customer_email + TEXT, + customer_name + TEXT, + payment_status + TEXT + NOT + NULL, + created_at + DATETIME + DEFAULT + CURRENT_TIMESTAMP + ); `); /* ----------------------------------------------------- @@ -566,6 +609,52 @@ export const stmtSOTDStats = flopoDB.prepare(` `); stmtSOTDStats.run(); +export const stmtTransactions = flopoDB.prepare(` + CREATE TABLE IF NOT EXISTS transactions + ( + id + TEXT + PRIMARY + KEY, + session_id + TEXT + UNIQUE + NOT + NULL, + user_id + TEXT + REFERENCES + users + NOT + NULL, + coins_amount + INTEGER + NOT + NULL, + amount_cents + INTEGER + NOT + NULL, + currency + TEXT + DEFAULT + 'eur', + customer_email + TEXT, + customer_name + TEXT, + payment_status + TEXT + NOT + NULL, + created_at + DATETIME + DEFAULT + CURRENT_TIMESTAMP + ) +`); +stmtTransactions.run(); + /* ------------------------- USER statements ----------------------------*/ @@ -861,3 +950,23 @@ export async function pruneOldLogs() { transaction(); } + +/* ------------------------- + TRANSACTION statements +----------------------------*/ +export const insertTransaction = flopoDB.prepare( + `INSERT INTO transactions (id, session_id, user_id, coins_amount, amount_cents, currency, customer_email, customer_name, payment_status) + VALUES (@id, @session_id, @user_id, @coins_amount, @amount_cents, @currency, @customer_email, @customer_name, @payment_status)`, +); + +export const getTransactionBySessionId = flopoDB.prepare( + `SELECT * FROM transactions WHERE session_id = ?`, +); + +export const getAllTransactions = flopoDB.prepare( + `SELECT * FROM transactions ORDER BY created_at DESC`, +); + +export const getUserTransactions = flopoDB.prepare( + `SELECT * FROM transactions WHERE user_id = ? ORDER BY created_at DESC`, +); diff --git a/src/server/app.js b/src/server/app.js index 70c746c..ff182c6 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -37,6 +37,9 @@ app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (re await handleInteraction(req, res, client); }); +// Stripe webhook endpoint needs raw body for signature verification +app.use("/api/buy-coins", express.raw({ type: "application/json" })); + // JSON Body Parser Middleware app.use(express.json()); diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 931e139..0e97e0b 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -1,5 +1,6 @@ import express from "express"; import { sleep } from "openai/core"; +import Stripe from "stripe"; // --- Database Imports --- import { @@ -21,6 +22,10 @@ import { queryDailyReward, updateSkin, updateUserCoins, + insertTransaction, + getTransactionBySessionId, + getAllTransactions, + getUserTransactions, } from "../../database/index.js"; // --- Game State Imports --- @@ -1277,24 +1282,174 @@ export function apiRoutes(client, io) { } }); - // --- Admin Routes --- + // Fixed coin offers - server-side source of truth + const COIN_OFFERS = [ + { id: "offer_5000", coins: 5000, amount_cents: 99, label: "5 000 FlopoCoins" }, + { id: "offer_20000", coins: 20000, amount_cents: 299, label: "20 000 FlopoCoins" }, + { id: "offer_40000", coins: 40000, amount_cents: 499, label: "40 000 FlopoCoins" }, + { id: "offer_100000", coins: 100000, amount_cents: 999, label: "100 000 FlopoCoins" }, + ]; - router.post("/buy-coins", (req, res) => { - const { commandUserId, coins } = req.body; - const user = getUser.get(commandUserId); - if (!user) return res.status(404).json({ error: "User not found" }); + router.get("/coin-offers", (req, res) => { + res.json({ offers: COIN_OFFERS }); + }); - const newCoins = user.coins + coins; - updateUserCoins.run({ id: commandUserId, coins: newCoins }); - insertLog.run({ - id: `${commandUserId}-buycoins-${Date.now()}`, - user_id: commandUserId, - action: "BUY_COINS_ADMIN", - coins_amount: coins, - user_new_amount: newCoins, - }); + router.post("/create-checkout-session", async (req, res) => { + const { userId, offerId } = req.body; - res.status(200).json({ message: `Added ${coins} coins.` }); + if (!userId || !offerId) { + return res.status(400).json({ error: "Missing required fields: userId, offerId" }); + } + + const offer = COIN_OFFERS.find((o) => o.id === offerId); + if (!offer) { + return res.status(400).json({ error: "Invalid offer" }); + } + + const user = getUser.get(userId); + if (!user) { + return res.status(404).json({ error: "User not found" }); + } + + try { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL; + + const session = await stripe.checkout.sessions.create({ + payment_method_types: ['card'], + line_items: [ + { + price_data: { + currency: 'eur', + product_data: { + name: offer.label, + description: `Achat de ${offer.label} pour FlopoBot`, + }, + unit_amount: offer.amount_cents, + }, + quantity: 1, + }, + ], + mode: 'payment', + success_url: `${FLAPI_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${FLAPI_URL}/dashboard`, + metadata: { + userId: userId, + coins: offer.coins.toString(), + }, + }); + + console.log(`[CHECKOUT] New session for user ${userId}: ${session.id}, offer: ${offer.id} (${offer.coins} coins for ${offer.amount_cents} cents)`); + + res.json({ sessionId: session.id }); + } catch (error) { + console.error("Error creating checkout session:", error); + res.status(500).json({ error: "Failed to create checkout session" }); + } + }); + + router.post("/buy-coins", async (req, res) => { + const sig = req.headers['stripe-signature']; + const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; + + if (!endpointSecret) { + console.error("STRIPE_WEBHOOK_SECRET not configured"); + return res.status(500).json({ error: "Webhook not configured" }); + } + + let event; + + try { + // Verify webhook signature - requires raw body + // Note: You need to configure Express to preserve raw body for this route + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); + } catch (err) { + console.error(`Webhook signature verification failed: ${err.message}`); + return res.status(400).json({ error: `Webhook Error: ${err.message}` }); + } + + // Handle the event + if (event.type === 'checkout.session.completed') { + const session = event.data.object; + + // Extract metadata from the checkout session + const commandUserId = session.metadata?.userId; + const expectedCoins = parseInt(session.metadata?.coins); + const amountPaid = session.amount_total; // in cents + const currency = session.currency; + const customerEmail = session.customer_details?.email; + const customerName = session.customer_details?.name; + + // Validate metadata exists + if (!commandUserId || !expectedCoins) { + console.error("Missing userId or coins in session metadata"); + return res.status(400).json({ error: "Invalid session metadata" }); + } + + // Verify payment was successful + if (session.payment_status !== 'paid') { + console.error(`Payment not completed for session ${session.id}`); + return res.status(400).json({ error: "Payment not completed" }); + } + + // Check for duplicate processing (idempotency) + const existingTransaction = getTransactionBySessionId.get(session.id); + if (existingTransaction) { + console.log(`Payment already processed: ${session.id}`); + return res.status(200).json({ message: "Already processed" }); + } + + // Get user + const user = getUser.get(commandUserId); + if (!user) { + console.error(`User not found: ${commandUserId}`); + return res.status(404).json({ error: "User not found" }); + } + + // Update coins + const newCoins = user.coins + expectedCoins; + updateUserCoins.run({ id: commandUserId, coins: newCoins }); + + // Insert transaction record + const transactionId = `${commandUserId}-transaction-${Date.now()}`; + insertTransaction.run({ + id: transactionId, + session_id: session.id, + user_id: commandUserId, + coins_amount: expectedCoins, + amount_cents: amountPaid, + currency: currency, + customer_email: customerEmail, + customer_name: customerName, + payment_status: session.payment_status, + }); + + // Insert log entry + insertLog.run({ + id: `${commandUserId}-buycoins-${Date.now()}`, + user_id: commandUserId, + action: "BUY_COINS", + target_user_id: null, + coins_amount: expectedCoins, + user_new_amount: newCoins, + }); + + console.log(`Payment processed: ${commandUserId} purchased ${expectedCoins} coins for ${amountPaid/100} ${currency}`); + + // Notify user via Discord if possible + try { + const discordUser = await client.users.fetch(commandUserId); + await discordUser.send(`✅ Votre achat de ${expectedCoins} FlopoCoins a été confirmé ! Merci pour votre soutien !`); + } catch (e) { + console.log(`Could not DM user ${commandUserId}:`, e.message); + } + + return res.status(200).json({ message: `Added ${expectedCoins} coins.` }); + } + + // Return 200 for unhandled event types (Stripe requires this) + res.status(200).json({ received: true }); }); return router;