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,370 @@
1
+ /**
2
+ * Admin Routes - Protected endpoints for administrative actions
3
+ * Only accessible to the admin wallet
4
+ */
5
+
6
+ const express = require('express');
7
+ const router = express.Router();
8
+ const { pool } = require('../services/db'); // Shared database pool
9
+
10
+ // Admin wallet - only this address can use admin endpoints
11
+ const ADMIN_WALLET = 'Hvv1ctqHLR5wonuuRguefS6EpGUe7tFRBX2YWHGr3mes';
12
+
13
+ /**
14
+ * Check if a wallet address is an admin
15
+ */
16
+ function isAdmin(walletAddress) {
17
+ return walletAddress === ADMIN_WALLET;
18
+ }
19
+
20
+ /**
21
+ * Middleware to verify admin wallet from request
22
+ * Expects wallet address in Authorization header or query param
23
+ */
24
+ function requireAdmin(req, res, next) {
25
+ const walletAddress = req.headers['x-wallet-address'] || req.query.wallet;
26
+
27
+ if (!walletAddress) {
28
+ return res.status(401).json({
29
+ success: false,
30
+ error: 'Wallet address required'
31
+ });
32
+ }
33
+
34
+ if (!isAdmin(walletAddress)) {
35
+ return res.status(403).json({
36
+ success: false,
37
+ error: 'Admin access required'
38
+ });
39
+ }
40
+
41
+ req.adminWallet = walletAddress;
42
+ next();
43
+ }
44
+
45
+ /**
46
+ * GET /api/admin/games/pending
47
+ * Get all unresolved automatic games for admin review
48
+ */
49
+ router.get('/games/pending', requireAdmin, async (req, res) => {
50
+ try {
51
+ const result = await pool.query(`
52
+ SELECT
53
+ g.game_id,
54
+ g.buy_in,
55
+ g.game_mode,
56
+ g.is_locked,
57
+ g.is_resolved,
58
+ g.automatic_status,
59
+ g.sports_event,
60
+ g.created_at,
61
+ g.updated_at,
62
+ g.matchup_image_url,
63
+ (SELECT COUNT(*) FROM user_game_refs ugr WHERE ugr.game_id = g.game_id AND ugr.team_choice = 'home') as home_count,
64
+ (SELECT COUNT(*) FROM user_game_refs ugr WHERE ugr.game_id = g.game_id AND ugr.team_choice = 'away') as away_count,
65
+ (SELECT COUNT(*) FROM user_game_refs ugr WHERE ugr.game_id = g.game_id AND ugr.team_choice = 'draw') as draw_count
66
+ FROM games g
67
+ WHERE g.game_mode = 4
68
+ AND g.is_resolved = false
69
+ ORDER BY g.created_at DESC
70
+ `);
71
+
72
+ const games = result.rows.map(row => ({
73
+ gameId: row.game_id,
74
+ buyIn: parseFloat(row.buy_in),
75
+ gameMode: row.game_mode,
76
+ isLocked: row.is_locked,
77
+ isResolved: row.is_resolved,
78
+ automaticStatus: row.automatic_status,
79
+ sportsEvent: row.sports_event,
80
+ matchupImageUrl: row.matchup_image_url,
81
+ createdAt: row.created_at,
82
+ updatedAt: row.updated_at,
83
+ homeCount: parseInt(row.home_count),
84
+ awayCount: parseInt(row.away_count),
85
+ drawCount: parseInt(row.draw_count),
86
+ totalPlayers: parseInt(row.home_count) + parseInt(row.away_count) + parseInt(row.draw_count)
87
+ }));
88
+
89
+ console.log(`[Admin] Found ${games.length} pending games`);
90
+
91
+ res.json({
92
+ success: true,
93
+ games
94
+ });
95
+
96
+ } catch (error) {
97
+ console.error('[Admin] Error fetching pending games:', error);
98
+ res.status(500).json({
99
+ success: false,
100
+ error: 'Failed to fetch pending games'
101
+ });
102
+ }
103
+ });
104
+
105
+ /**
106
+ * POST /api/admin/games/:gameId/resolve
107
+ * Manually resolve a game (admin escape hatch)
108
+ * This updates the database only - on-chain resolution should be handled separately
109
+ */
110
+ router.post('/games/:gameId/resolve', requireAdmin, async (req, res) => {
111
+ try {
112
+ const { gameId } = req.params;
113
+ const { winner, homeScore, awayScore } = req.body;
114
+
115
+ console.log(`[Admin] Manual resolution requested for game: ${gameId}`);
116
+ console.log(`[Admin] Winner: ${winner}, Score: ${homeScore}-${awayScore}`);
117
+ console.log(`[Admin] Requested by: ${req.adminWallet}`);
118
+
119
+ // Validate winner
120
+ if (!['home', 'away', 'draw', null].includes(winner)) {
121
+ return res.status(400).json({
122
+ success: false,
123
+ error: 'Invalid winner. Must be "home", "away", "draw", or null (for refund)'
124
+ });
125
+ }
126
+
127
+ // Check if game exists and is not already resolved
128
+ const checkResult = await pool.query(`
129
+ SELECT game_id, is_resolved, automatic_status, sports_event
130
+ FROM games WHERE game_id = $1
131
+ `, [gameId]);
132
+
133
+ if (checkResult.rows.length === 0) {
134
+ return res.status(404).json({
135
+ success: false,
136
+ error: 'Game not found'
137
+ });
138
+ }
139
+
140
+ if (checkResult.rows[0].is_resolved) {
141
+ return res.status(400).json({
142
+ success: false,
143
+ error: 'Game is already resolved'
144
+ });
145
+ }
146
+
147
+ // Update game status in PostgreSQL
148
+ const result = await pool.query(`
149
+ UPDATE games
150
+ SET
151
+ is_resolved = true,
152
+ automatic_status = 'resolved',
153
+ sports_event = COALESCE(sports_event, '{}'::jsonb) || $1::jsonb,
154
+ updated_at = NOW()
155
+ WHERE game_id = $2
156
+ RETURNING *
157
+ `, [
158
+ JSON.stringify({
159
+ finalScore: {
160
+ winner,
161
+ homeScore: homeScore || 0,
162
+ awayScore: awayScore || 0,
163
+ resolvedAt: new Date().toISOString(),
164
+ resolvedBy: 'admin_manual'
165
+ }
166
+ }),
167
+ gameId
168
+ ]);
169
+
170
+ if (result.rows.length === 0) {
171
+ return res.status(404).json({
172
+ success: false,
173
+ error: 'Failed to update game'
174
+ });
175
+ }
176
+
177
+ console.log(`[Admin] Successfully resolved game ${gameId} - Winner: ${winner}`);
178
+
179
+ const resolvedGame = result.rows[0];
180
+ const sportsEvent = resolvedGame.sports_event;
181
+
182
+ // Broadcast game state update to all clients via global chatNamespace
183
+ const chatNamespace = global.chatNamespace;
184
+ if (chatNamespace) {
185
+ chatNamespace.emit('game:stateUpdate', {
186
+ gameId,
187
+ isResolved: true,
188
+ winner,
189
+ homeScore: homeScore || 0,
190
+ awayScore: awayScore || 0,
191
+ resolvedBy: 'admin_manual',
192
+ timestamp: Date.now()
193
+ });
194
+ console.log(`[Admin] Broadcasted game:stateUpdate for ${gameId}`);
195
+ }
196
+
197
+ // Send notifications to all participants (won/lost/refund)
198
+ try {
199
+ console.log(`[Admin] Sending notifications to participants...`);
200
+
201
+ // Get all participants
202
+ const participantsResult = await pool.query(`
203
+ SELECT wallet_address, team_choice
204
+ FROM user_game_refs
205
+ WHERE game_id = $1
206
+ `, [gameId]);
207
+
208
+ const participants = participantsResult.rows;
209
+ console.log(`[Admin] Found ${participants.length} participant(s) to notify`);
210
+
211
+ // Build gameInvite metadata for notifications
212
+ const gameInvite = {
213
+ gameId: gameId,
214
+ gameAddress: resolvedGame.game_address || '',
215
+ title: resolvedGame.title,
216
+ imageUrl: resolvedGame.image_url,
217
+ matchupImageUrl: resolvedGame.matchup_image_url,
218
+ buyIn: parseFloat(resolvedGame.buy_in),
219
+ homeTeam: sportsEvent?.strHomeTeam,
220
+ awayTeam: sportsEvent?.strAwayTeam,
221
+ homeTeamBadge: sportsEvent?.strHomeTeamBadge,
222
+ awayTeamBadge: sportsEvent?.strAwayTeamBadge,
223
+ league: sportsEvent?.strLeague,
224
+ strTimestamp: sportsEvent?.strTimestamp,
225
+ };
226
+
227
+ const finalScore = {
228
+ winner,
229
+ homeScore: homeScore || 0,
230
+ awayScore: awayScore || 0,
231
+ };
232
+
233
+ // Send notification to each participant
234
+ for (const participant of participants) {
235
+ try {
236
+ const isRefund = winner === null;
237
+ const userWon = !isRefund && participant.team_choice === winner;
238
+ const notificationType = isRefund ? 'game_refund' : (userWon ? 'game_won' : 'game_lost');
239
+ const message = `${homeScore || 0}-${awayScore || 0}`;
240
+
241
+ // Insert notification into database
242
+ await pool.query(`
243
+ INSERT INTO chat_notifications (
244
+ wallet_address,
245
+ notification_type,
246
+ message,
247
+ game_invite,
248
+ final_score,
249
+ is_read,
250
+ created_at
251
+ ) VALUES ($1, $2, $3, $4, $5, false, NOW())
252
+ `, [
253
+ participant.wallet_address,
254
+ notificationType,
255
+ message,
256
+ JSON.stringify(gameInvite),
257
+ JSON.stringify(finalScore)
258
+ ]);
259
+
260
+ // Emit WebSocket notification to the specific user
261
+ if (chatNamespace) {
262
+ chatNamespace.to(`user:${participant.wallet_address}`).emit('notification', {
263
+ type: notificationType,
264
+ message,
265
+ gameInvite,
266
+ finalScore,
267
+ timestamp: Date.now()
268
+ });
269
+ }
270
+
271
+ console.log(`[Admin] Notified ${participant.wallet_address}: ${notificationType}`);
272
+ } catch (notifyError) {
273
+ console.error(`[Admin] Failed to notify ${participant.wallet_address}:`, notifyError.message);
274
+ }
275
+ }
276
+
277
+ console.log(`[Admin] Sent notifications to ${participants.length} participant(s)`);
278
+ } catch (notificationError) {
279
+ console.error('[Admin] Error sending notifications:', notificationError.message);
280
+ // Don't fail the request - notifications are non-critical
281
+ }
282
+
283
+ res.json({
284
+ success: true,
285
+ message: `Game ${gameId} resolved successfully`,
286
+ game: {
287
+ gameId,
288
+ winner,
289
+ homeScore: homeScore || 0,
290
+ awayScore: awayScore || 0,
291
+ resolvedAt: new Date().toISOString()
292
+ }
293
+ });
294
+
295
+ } catch (error) {
296
+ console.error('[Admin] Error resolving game:', error);
297
+ res.status(500).json({
298
+ success: false,
299
+ error: 'Failed to resolve game'
300
+ });
301
+ }
302
+ });
303
+
304
+ /**
305
+ * DELETE /api/admin/games/:gameId
306
+ * Delete a game (admin only - for cleaning up test/duplicate games)
307
+ */
308
+ router.delete('/games/:gameId', requireAdmin, async (req, res) => {
309
+ try {
310
+ const { gameId } = req.params;
311
+
312
+ console.log(`[Admin] Delete requested for game: ${gameId}`);
313
+ console.log(`[Admin] Requested by: ${req.adminWallet}`);
314
+
315
+ // First delete user_game_refs (foreign key constraint)
316
+ await pool.query(`DELETE FROM user_game_refs WHERE game_id = $1`, [gameId]);
317
+
318
+ // Then delete the game
319
+ const result = await pool.query(`
320
+ DELETE FROM games WHERE game_id = $1 RETURNING game_id
321
+ `, [gameId]);
322
+
323
+ if (result.rows.length === 0) {
324
+ return res.status(404).json({
325
+ success: false,
326
+ error: 'Game not found'
327
+ });
328
+ }
329
+
330
+ console.log(`[Admin] Successfully deleted game ${gameId}`);
331
+
332
+ res.json({
333
+ success: true,
334
+ message: `Game ${gameId} deleted successfully`
335
+ });
336
+
337
+ } catch (error) {
338
+ console.error('[Admin] Error deleting game:', error);
339
+ res.status(500).json({
340
+ success: false,
341
+ error: 'Failed to delete game'
342
+ });
343
+ }
344
+ });
345
+
346
+ /**
347
+ * POST /api/admin/cache/flush-games
348
+ * Flush all games cache from Redis. Safe for production — requests fall through
349
+ * to PostgreSQL and re-warm automatically.
350
+ */
351
+ router.post('/cache/flush-games', requireAdmin, async (req, res) => {
352
+ try {
353
+ const gamesCacheService = require('../services/gamesCacheService');
354
+ console.log(`[Admin] Games cache flush requested by ${req.adminWallet}`);
355
+
356
+ const result = await gamesCacheService.flushAllGamesCaches();
357
+
358
+ console.log(`[Admin] Games cache flushed: ${result.deletedKeys} keys deleted`);
359
+ res.json({
360
+ success: true,
361
+ message: `Flushed ${result.deletedKeys} games cache keys`,
362
+ deletedKeys: result.deletedKeys,
363
+ });
364
+ } catch (error) {
365
+ console.error('[Admin] Error flushing games cache:', error);
366
+ res.status(500).json({ success: false, error: error.message });
367
+ }
368
+ });
369
+
370
+ module.exports = router;