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,199 @@
1
+ /**
2
+ * Pick'em Admin Routes - Simulation endpoints for testing fight cards
3
+ * Requires admin wallet in x-wallet-address header
4
+ * Emits Socket.IO events so connected clients react in real-time
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const { pool } = require('../services/db');
10
+ const pickemController = require('../controllers/pickemController');
11
+ const expoPushService = require('../services/expoPushService');
12
+
13
+ const ADMIN_WALLET = 'Hvv1ctqHLR5wonuuRguefS6EpGUe7tFRBX2YWHGr3mes';
14
+
15
+ let io = null;
16
+ router.setSocketIO = (ioInstance) => {
17
+ io = ioInstance;
18
+ console.log('[PickemAdmin] Socket.IO injected');
19
+ };
20
+
21
+ function emit(event, data) {
22
+ if (io) io.emit(event, data);
23
+ }
24
+
25
+ function requireAdmin(req, res, next) {
26
+ const walletAddress = req.headers['x-wallet-address'] || req.query.wallet;
27
+ if (!walletAddress || walletAddress !== ADMIN_WALLET) {
28
+ return res.status(403).json({ success: false, error: 'Admin access required' });
29
+ }
30
+ req.adminWallet = walletAddress;
31
+ next();
32
+ }
33
+
34
+ router.use(requireAdmin);
35
+
36
+ /**
37
+ * POST /api/pickem/admin/fights/:fightId/update
38
+ * Update a fight's status, winner, and method. Recomputes scores if fight is resolved.
39
+ */
40
+ router.post('/fights/:fightId/update', async (req, res) => {
41
+ try {
42
+ const fightId = parseInt(req.params.fightId);
43
+ const { status, winner, method } = req.body;
44
+
45
+ if (!status) {
46
+ return res.status(400).json({ success: false, error: 'status is required' });
47
+ }
48
+
49
+ console.log(`[PickemAdmin] Updating fight ${fightId}: status=${status}, winner=${winner || 'none'}, method=${method || 'none'}`);
50
+
51
+ const fight = await pickemController.updateFightResult(fightId, {
52
+ winner: winner || null,
53
+ method: method || null,
54
+ status,
55
+ });
56
+
57
+ if (!fight) {
58
+ return res.status(404).json({ success: false, error: 'Fight not found' });
59
+ }
60
+
61
+ // Emit fight status change
62
+ if (status === 'live') {
63
+ emit('pickem:fight_live', { poolId: fight.poolId, fight });
64
+ expoPushService.sendFightLiveNotifications(fightId, fight)
65
+ .catch(err => console.error('[PickemAdmin] Push error (fight live):', err.message));
66
+ }
67
+
68
+ let scores = null;
69
+ if (status === 'final' && winner) {
70
+ scores = await pickemController.computeScores(fight.poolId);
71
+ console.log(`[PickemAdmin] Scores recomputed for pool ${fight.poolId}: ${scores.entries.length} entries, max=${scores.maxScore}`);
72
+ emit('pickem:fight_resolved', { poolId: fight.poolId, fight, scores });
73
+ expoPushService.sendFightResolvedNotifications(fightId, fight)
74
+ .catch(err => console.error('[PickemAdmin] Push error (fight resolved):', err.message));
75
+ }
76
+
77
+ // Always emit a generic update so clients know to refetch
78
+ emit('pickem:pool_updated', { poolId: fight.poolId });
79
+
80
+ res.json({ success: true, fight, scores });
81
+ } catch (error) {
82
+ console.error('[PickemAdmin] Error updating fight:', error);
83
+ res.status(500).json({ success: false, error: error.message });
84
+ }
85
+ });
86
+
87
+ /**
88
+ * POST /api/pickem/admin/pools/:poolId/reset-fights
89
+ * Reset all fights to scheduled, clear winners/scores for re-testing
90
+ */
91
+ router.post('/pools/:poolId/reset-fights', async (req, res) => {
92
+ try {
93
+ const poolId = parseInt(req.params.poolId);
94
+ const { resetPoolStatus } = req.body || {};
95
+
96
+ console.log(`[PickemAdmin] Resetting fights for pool ${poolId}`);
97
+
98
+ const fightsResult = await pool.query(
99
+ `UPDATE pickem_fights
100
+ SET status = 'scheduled', winner = NULL, method = NULL, updated_at = NOW()
101
+ WHERE pool_id = $1`,
102
+ [poolId]
103
+ );
104
+
105
+ await pool.query(
106
+ `UPDATE pickem_picks pp
107
+ SET is_correct = NULL, updated_at = NOW()
108
+ FROM pickem_entries pe
109
+ WHERE pp.entry_id = pe.id AND pe.pool_id = $1`,
110
+ [poolId]
111
+ );
112
+
113
+ await pool.query(
114
+ `UPDATE pickem_entries
115
+ SET score = 0, rank = NULL, updated_at = NOW()
116
+ WHERE pool_id = $1`,
117
+ [poolId]
118
+ );
119
+
120
+ if (resetPoolStatus) {
121
+ await pickemController.updatePool(poolId, { status: 'locked' });
122
+ }
123
+
124
+ console.log(`[PickemAdmin] Reset ${fightsResult.rowCount} fights for pool ${poolId}`);
125
+
126
+ emit('pickem:pool_reset', { poolId });
127
+ emit('pickem:pool_updated', { poolId });
128
+
129
+ res.json({
130
+ success: true,
131
+ fightsReset: fightsResult.rowCount,
132
+ poolStatusReset: !!resetPoolStatus,
133
+ });
134
+ } catch (error) {
135
+ console.error('[PickemAdmin] Error resetting fights:', error);
136
+ res.status(500).json({ success: false, error: error.message });
137
+ }
138
+ });
139
+
140
+ /**
141
+ * POST /api/pickem/admin/pools/:poolId/status
142
+ * Change pool status
143
+ */
144
+ router.post('/pools/:poolId/status', async (req, res) => {
145
+ try {
146
+ const poolId = parseInt(req.params.poolId);
147
+ const { status } = req.body;
148
+
149
+ if (!status) {
150
+ return res.status(400).json({ success: false, error: 'status is required' });
151
+ }
152
+
153
+ console.log(`[PickemAdmin] Setting pool ${poolId} status to ${status}`);
154
+
155
+ const updatedPool = await pickemController.updatePool(poolId, { status });
156
+
157
+ if (status === 'locked') emit('pickem:pool_locked', { poolId });
158
+ emit('pickem:pool_updated', { poolId });
159
+
160
+ res.json({ success: true, pool: updatedPool });
161
+ } catch (error) {
162
+ console.error('[PickemAdmin] Error updating pool status:', error);
163
+ res.status(500).json({ success: false, error: error.message });
164
+ }
165
+ });
166
+
167
+ /**
168
+ * POST /api/pickem/admin/pools/:poolId/resolve
169
+ * Trigger full pool resolution: compute final scores + set complete
170
+ */
171
+ router.post('/pools/:poolId/resolve', async (req, res) => {
172
+ try {
173
+ const poolId = parseInt(req.params.poolId);
174
+
175
+ console.log(`[PickemAdmin] Resolving pool ${poolId}`);
176
+
177
+ await pickemController.updatePool(poolId, { status: 'resolving' });
178
+ const scores = await pickemController.computeScores(poolId);
179
+ const updatedPool = await pickemController.updatePool(poolId, { status: 'complete' });
180
+
181
+ console.log(`[PickemAdmin] Pool ${poolId} resolved: ${scores.winnerCount} winner(s) with score ${scores.maxScore}`);
182
+
183
+ emit('pickem:pool_resolved', { poolId, scores });
184
+ emit('pickem:pool_updated', { poolId });
185
+ expoPushService.sendPoolResolvedNotifications(poolId, scores)
186
+ .catch(err => console.error('[PickemAdmin] Push error (pool resolved):', err.message));
187
+
188
+ res.json({
189
+ success: true,
190
+ pool: updatedPool,
191
+ scores,
192
+ });
193
+ } catch (error) {
194
+ console.error('[PickemAdmin] Error resolving pool:', error);
195
+ res.status(500).json({ success: false, error: error.message });
196
+ }
197
+ });
198
+
199
+ module.exports = router;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Pick'em Routes
3
+ * UFC Pick'em pool endpoints
4
+ * Pattern mirrors survivorRoutes.js
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const pickemController = require('../controllers/pickemController');
10
+ const { authenticate } = require('../middleware/authenticate');
11
+
12
+ let io = null;
13
+ router.setSocketIO = (ioInstance) => {
14
+ io = ioInstance;
15
+ console.log('[Pickem] Socket.IO injected');
16
+ };
17
+
18
+ // ========== POOL ENDPOINTS ==========
19
+
20
+ // Create a new pick'em pool (admin)
21
+ router.post('/pools', async (req, res) => {
22
+ try {
23
+ const { name, espnEventId, eventDate, lockTime, solanaGameId, solanaGameAddress } = req.body;
24
+ const createdBy = req.body.createdBy || req.headers['x-wallet-address'] || null;
25
+
26
+ if (!name || !eventDate || !lockTime) {
27
+ return res.status(400).json({ success: false, error: 'name, eventDate, and lockTime are required' });
28
+ }
29
+
30
+ const pool = await pickemController.createPool({
31
+ name, espnEventId, eventDate, lockTime, solanaGameId, solanaGameAddress, createdBy,
32
+ });
33
+
34
+ res.json({ success: true, pool });
35
+ } catch (error) {
36
+ console.error('[Pickem] Error creating pool:', error.message);
37
+ res.status(500).json({ success: false, error: error.message });
38
+ }
39
+ });
40
+
41
+ // List pools
42
+ router.get('/pools', async (req, res) => {
43
+ try {
44
+ const { status } = req.query;
45
+ const pools = await pickemController.getPools({ status: status || undefined });
46
+ res.json({ success: true, pools });
47
+ } catch (error) {
48
+ console.error('[Pickem] Error listing pools:', error.message);
49
+ res.status(500).json({ success: false, error: error.message });
50
+ }
51
+ });
52
+
53
+ // Get pool detail with fights and stats
54
+ router.get('/pools/:id', async (req, res) => {
55
+ try {
56
+ const poolId = parseInt(req.params.id);
57
+ const pool = await pickemController.getPoolById(poolId);
58
+ if (!pool) return res.status(404).json({ success: false, error: 'Pool not found' });
59
+
60
+ const fights = await pickemController.getFights(poolId);
61
+ const stats = await pickemController.getPoolStats(poolId);
62
+
63
+ res.json({ success: true, pool, fights, stats });
64
+ } catch (error) {
65
+ console.error('[Pickem] Error getting pool:', error.message);
66
+ res.status(500).json({ success: false, error: error.message });
67
+ }
68
+ });
69
+
70
+ // Update a pool (authenticated — set solana_game_id/address after on-chain creation)
71
+ router.patch('/pools/:id', authenticate, async (req, res) => {
72
+ try {
73
+ const poolId = parseInt(req.params.id);
74
+ const pool = await pickemController.updatePool(poolId, req.body);
75
+ if (!pool) return res.status(404).json({ success: false, error: 'Pool not found' });
76
+ res.json({ success: true, pool });
77
+ } catch (error) {
78
+ console.error('[Pickem] Error updating pool:', error.message);
79
+ res.status(500).json({ success: false, error: error.message });
80
+ }
81
+ });
82
+
83
+ // ========== FIGHT ENDPOINTS ==========
84
+
85
+ // Get fights for a pool
86
+ router.get('/pools/:id/fights', async (req, res) => {
87
+ try {
88
+ const poolId = parseInt(req.params.id);
89
+ const fights = await pickemController.getFights(poolId);
90
+ res.json({ success: true, count: fights.length, fights });
91
+ } catch (error) {
92
+ console.error('[Pickem] Error getting fights:', error.message);
93
+ res.status(500).json({ success: false, error: error.message });
94
+ }
95
+ });
96
+
97
+ // Import fights for a pool (admin)
98
+ router.post('/pools/:id/fights', async (req, res) => {
99
+ try {
100
+ const poolId = parseInt(req.params.id);
101
+ const { fights } = req.body;
102
+
103
+ if (!fights || !Array.isArray(fights) || fights.length === 0) {
104
+ return res.status(400).json({ success: false, error: 'fights array is required' });
105
+ }
106
+
107
+ const imported = await pickemController.importFights(poolId, fights);
108
+ res.json({ success: true, count: imported.length, fights: imported });
109
+ } catch (error) {
110
+ console.error('[Pickem] Error importing fights:', error.message);
111
+ res.status(500).json({ success: false, error: error.message });
112
+ }
113
+ });
114
+
115
+ // ========== ENTRY ENDPOINTS ==========
116
+
117
+ // Enter a pool (authenticated — after on-chain join)
118
+ router.post('/pools/:id/enter', authenticate, async (req, res) => {
119
+ try {
120
+ const poolId = parseInt(req.params.id);
121
+ const { txSignature } = req.body;
122
+
123
+ const entry = await pickemController.joinPool({
124
+ poolId,
125
+ userId: req.user.userId,
126
+ walletAddress: req.user.walletAddress,
127
+ txSignature,
128
+ });
129
+
130
+ if (io) {
131
+ io.emit('pickem:entry_joined', { poolId, entry });
132
+ }
133
+
134
+ res.json({ success: true, entry });
135
+ } catch (error) {
136
+ console.error('[Pickem] Error joining pool:', error.message);
137
+ const status = error.message.includes('not found') ? 404
138
+ : error.message.includes('already') || error.message.includes('duplicate') ? 409
139
+ : 400;
140
+ res.status(status).json({ success: false, error: error.message });
141
+ }
142
+ });
143
+
144
+ // ========== PICK ENDPOINTS ==========
145
+
146
+ // Submit all picks at once (authenticated)
147
+ router.post('/pools/:id/picks', authenticate, async (req, res) => {
148
+ try {
149
+ const poolId = parseInt(req.params.id);
150
+ const { picks } = req.body;
151
+
152
+ if (!picks || !Array.isArray(picks)) {
153
+ return res.status(400).json({ success: false, error: 'picks array is required' });
154
+ }
155
+
156
+ const submitted = await pickemController.submitPicks({
157
+ poolId,
158
+ userId: req.user.userId,
159
+ picks,
160
+ });
161
+
162
+ if (io) {
163
+ io.emit('pickem:picks_submitted', { poolId, userId: req.user.userId });
164
+ }
165
+
166
+ res.json({ success: true, picks: submitted });
167
+ } catch (error) {
168
+ console.error('[Pickem] Error submitting picks:', error.message);
169
+ const status = error.message.includes('not found') ? 404
170
+ : error.message.includes('Lock time') ? 403
171
+ : 400;
172
+ res.status(status).json({ success: false, error: error.message });
173
+ }
174
+ });
175
+
176
+ // Get user's picks (authenticated)
177
+ router.get('/pools/:id/my-picks', authenticate, async (req, res) => {
178
+ try {
179
+ const poolId = parseInt(req.params.id);
180
+ const entry = await pickemController.getUserEntry(poolId, req.user.userId);
181
+
182
+ if (!entry) {
183
+ return res.status(404).json({ success: false, error: 'Not entered in this pool' });
184
+ }
185
+
186
+ res.json({ success: true, entry });
187
+ } catch (error) {
188
+ console.error('[Pickem] Error getting picks:', error.message);
189
+ res.status(500).json({ success: false, error: error.message });
190
+ }
191
+ });
192
+
193
+ // ========== LEADERBOARD / RESULTS ==========
194
+
195
+ // Leaderboard (public)
196
+ router.get('/pools/:id/leaderboard', async (req, res) => {
197
+ try {
198
+ const poolId = parseInt(req.params.id);
199
+ const leaderboard = await pickemController.getLeaderboard(poolId);
200
+ const stats = await pickemController.getPoolStats(poolId);
201
+ res.json({ success: true, totalEntries: leaderboard.length, leaderboard, stats });
202
+ } catch (error) {
203
+ console.error('[Pickem] Error getting leaderboard:', error.message);
204
+ res.status(500).json({ success: false, error: error.message });
205
+ }
206
+ });
207
+
208
+ // Final results (public, pool must be complete)
209
+ router.get('/pools/:id/results', async (req, res) => {
210
+ try {
211
+ const poolId = parseInt(req.params.id);
212
+ const pool = await pickemController.getPoolById(poolId);
213
+ if (!pool) return res.status(404).json({ success: false, error: 'Pool not found' });
214
+ if (pool.status !== 'complete') {
215
+ return res.status(400).json({ success: false, error: `Pool is ${pool.status}, results not available yet` });
216
+ }
217
+
218
+ const fights = await pickemController.getFights(poolId);
219
+ const leaderboard = await pickemController.getLeaderboard(poolId);
220
+ const winners = await pickemController.getWinners(poolId);
221
+ const payouts = await pickemController.getPayouts(poolId);
222
+ const stats = await pickemController.getPoolStats(poolId);
223
+
224
+ res.json({ success: true, pool, fights, leaderboard, winners, payouts, stats });
225
+ } catch (error) {
226
+ console.error('[Pickem] Error getting results:', error.message);
227
+ res.status(500).json({ success: false, error: error.message });
228
+ }
229
+ });
230
+
231
+ module.exports = router;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 📊 Player Stats API Routes
3
+ *
4
+ * Endpoints for player PNL, history, and leaderboards
5
+ */
6
+
7
+ const express = require('express');
8
+ const router = express.Router();
9
+ const { authenticate } = require('../middleware/authenticate');
10
+
11
+ module.exports = (playerStatsService) => {
12
+
13
+ /**
14
+ * GET /stats/player/:walletAddress
15
+ * Get player statistics (PUBLIC - basic stats for profile cards)
16
+ */
17
+ router.get('/player/:walletAddress', async (req, res) => {
18
+ try {
19
+ const { walletAddress } = req.params;
20
+
21
+ const stats = await playerStatsService.getPlayerStats(walletAddress);
22
+
23
+ if (!stats) {
24
+ return res.status(404).json({
25
+ success: false,
26
+ error: 'Player not found'
27
+ });
28
+ }
29
+
30
+ res.json({
31
+ success: true,
32
+ stats
33
+ });
34
+ } catch (error) {
35
+ console.error('Error fetching player stats:', error);
36
+ res.status(500).json({
37
+ success: false,
38
+ error: error.message
39
+ });
40
+ }
41
+ });
42
+
43
+ /**
44
+ * GET /stats/player/:walletAddress/history
45
+ * Get player history for chart (SECURED - detailed betting history is sensitive)
46
+ */
47
+ router.get('/player/:walletAddress/history', authenticate, async (req, res) => {
48
+ try {
49
+ const { walletAddress } = req.params;
50
+ const limit = parseInt(req.query.limit) || 100;
51
+
52
+ const history = await playerStatsService.getPlayerHistory(walletAddress, limit);
53
+
54
+ res.json({
55
+ success: true,
56
+ history,
57
+ count: history.length
58
+ });
59
+ } catch (error) {
60
+ console.error('Error fetching player history:', error);
61
+ res.status(500).json({
62
+ success: false,
63
+ error: error.message
64
+ });
65
+ }
66
+ });
67
+
68
+ /**
69
+ * GET /stats/leaderboard
70
+ * Get top players by PNL
71
+ */
72
+ router.get('/leaderboard', async (req, res) => {
73
+ try {
74
+ const limit = parseInt(req.query.limit) || 10;
75
+
76
+ const leaderboard = await playerStatsService.getLeaderboard(limit);
77
+
78
+ res.json({
79
+ success: true,
80
+ leaderboard,
81
+ count: leaderboard.length
82
+ });
83
+ } catch (error) {
84
+ console.error('Error fetching leaderboard:', error);
85
+ res.status(500).json({
86
+ success: false,
87
+ error: error.message
88
+ });
89
+ }
90
+ });
91
+
92
+ /**
93
+ * GET /stats/lucky
94
+ * Get the luckiest player (highest current winning streak)
95
+ */
96
+ router.get('/lucky', async (req, res) => {
97
+ try {
98
+ const luckyPlayer = await playerStatsService.getLuckiestPlayer();
99
+
100
+ if (!luckyPlayer) {
101
+ return res.json({
102
+ success: true,
103
+ lucky: null,
104
+ message: 'No active winning streaks'
105
+ });
106
+ }
107
+
108
+ res.json({
109
+ success: true,
110
+ lucky: luckyPlayer
111
+ });
112
+ } catch (error) {
113
+ console.error('Error fetching luckiest player:', error);
114
+ res.status(500).json({
115
+ success: false,
116
+ error: error.message
117
+ });
118
+ }
119
+ });
120
+
121
+ /**
122
+ * GET /stats/streaks
123
+ * Get streak leaderboard (top players by current winning streak)
124
+ */
125
+ router.get('/streaks', async (req, res) => {
126
+ try {
127
+ const limit = parseInt(req.query.limit) || 10;
128
+
129
+ const streakLeaderboard = await playerStatsService.getStreakLeaderboard(limit);
130
+
131
+ res.json({
132
+ success: true,
133
+ streaks: streakLeaderboard,
134
+ count: streakLeaderboard.length
135
+ });
136
+ } catch (error) {
137
+ console.error('Error fetching streak leaderboard:', error);
138
+ res.status(500).json({
139
+ success: false,
140
+ error: error.message
141
+ });
142
+ }
143
+ });
144
+
145
+ return router;
146
+ };
147
+