dubs-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,806 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const axios = require('axios');
4
+ const { pool } = require('../services/db'); // Shared database pool
5
+
6
+ const PANDASCORE_BASE_URL = 'https://api.pandascore.co';
7
+ const PANDASCORE_API_KEY = process.env.PANDASCORE_API_KEY;
8
+
9
+ // Socket.IO will be injected by server.js
10
+ let chatNamespace = null;
11
+
12
+ // Inject Socket.IO instance
13
+ router.setSocketIO = (ioInstance, chatNS) => {
14
+ chatNamespace = chatNS;
15
+ console.log('🔌 Socket.IO injected into esports routes');
16
+ };
17
+
18
+ /**
19
+ * Helper: build PandaScore request headers
20
+ */
21
+ function pandaHeaders() {
22
+ return {
23
+ accept: 'application/json',
24
+ authorization: `Bearer ${PANDASCORE_API_KEY}`
25
+ };
26
+ }
27
+
28
+ // ============================================
29
+ // LEAGUES
30
+ // ============================================
31
+
32
+ /**
33
+ * @route GET /api/esports/leagues
34
+ * @desc List all esports leagues (paginated)
35
+ * @access Public
36
+ * @query page, per_page, sort, search[name], filter[videogame_id]
37
+ */
38
+ router.get('/leagues', async (req, res) => {
39
+ try {
40
+ const params = buildParams(req.query);
41
+
42
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues`, {
43
+ headers: pandaHeaders(),
44
+ params,
45
+ timeout: 10000
46
+ });
47
+
48
+ res.json({ success: true, data: response.data });
49
+ } catch (error) {
50
+ console.error('Error fetching esports leagues:', error.message);
51
+ const status = error.response?.status || 500;
52
+ res.status(status).json({ success: false, error: 'Failed to fetch esports leagues' });
53
+ }
54
+ });
55
+
56
+ /**
57
+ * @route GET /api/esports/videogames/:videogameId/leagues
58
+ * @desc List leagues for a specific videogame
59
+ * @access Public
60
+ * @query page, per_page, sort, search[name]
61
+ */
62
+ router.get('/videogames/:videogameId/leagues', async (req, res) => {
63
+ try {
64
+ const { videogameId } = req.params;
65
+ const params = buildParams(req.query);
66
+
67
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/videogames/${videogameId}/leagues`, {
68
+ headers: pandaHeaders(),
69
+ params,
70
+ timeout: 10000
71
+ });
72
+
73
+ res.json({ success: true, data: response.data });
74
+ } catch (error) {
75
+ console.error(`Error fetching leagues for videogame ${req.params.videogameId}:`, error.message);
76
+ const status = error.response?.status || 500;
77
+ res.status(status).json({ success: false, error: 'Failed to fetch videogame leagues' });
78
+ }
79
+ });
80
+
81
+ /**
82
+ * @route GET /api/esports/leagues/:leagueId
83
+ * @desc Get a single esports league by ID
84
+ * @access Public
85
+ */
86
+ router.get('/leagues/:leagueId', async (req, res) => {
87
+ try {
88
+ const { leagueId } = req.params;
89
+
90
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueId}`, {
91
+ headers: pandaHeaders(),
92
+ timeout: 10000
93
+ });
94
+
95
+ res.json({ success: true, data: response.data });
96
+ } catch (error) {
97
+ console.error(`Error fetching esports league ${req.params.leagueId}:`, error.message);
98
+ const status = error.response?.status || 500;
99
+ res.status(status).json({ success: false, error: 'Failed to fetch esports league' });
100
+ }
101
+ });
102
+
103
+ // ============================================
104
+ // LEAGUE SUB-RESOURCES (Series, Tournaments, Matches)
105
+ // ============================================
106
+
107
+ /**
108
+ * Helper: build common query params from request.
109
+ * Express parses bracket notation (e.g. filter[status]=running) into nested
110
+ * objects ({ filter: { status: 'running' } }). We need to reconstruct the
111
+ * bracket keys for PandaScore's API.
112
+ */
113
+ function buildParams(query) {
114
+ const params = {};
115
+ const { page, per_page, sort } = query;
116
+ if (page) params.page = page;
117
+ if (per_page) params.per_page = per_page;
118
+ if (sort) params.sort = sort;
119
+
120
+ // Handle Express-parsed nested objects (filter[x] → { filter: { x: val } })
121
+ for (const prefix of ['filter', 'search', 'range']) {
122
+ if (query[prefix] && typeof query[prefix] === 'object') {
123
+ for (const [key, value] of Object.entries(query[prefix])) {
124
+ params[`${prefix}[${key}]`] = value;
125
+ }
126
+ }
127
+ }
128
+
129
+ // Also handle literal bracket keys (e.g. from curl or non-Express clients)
130
+ for (const key of Object.keys(query)) {
131
+ if (key.startsWith('filter[') || key.startsWith('search[') || key.startsWith('range[')) {
132
+ params[key] = query[key];
133
+ }
134
+ }
135
+
136
+ return params;
137
+ }
138
+
139
+ /**
140
+ * @route GET /api/esports/leagues/:leagueIdOrSlug/series
141
+ * @desc List series for a league
142
+ * @access Public
143
+ * @query page, per_page, sort, filter[], search[], range[]
144
+ */
145
+ router.get('/leagues/:leagueIdOrSlug/series', async (req, res) => {
146
+ try {
147
+ const { leagueIdOrSlug } = req.params;
148
+ const params = buildParams(req.query);
149
+
150
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/series`, {
151
+ headers: pandaHeaders(),
152
+ params,
153
+ timeout: 10000
154
+ });
155
+
156
+ res.json({ success: true, data: response.data });
157
+ } catch (error) {
158
+ console.error(`Error fetching series for league ${req.params.leagueIdOrSlug}:`, error.message);
159
+ const status = error.response?.status || 500;
160
+ res.status(status).json({ success: false, error: 'Failed to fetch league series' });
161
+ }
162
+ });
163
+
164
+ /**
165
+ * @route GET /api/esports/leagues/:leagueIdOrSlug/tournaments
166
+ * @desc List tournaments for a league
167
+ * @access Public
168
+ * @query page, per_page, sort, filter[], search[], range[]
169
+ */
170
+ router.get('/leagues/:leagueIdOrSlug/tournaments', async (req, res) => {
171
+ try {
172
+ const { leagueIdOrSlug } = req.params;
173
+ const params = buildParams(req.query);
174
+
175
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/tournaments`, {
176
+ headers: pandaHeaders(),
177
+ params,
178
+ timeout: 10000
179
+ });
180
+
181
+ res.json({ success: true, data: response.data });
182
+ } catch (error) {
183
+ console.error(`Error fetching tournaments for league ${req.params.leagueIdOrSlug}:`, error.message);
184
+ const status = error.response?.status || 500;
185
+ res.status(status).json({ success: false, error: 'Failed to fetch league tournaments' });
186
+ }
187
+ });
188
+
189
+ /**
190
+ * @route GET /api/esports/leagues/:leagueIdOrSlug/matches/upcoming
191
+ * @desc List upcoming matches for a league
192
+ * @access Public
193
+ * @query page, per_page, sort, filter[], search[], range[]
194
+ */
195
+ router.get('/leagues/:leagueIdOrSlug/matches/upcoming', async (req, res) => {
196
+ try {
197
+ const { leagueIdOrSlug } = req.params;
198
+ const params = buildParams(req.query);
199
+
200
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/matches/upcoming`, {
201
+ headers: pandaHeaders(),
202
+ params,
203
+ timeout: 10000
204
+ });
205
+
206
+ res.json({ success: true, data: response.data });
207
+ } catch (error) {
208
+ console.error(`Error fetching upcoming matches for league ${req.params.leagueIdOrSlug}:`, error.message);
209
+ const status = error.response?.status || 500;
210
+ res.status(status).json({ success: false, error: 'Failed to fetch upcoming matches' });
211
+ }
212
+ });
213
+
214
+ /**
215
+ * @route GET /api/esports/leagues/:leagueIdOrSlug/matches/running
216
+ * @desc List currently running (live) matches for a league
217
+ * @access Public
218
+ * @query page, per_page, sort, filter[], search[], range[]
219
+ */
220
+ router.get('/leagues/:leagueIdOrSlug/matches/running', async (req, res) => {
221
+ try {
222
+ const { leagueIdOrSlug } = req.params;
223
+ const params = buildParams(req.query);
224
+
225
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/matches/running`, {
226
+ headers: pandaHeaders(),
227
+ params,
228
+ timeout: 10000
229
+ });
230
+
231
+ res.json({ success: true, data: response.data });
232
+ } catch (error) {
233
+ console.error(`Error fetching running matches for league ${req.params.leagueIdOrSlug}:`, error.message);
234
+ const status = error.response?.status || 500;
235
+ res.status(status).json({ success: false, error: 'Failed to fetch running matches' });
236
+ }
237
+ });
238
+
239
+ /**
240
+ * @route GET /api/esports/leagues/:leagueIdOrSlug/matches
241
+ * @desc List ALL matches for a league (with rich filtering)
242
+ * @access Public
243
+ * @query page, per_page, sort, filter[status], filter[opponent_id], filter[future],
244
+ * filter[finished], filter[running], filter[not_started], filter[past],
245
+ * filter[tournament_id], filter[serie_id], filter[match_type],
246
+ * filter[opponents_filled], range[begin_at], search[name]
247
+ */
248
+ router.get('/leagues/:leagueIdOrSlug/matches', async (req, res) => {
249
+ try {
250
+ const { leagueIdOrSlug } = req.params;
251
+ const params = buildParams(req.query);
252
+
253
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/leagues/${leagueIdOrSlug}/matches`, {
254
+ headers: pandaHeaders(),
255
+ params,
256
+ timeout: 10000
257
+ });
258
+
259
+ res.json({ success: true, data: response.data });
260
+ } catch (error) {
261
+ console.error(`Error fetching matches for league ${req.params.leagueIdOrSlug}:`, error.message);
262
+ const status = error.response?.status || 500;
263
+ res.status(status).json({ success: false, error: 'Failed to fetch league matches' });
264
+ }
265
+ });
266
+
267
+ // ============================================
268
+ // LIVES (real-time match data with WebSocket URLs)
269
+ // ============================================
270
+
271
+ /**
272
+ * @route GET /api/esports/lives
273
+ * @desc Get all currently live matches with WebSocket endpoint URLs
274
+ * @access Public
275
+ * @query page, per_page, sort, filter[], search[], range[]
276
+ */
277
+ router.get('/lives', async (req, res) => {
278
+ try {
279
+ const params = buildParams(req.query);
280
+
281
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/lives`, {
282
+ headers: pandaHeaders(),
283
+ params,
284
+ timeout: 10000
285
+ });
286
+
287
+ res.json({ success: true, data: response.data });
288
+ } catch (error) {
289
+ console.error('Error fetching live matches:', error.message);
290
+ const status = error.response?.status || 500;
291
+ res.status(status).json({ success: false, error: 'Failed to fetch live matches' });
292
+ }
293
+ });
294
+
295
+ // ============================================
296
+ // VIDEOGAME-SPECIFIC ENDPOINTS
297
+ // ============================================
298
+
299
+ /**
300
+ * Map PandaScore videogame slugs to their API path segments.
301
+ * The slug in data responses (e.g. "cs-go") often differs from
302
+ * the API URL path (e.g. "csgo").
303
+ */
304
+ function apiSlug(videogameSlug) {
305
+ const map = {
306
+ 'cs-go': 'csgo',
307
+ 'dota-2': 'dota2',
308
+ 'cod-mw': 'codmw',
309
+ 'league-of-legends': 'lol',
310
+ 'r6-siege': 'r6siege',
311
+ };
312
+ return map[videogameSlug] || videogameSlug;
313
+ }
314
+
315
+ /**
316
+ * @route GET /api/esports/:videogameSlug/tournaments
317
+ * @desc List tournaments for a specific videogame (e.g. cod-mw, valorant, cs-go)
318
+ * @access Public
319
+ * @query page, per_page, sort, filter[], search[], range[]
320
+ */
321
+ router.get('/:videogameSlug/tournaments', async (req, res) => {
322
+ try {
323
+ const { videogameSlug } = req.params;
324
+ const params = buildParams(req.query);
325
+
326
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/tournaments`, {
327
+ headers: pandaHeaders(),
328
+ params,
329
+ timeout: 10000
330
+ });
331
+
332
+ res.json({ success: true, data: response.data });
333
+ } catch (error) {
334
+ console.error(`Error fetching tournaments for ${req.params.videogameSlug}:`, error.message);
335
+ const status = error.response?.status || 500;
336
+ res.status(status).json({ success: false, error: `Failed to fetch ${req.params.videogameSlug} tournaments` });
337
+ }
338
+ });
339
+
340
+ /**
341
+ * @route GET /api/esports/:videogameSlug/tournaments/running
342
+ * @desc List currently running tournaments for a videogame
343
+ * @access Public
344
+ * @query page, per_page, sort, filter[], search[], range[]
345
+ */
346
+ router.get('/:videogameSlug/tournaments/running', async (req, res) => {
347
+ try {
348
+ const { videogameSlug } = req.params;
349
+ const params = buildParams(req.query);
350
+
351
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/tournaments/running`, {
352
+ headers: pandaHeaders(),
353
+ params,
354
+ timeout: 10000
355
+ });
356
+
357
+ res.json({ success: true, data: response.data });
358
+ } catch (error) {
359
+ console.error(`Error fetching running tournaments for ${req.params.videogameSlug}:`, error.message);
360
+ const status = error.response?.status || 500;
361
+ res.status(status).json({ success: false, error: `Failed to fetch running ${req.params.videogameSlug} tournaments` });
362
+ }
363
+ });
364
+
365
+ /**
366
+ * @route GET /api/esports/:videogameSlug/tournaments/upcoming
367
+ * @desc List upcoming tournaments for a videogame
368
+ * @access Public
369
+ * @query page, per_page, sort, filter[], search[], range[]
370
+ */
371
+ router.get('/:videogameSlug/tournaments/upcoming', async (req, res) => {
372
+ try {
373
+ const { videogameSlug } = req.params;
374
+ const params = buildParams(req.query);
375
+
376
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/tournaments/upcoming`, {
377
+ headers: pandaHeaders(),
378
+ params,
379
+ timeout: 10000
380
+ });
381
+
382
+ res.json({ success: true, data: response.data });
383
+ } catch (error) {
384
+ console.error(`Error fetching upcoming tournaments for ${req.params.videogameSlug}:`, error.message);
385
+ const status = error.response?.status || 500;
386
+ res.status(status).json({ success: false, error: `Failed to fetch upcoming ${req.params.videogameSlug} tournaments` });
387
+ }
388
+ });
389
+
390
+ /**
391
+ * @route GET /api/esports/tournaments/:tournamentId/matches
392
+ * @desc Get full matches (with opponents, results, games) for a tournament
393
+ * @access Public
394
+ * @query page, per_page, sort, filter[], search[], range[]
395
+ */
396
+ router.get('/tournaments/:tournamentId/matches', async (req, res) => {
397
+ try {
398
+ const { tournamentId } = req.params;
399
+ const params = buildParams(req.query);
400
+
401
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/tournaments/${tournamentId}/matches`, {
402
+ headers: pandaHeaders(),
403
+ params,
404
+ timeout: 10000
405
+ });
406
+
407
+ res.json({ success: true, data: response.data });
408
+ } catch (error) {
409
+ console.error(`Error fetching matches for tournament ${req.params.tournamentId}:`, error.message);
410
+ const status = error.response?.status || 500;
411
+ res.status(status).json({ success: false, error: `Failed to fetch tournament matches` });
412
+ }
413
+ });
414
+
415
+ /**
416
+ * @route GET /api/esports/:videogameSlug/matches
417
+ * @desc List matches for a specific videogame
418
+ * @access Public
419
+ * @query page, per_page, sort, filter[], search[], range[]
420
+ */
421
+ router.get('/:videogameSlug/matches', async (req, res) => {
422
+ try {
423
+ const { videogameSlug } = req.params;
424
+ const params = buildParams(req.query);
425
+
426
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches`, {
427
+ headers: pandaHeaders(),
428
+ params,
429
+ timeout: 10000
430
+ });
431
+
432
+ res.json({ success: true, data: response.data });
433
+ } catch (error) {
434
+ console.error(`Error fetching matches for ${req.params.videogameSlug}:`, error.message);
435
+ const status = error.response?.status || 500;
436
+ res.status(status).json({ success: false, error: `Failed to fetch ${req.params.videogameSlug} matches` });
437
+ }
438
+ });
439
+
440
+ /**
441
+ * @route GET /api/esports/:videogameSlug/matches/running
442
+ * @desc List currently running matches for a videogame
443
+ * @access Public
444
+ * @query page, per_page, sort, filter[], search[], range[]
445
+ */
446
+ router.get('/:videogameSlug/matches/running', async (req, res) => {
447
+ try {
448
+ const { videogameSlug } = req.params;
449
+ const params = buildParams(req.query);
450
+
451
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches/running`, {
452
+ headers: pandaHeaders(),
453
+ params,
454
+ timeout: 10000
455
+ });
456
+
457
+ res.json({ success: true, data: response.data });
458
+ } catch (error) {
459
+ console.error(`Error fetching running matches for ${req.params.videogameSlug}:`, error.message);
460
+ const status = error.response?.status || 500;
461
+ res.status(status).json({ success: false, error: `Failed to fetch running ${req.params.videogameSlug} matches` });
462
+ }
463
+ });
464
+
465
+ /**
466
+ * @route GET /api/esports/:videogameSlug/matches/upcoming
467
+ * @desc List upcoming matches for a videogame
468
+ * @access Public
469
+ * @query page, per_page, sort, filter[], search[], range[]
470
+ */
471
+ router.get('/:videogameSlug/matches/upcoming', async (req, res) => {
472
+ try {
473
+ const { videogameSlug } = req.params;
474
+ const params = buildParams(req.query);
475
+
476
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches/upcoming`, {
477
+ headers: pandaHeaders(),
478
+ params,
479
+ timeout: 10000
480
+ });
481
+
482
+ res.json({ success: true, data: response.data });
483
+ } catch (error) {
484
+ console.error(`Error fetching upcoming matches for ${req.params.videogameSlug}:`, error.message);
485
+ const status = error.response?.status || 500;
486
+ res.status(status).json({ success: false, error: `Failed to fetch upcoming ${req.params.videogameSlug} matches` });
487
+ }
488
+ });
489
+
490
+ // ============================================
491
+ // SINGLE MATCH BY ID
492
+ // ============================================
493
+
494
+ /**
495
+ * @route GET /api/esports/matches/:matchId
496
+ * @desc Get a single match by PandaScore match ID (game-agnostic)
497
+ * @access Public
498
+ */
499
+ router.get('/matches/:matchId', async (req, res) => {
500
+ try {
501
+ const { matchId } = req.params;
502
+
503
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/matches/${matchId}`, {
504
+ headers: pandaHeaders(),
505
+ timeout: 10000
506
+ });
507
+
508
+ res.json({ success: true, data: response.data });
509
+ } catch (error) {
510
+ console.error(`Error fetching match ${req.params.matchId}:`, error.message);
511
+ const status = error.response?.status || 500;
512
+ res.status(status).json({ success: false, error: 'Failed to fetch match' });
513
+ }
514
+ });
515
+
516
+ /**
517
+ * @route GET /api/esports/:videogameSlug/matches/:matchId
518
+ * @desc Get a single match by PandaScore match ID for a specific videogame
519
+ * @access Public
520
+ */
521
+ router.get('/:videogameSlug/matches/:matchId', async (req, res) => {
522
+ try {
523
+ const { videogameSlug, matchId } = req.params;
524
+
525
+ const response = await axios.get(`${PANDASCORE_BASE_URL}/${apiSlug(videogameSlug)}/matches/${matchId}`, {
526
+ headers: pandaHeaders(),
527
+ timeout: 10000
528
+ });
529
+
530
+ res.json({ success: true, data: response.data });
531
+ } catch (error) {
532
+ console.error(`Error fetching match ${req.params.matchId} for ${req.params.videogameSlug}:`, error.message);
533
+ const status = error.response?.status || 500;
534
+ res.status(status).json({ success: false, error: `Failed to fetch ${req.params.videogameSlug} match` });
535
+ }
536
+ });
537
+
538
+ // ============================================
539
+ // ESPORTS GAME CREATION
540
+ // ============================================
541
+
542
+ /**
543
+ * @route POST /api/esports/games/create
544
+ * @desc Validate a PandaScore match and return enriched data for game creation.
545
+ * The frontend uses this data to build the on-chain transaction, then
546
+ * calls the existing /api/auth/games/save endpoint to persist it.
547
+ * @access Public
548
+ * @body { pandascoreMatchId: number, videogameSlug: string }
549
+ */
550
+ router.post('/games/validate', async (req, res) => {
551
+ try {
552
+ const { pandascoreMatchId, videogameSlug } = req.body;
553
+
554
+ if (!pandascoreMatchId) {
555
+ return res.status(400).json({
556
+ success: false,
557
+ error: 'pandascoreMatchId is required'
558
+ });
559
+ }
560
+
561
+ // Fetch full match data from PandaScore
562
+ const matchResponse = await axios.get(`${PANDASCORE_BASE_URL}/matches/${pandascoreMatchId}`, {
563
+ headers: pandaHeaders(),
564
+ timeout: 10000
565
+ });
566
+
567
+ const match = matchResponse.data;
568
+ if (!match) {
569
+ return res.status(404).json({ success: false, error: 'Match not found on PandaScore' });
570
+ }
571
+
572
+ // Validate: must be not_started or running (live betting allowed)
573
+ const isLive = match.status === 'running';
574
+ if (match.status !== 'not_started' && !isLive) {
575
+ return res.status(400).json({
576
+ success: false,
577
+ error: `Match status is "${match.status}" — can only bet on matches that haven't started or are currently live`,
578
+ matchStatus: match.status
579
+ });
580
+ }
581
+
582
+ // Validate: must have 2 opponents
583
+ if (!match.opponents || match.opponents.length < 2) {
584
+ return res.status(400).json({
585
+ success: false,
586
+ error: 'Match does not have 2 confirmed opponents yet'
587
+ });
588
+ }
589
+
590
+ // For upcoming matches: scheduled_at must be > now + 2 minutes (Solana constraint)
591
+ // For live matches: skip this check — lock is set to now + 5 minutes instead
592
+ const scheduledAt = new Date(match.scheduled_at || match.begin_at);
593
+ if (!isLive) {
594
+ const minTime = new Date(Date.now() + 2 * 60 * 1000);
595
+ if (scheduledAt <= minTime) {
596
+ return res.status(400).json({
597
+ success: false,
598
+ error: 'Match starts too soon — must be at least 2 minutes from now'
599
+ });
600
+ }
601
+ }
602
+
603
+ // Build the sportsEvent JSONB that will be stored alongside the game
604
+ const opp0 = match.opponents[0];
605
+ const opp1 = match.opponents[1];
606
+ const sportsEvent = {
607
+ pandascoreMatchId: match.id,
608
+ videogameSlug: match.videogame?.slug || videogameSlug || 'unknown',
609
+ videogame: match.videogame?.name || videogameSlug || 'Unknown',
610
+ matchName: match.name || `${opp0.opponent?.name} vs ${opp1.opponent?.name}`,
611
+ tournament: match.tournament?.name || '',
612
+ tournamentId: match.tournament?.id || null,
613
+ serie: match.serie?.full_name || match.serie?.name || '',
614
+ tier: match.tournament?.tier || match.serie?.tier || null,
615
+ matchType: match.match_type || 'best_of',
616
+ numberOfGames: match.number_of_games || 1,
617
+ scheduledAt: match.scheduled_at || match.begin_at,
618
+ // strTimestamp is used by the oracle for lock timing — reuse same convention
619
+ // For live matches, use the lock time (now + 5 min) so countdown shows correctly
620
+ strTimestamp: isLive
621
+ ? new Date(Date.now() + 5 * 60 * 1000).toISOString().replace('Z', '').split('.')[0]
622
+ : (match.scheduled_at || match.begin_at || '').replace('Z', '').split('.')[0],
623
+ opponents: match.opponents,
624
+ streamsList: match.streams_list || [],
625
+ leagueName: match.league?.name || '',
626
+ leagueId: match.league?.id || null,
627
+ // TheSportsDB-compatible fields so existing notification code works
628
+ strEvent: match.name || `${opp0.opponent?.name} vs ${opp1.opponent?.name}`,
629
+ strHomeTeam: opp0.opponent?.name,
630
+ strAwayTeam: opp1.opponent?.name,
631
+ strHomeTeamBadge: opp0.opponent?.image_url,
632
+ strAwayTeamBadge: opp1.opponent?.image_url,
633
+ strLeague: match.videogame?.name || videogameSlug,
634
+ };
635
+
636
+ // Calculate lock_timestamp (Unix seconds)
637
+ // For live matches: lock 5 minutes from now (gives time for opponent to join)
638
+ // For upcoming matches: lock at scheduled start time
639
+ const lockTimestamp = isLive
640
+ ? Math.floor((Date.now() + 5 * 60 * 1000) / 1000)
641
+ : Math.floor(scheduledAt.getTime() / 1000);
642
+
643
+ res.json({
644
+ success: true,
645
+ match: {
646
+ id: match.id,
647
+ name: sportsEvent.matchName,
648
+ status: match.status,
649
+ scheduledAt: sportsEvent.scheduledAt,
650
+ lockTimestamp,
651
+ tournament: sportsEvent.tournament,
652
+ tier: sportsEvent.tier,
653
+ matchType: sportsEvent.matchType,
654
+ numberOfGames: sportsEvent.numberOfGames,
655
+ opponents: match.opponents.map(o => ({
656
+ id: o.opponent?.id,
657
+ name: o.opponent?.name,
658
+ acronym: o.opponent?.acronym,
659
+ imageUrl: o.opponent?.image_url,
660
+ })),
661
+ streams: sportsEvent.streamsList,
662
+ },
663
+ // Pre-built sportsEvent for the frontend to pass to /api/auth/games/save
664
+ sportsEvent,
665
+ // gameMode 5 = esports
666
+ gameMode: 5,
667
+ });
668
+
669
+ } catch (error) {
670
+ console.error('Error validating esports match:', error.message);
671
+ const status = error.response?.status || 500;
672
+ res.status(status).json({ success: false, error: 'Failed to validate esports match' });
673
+ }
674
+ });
675
+
676
+ /**
677
+ * @route GET /api/esports/games/upcoming
678
+ * @desc Get bettable upcoming esports matches (CS + Valorant, S/A tier, not_started)
679
+ * @access Public
680
+ * @query videogame (cs-go|valorant), page, per_page
681
+ */
682
+ router.get('/games/upcoming', async (req, res) => {
683
+ try {
684
+ const videogames = req.query.videogame
685
+ ? [req.query.videogame]
686
+ : ['cs-go', 'valorant'];
687
+
688
+ const page = req.query.page || 1;
689
+ const perPage = req.query.per_page || 20;
690
+
691
+ const allMatches = [];
692
+
693
+ for (const vg of videogames) {
694
+ try {
695
+ const response = await axios.get(
696
+ `${PANDASCORE_BASE_URL}/${apiSlug(vg)}/matches/upcoming`,
697
+ {
698
+ headers: pandaHeaders(),
699
+ params: {
700
+ page,
701
+ per_page: perPage,
702
+ sort: 'scheduled_at',
703
+ 'filter[opponents_filled]': true,
704
+ },
705
+ timeout: 10000
706
+ }
707
+ );
708
+
709
+ const matches = (response.data || [])
710
+ .filter(m => {
711
+ // Allow S, A, B, C, D tier tournaments
712
+ const tier = m.tournament?.tier || m.serie?.tier;
713
+ return ['s', 'a', 'b', 'c', 'd'].includes(tier);
714
+ })
715
+ .filter(m => {
716
+ // Must start > 2 min from now
717
+ const scheduled = new Date(m.scheduled_at || m.begin_at);
718
+ return scheduled.getTime() > Date.now() + 2 * 60 * 1000;
719
+ })
720
+ .map(m => ({
721
+ id: m.id,
722
+ name: m.name,
723
+ videogame: vg,
724
+ videogameName: m.videogame?.name || vg,
725
+ scheduledAt: m.scheduled_at || m.begin_at,
726
+ matchType: m.match_type,
727
+ numberOfGames: m.number_of_games,
728
+ tournament: m.tournament?.name,
729
+ tier: m.tournament?.tier || m.serie?.tier,
730
+ league: m.league?.name,
731
+ opponents: (m.opponents || []).map(o => ({
732
+ id: o.opponent?.id,
733
+ name: o.opponent?.name,
734
+ acronym: o.opponent?.acronym,
735
+ imageUrl: o.opponent?.image_url,
736
+ })),
737
+ streams: m.streams_list || [],
738
+ }));
739
+
740
+ allMatches.push(...matches);
741
+ } catch (err) {
742
+ console.error(`Error fetching upcoming ${vg} matches:`, err.message);
743
+ }
744
+ }
745
+
746
+ // Sort by scheduled time
747
+ allMatches.sort((a, b) => new Date(a.scheduledAt) - new Date(b.scheduledAt));
748
+
749
+ res.json({ success: true, data: allMatches });
750
+ } catch (error) {
751
+ console.error('Error fetching upcoming esports games:', error.message);
752
+ res.status(500).json({ success: false, error: 'Failed to fetch upcoming esports games' });
753
+ }
754
+ });
755
+
756
+ /**
757
+ * @route GET /api/esports/games/pending
758
+ * @desc Get esports games that are in our DB (game_mode=5, not resolved)
759
+ * @access Public
760
+ */
761
+ router.get('/games/pending', async (req, res) => {
762
+ try {
763
+ const result = await pool.query(`
764
+ SELECT * FROM games
765
+ WHERE game_mode = 5
766
+ AND is_resolved = false
767
+ ORDER BY created_at DESC
768
+ `);
769
+
770
+ const games = result.rows.map(row => {
771
+ const se = row.sports_event || {};
772
+ const opp0 = se.opponents?.[0]?.opponent || {};
773
+ const opp1 = se.opponents?.[1]?.opponent || {};
774
+ return {
775
+ gameId: row.game_id,
776
+ gameAddress: row.game_address,
777
+ title: row.title || se.matchName || `${opp0.name || '?'} vs ${opp1.name || '?'}`,
778
+ gameMode: row.game_mode,
779
+ buyIn: parseFloat(row.buy_in),
780
+ createdBy: row.created_by,
781
+ sportsEvent: se,
782
+ homeTeam: opp0.name,
783
+ awayTeam: opp1.name,
784
+ homeTeamBadge: opp0.image_url,
785
+ awayTeamBadge: opp1.image_url,
786
+ videogame: se.videogame || se.videogameSlug,
787
+ tournament: se.tournament,
788
+ tier: se.tier,
789
+ matchType: se.matchType,
790
+ scheduledAt: se.scheduledAt,
791
+ homeTeamPlayers: row.home_team_players || [],
792
+ awayTeamPlayers: row.away_team_players || [],
793
+ isLocked: row.is_locked,
794
+ isResolved: row.is_resolved,
795
+ createdAt: row.created_at,
796
+ };
797
+ });
798
+
799
+ res.json({ success: true, games });
800
+ } catch (error) {
801
+ console.error('Error fetching pending esports games:', error.message);
802
+ res.status(500).json({ success: false, error: error.message });
803
+ }
804
+ });
805
+
806
+ module.exports = router;