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,376 @@
1
+ /**
2
+ * 🏀 Live Scores Controller
3
+ *
4
+ * Fetches live sports scores from ESPN API
5
+ * Supports: MLB, NBA, NHL, NFL, EPL, UFC, NCAAF, NCAAB
6
+ */
7
+
8
+ const axios = require('axios');
9
+
10
+ const ESPN_URLS = {
11
+ MLB: 'https://site.api.espn.com/apis/site/v2/sports/baseball/mlb/scoreboard',
12
+ NBA: 'https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard',
13
+ NHL: 'https://site.api.espn.com/apis/site/v2/sports/hockey/nhl/scoreboard',
14
+ NFL: 'https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard',
15
+ EPL: 'https://site.api.espn.com/apis/site/v2/sports/soccer/eng.1/scoreboard',
16
+ UFC: 'https://site.api.espn.com/apis/site/v2/sports/mma/ufc/scoreboard',
17
+ NCAAF: 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard',
18
+ NCAAB: 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard'
19
+ };
20
+
21
+ /**
22
+ * Fetch and parse scores for a specific league
23
+ */
24
+ async function fetchScoresForLeague(url, league) {
25
+ try {
26
+ console.log(`[LiveScores] Fetching scores from: ${url}`);
27
+ const { data } = await axios.get(url);
28
+
29
+ let scores = data.events.map(event => {
30
+ const competition = event.competitions?.[0];
31
+ const competitors = competition?.competitors || [];
32
+ const statusObj = event.status || {};
33
+
34
+ return {
35
+ date: event.date.split('T')[0],
36
+ game: event.name,
37
+ status: statusObj.type?.description || 'Unknown',
38
+ // Period/quarter info from ESPN
39
+ period: statusObj.period || null,
40
+ displayClock: statusObj.displayClock || null,
41
+ detail: statusObj.type?.detail || null,
42
+ shortDetail: statusObj.type?.shortDetail || null,
43
+ competitors: competitors.map(team => ({
44
+ name: team.team?.displayName,
45
+ homeAway: team.homeAway,
46
+ score: parseInt(team.score, 10) || 0,
47
+ logo: team.team?.logo || null,
48
+ abbreviation: team.team?.abbreviation || ''
49
+ }))
50
+ };
51
+ });
52
+
53
+ // In development, append mock data for testing
54
+ if (process.env.NODE_ENV === 'development') {
55
+ const today = new Date().toISOString().split('T')[0];
56
+
57
+ // Mock NBA game (Amy wins)
58
+ if (league === 'NBA') {
59
+ scores.push({
60
+ date: today,
61
+ game: "Amy @ Adam",
62
+ status: "Final",
63
+ competitors: [
64
+ { name: "Amy", homeAway: "away", score: 3 },
65
+ { name: "Adam", homeAway: "home", score: 2 }
66
+ ]
67
+ });
68
+ }
69
+
70
+ // Mock EPL game (Draw - equal scores for testing draw betting)
71
+ if (league === 'EPL') {
72
+ scores.push({
73
+ date: today,
74
+ game: "Chelsea @ Arsenal",
75
+ status: "Final",
76
+ competitors: [
77
+ { name: "Chelsea", homeAway: "away", score: 1 },
78
+ { name: "Arsenal", homeAway: "home", score: 1 }
79
+ ]
80
+ });
81
+ }
82
+
83
+ // Mock NCAAF game (Ohio State wins)
84
+ if (league === 'NCAAF') {
85
+ scores.push({
86
+ date: today,
87
+ game: "Michigan @ Ohio State",
88
+ status: "Final",
89
+ competitors: [
90
+ { name: "Michigan", homeAway: "away", score: 24 },
91
+ { name: "Ohio State", homeAway: "home", score: 31 }
92
+ ]
93
+ });
94
+ }
95
+
96
+ // Mock NCAAB game (Duke wins)
97
+ if (league === 'NCAAB') {
98
+ scores.push({
99
+ date: today,
100
+ game: "North Carolina @ Duke",
101
+ status: "Final",
102
+ competitors: [
103
+ { name: "North Carolina", homeAway: "away", score: 78 },
104
+ { name: "Duke", homeAway: "home", score: 85 }
105
+ ]
106
+ });
107
+ }
108
+ }
109
+
110
+ console.log(`[LiveScores] Successfully fetched ${scores.length} games`);
111
+ return scores;
112
+
113
+ } catch (error) {
114
+ console.error(`[LiveScores] Failed to fetch from ${url}:`, error.message);
115
+ return [];
116
+ }
117
+ }
118
+
119
+ /**
120
+ * ═══════════════════════════════════════════════════════════════════════════
121
+ * 🥊 UFC SCORES - ISOLATED HANDLER
122
+ * ═══════════════════════════════════════════════════════════════════════════
123
+ * This function is completely separate from team sports to prevent regressions.
124
+ * UFC data structure is different: individual fighters with winner:boolean
125
+ * instead of teams with scores.
126
+ *
127
+ * Transforms UFC data to match the standard format expected by the oracle:
128
+ * - winner:true → score: 1
129
+ * - winner:false → score: 0
130
+ * - competitor order 1 → homeAway: 'home'
131
+ * - competitor order 2 → homeAway: 'away'
132
+ * ═══════════════════════════════════════════════════════════════════════════
133
+ */
134
+ async function fetchUFCScores(url) {
135
+ try {
136
+ console.log(`[LiveScores:UFC] Fetching UFC scores from: ${url}`);
137
+ const { data } = await axios.get(url);
138
+
139
+ const scores = [];
140
+
141
+ // ESPN UFC returns events, each containing multiple competitions (fights)
142
+ if (!data.events || !Array.isArray(data.events)) {
143
+ console.log(`[LiveScores:UFC] No events found in response`);
144
+ return scores;
145
+ }
146
+
147
+ for (const event of data.events) {
148
+ // Each event (e.g., "UFC 324") contains multiple competitions (fights)
149
+ if (!event.competitions || !Array.isArray(event.competitions)) {
150
+ continue;
151
+ }
152
+
153
+ for (const competition of event.competitions) {
154
+ // Each competition is a single fight between two fighters
155
+ const competitors = competition.competitors || [];
156
+
157
+ if (competitors.length !== 2) {
158
+ continue; // Skip if not exactly 2 fighters
159
+ }
160
+
161
+ // Determine fight status
162
+ const status = competition.status?.type;
163
+ let statusDescription = status?.description || 'Unknown';
164
+
165
+ // Map UFC status to standard format the oracle understands
166
+ // UFC uses: "Scheduled", "In Progress", "Final", etc.
167
+ if (status?.completed === true || status?.state === 'post') {
168
+ statusDescription = 'Final';
169
+ }
170
+
171
+ // Extract UFC-specific live data
172
+ const currentRound = competition.status?.period || 0;
173
+ const totalRounds = competition.format?.regulation?.periods || 3; // 3 for regular, 5 for title fights
174
+ const clock = competition.status?.displayClock || '';
175
+ const fightState = status?.state || 'pre'; // 'pre', 'in', 'post'
176
+ const statusDetail = status?.detail || '';
177
+ const statusShortDetail = status?.shortDetail || '';
178
+
179
+ // Parse competitors - order 1 is "home", order 2 is "away"
180
+ // Transform winner:boolean to score (1 for winner, 0 for loser)
181
+ const fighter1 = competitors.find(c => c.order === 1) || competitors[0];
182
+ const fighter2 = competitors.find(c => c.order === 2) || competitors[1];
183
+
184
+ // Extract fighter records (e.g., "26-5-0")
185
+ const fighter1Record = fighter1.records?.find(r => r.name === 'overall')?.summary || '';
186
+ const fighter2Record = fighter2.records?.find(r => r.name === 'overall')?.summary || '';
187
+
188
+ const transformedCompetitors = [
189
+ {
190
+ name: fighter1.athlete?.fullName || fighter1.athlete?.displayName || 'Unknown',
191
+ homeAway: 'home',
192
+ score: fighter1.winner === true ? 1 : 0,
193
+ winner: fighter1.winner || false,
194
+ logo: fighter1.athlete?.flag?.href || null,
195
+ headshot: fighter1.id ? `https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/${fighter1.id}.png&w=350&h=254` : null,
196
+ country: fighter1.athlete?.flag?.alt || null,
197
+ abbreviation: fighter1.athlete?.shortName || '',
198
+ record: fighter1Record,
199
+ athleteId: fighter1.id || null
200
+ },
201
+ {
202
+ name: fighter2.athlete?.fullName || fighter2.athlete?.displayName || 'Unknown',
203
+ homeAway: 'away',
204
+ score: fighter2.winner === true ? 1 : 0,
205
+ winner: fighter2.winner || false,
206
+ logo: fighter2.athlete?.flag?.href || null,
207
+ headshot: fighter2.id ? `https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/${fighter2.id}.png&w=350&h=254` : null,
208
+ country: fighter2.athlete?.flag?.alt || null,
209
+ abbreviation: fighter2.athlete?.shortName || '',
210
+ record: fighter2Record,
211
+ athleteId: fighter2.id || null
212
+ }
213
+ ];
214
+
215
+ // Extract date from competition or event
216
+ const fightDate = competition.date || event.date;
217
+ const dateOnly = fightDate ? fightDate.split('T')[0] : null;
218
+
219
+ // Build the game name as "Fighter1 vs Fighter2"
220
+ const gameName = `${transformedCompetitors[0].name} vs ${transformedCompetitors[1].name}`;
221
+
222
+ scores.push({
223
+ date: dateOnly,
224
+ game: gameName,
225
+ status: statusDescription,
226
+ eventName: event.name || event.shortName, // e.g., "UFC 324: Gaethje vs. Pimblett"
227
+ weightClass: competition.type?.abbreviation || '', // e.g., "Lightweight"
228
+ competitionId: competition.id,
229
+ competitors: transformedCompetitors,
230
+ // UFC-specific live data
231
+ ufcData: {
232
+ currentRound,
233
+ totalRounds,
234
+ clock,
235
+ fightState, // 'pre', 'in', 'post'
236
+ statusDetail,
237
+ statusShortDetail
238
+ }
239
+ });
240
+ }
241
+ }
242
+
243
+ console.log(`[LiveScores:UFC] Successfully parsed ${scores.length} fights`);
244
+
245
+ // ═══════════════════════════════════════════════════════════════════════
246
+ // 🧪 MOCK UFC DATA - Development Testing
247
+ // ═══════════════════════════════════════════════════════════════════════
248
+ // In development, append mock UFC fights for testing the oracle.
249
+ // These match the stub events in sportsRoutes.js
250
+ //
251
+ // IMPORTANT: The winner mapping:
252
+ // - homeAway: 'home' = strHomeTeam (fighter 1)
253
+ // - homeAway: 'away' = strAwayTeam (fighter 2)
254
+ // - score: 1 = winner, score: 0 = loser
255
+ // ═══════════════════════════════════════════════════════════════════════
256
+ if (process.env.NODE_ENV === 'development') {
257
+ // Use same date calculation as sportsRoutes.js mock event (5 min from now)
258
+ const startTime = new Date(Date.now() + 5 * 60000);
259
+ const mockDate = startTime.toISOString().split('T')[0];
260
+
261
+ // Mock UFC Fight: Bautista vs Oliveira - Bautista (HOME) WINS
262
+ // strHomeTeam = "Mario Bautista" → homeAway: 'home' → score: 1 (winner)
263
+ // strAwayTeam = "Vinicius Oliveira" → homeAway: 'away' → score: 0 (loser)
264
+ // MUST match sportsRoutes.js mock event fighters!
265
+ scores.push({
266
+ date: mockDate,
267
+ game: "Mario Bautista vs Vinicius Oliveira",
268
+ status: "Final",
269
+ eventName: "[MOCK] UFC Fight Night: Bautista vs Oliveira",
270
+ weightClass: "Bantamweight",
271
+ competitionId: "mock-ufc-dev-test",
272
+ competitors: [
273
+ {
274
+ name: "Mario Bautista",
275
+ homeAway: "home",
276
+ score: 1, // WINNER
277
+ winner: true,
278
+ logo: "https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/4410868.png&w=350&h=254",
279
+ abbreviation: "Bautista",
280
+ record: "16-3-0"
281
+ },
282
+ {
283
+ name: "Vinicius Oliveira",
284
+ homeAway: "away",
285
+ score: 0, // LOSER
286
+ winner: false,
287
+ logo: "https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/4884877.png&w=350&h=254",
288
+ abbreviation: "Oliveira",
289
+ record: "22-4-0"
290
+ }
291
+ ],
292
+ // UFC-specific data
293
+ ufcData: {
294
+ currentRound: 3,
295
+ totalRounds: 5,
296
+ clock: '',
297
+ fightState: 'post',
298
+ statusDetail: 'Decision - Unanimous',
299
+ statusShortDetail: 'Final - UD'
300
+ }
301
+ });
302
+
303
+ console.log(`[LiveScores:UFC] Added mock UFC fight for ${mockDate}`);
304
+ }
305
+
306
+ return scores;
307
+
308
+ } catch (error) {
309
+ console.error(`[LiveScores:UFC] Failed to fetch UFC scores:`, error.message);
310
+ return [];
311
+ }
312
+ }
313
+
314
+ /**
315
+ * GET /api/livescores
316
+ */
317
+ const getAllLivescores = async (req, res) => {
318
+ try {
319
+ const results = {};
320
+
321
+ await Promise.all(
322
+ Object.entries(ESPN_URLS).map(async ([league, url]) => {
323
+ // Route UFC to isolated handler, all other sports use existing handler
324
+ if (league === 'UFC') {
325
+ results[league] = await fetchUFCScores(url);
326
+ } else {
327
+ results[league] = await fetchScoresForLeague(url, league);
328
+ }
329
+ })
330
+ );
331
+
332
+ res.json({ success: true, data: results });
333
+
334
+ } catch (error) {
335
+ console.error('[LiveScores] Error fetching all livescores:', error);
336
+ res.status(500).json({ success: false, error: 'Failed to fetch sports scores' });
337
+ }
338
+ };
339
+
340
+ /**
341
+ * GET /api/livescores/:league
342
+ */
343
+ const getLivescoresByLeague = async (req, res) => {
344
+ const { league } = req.params;
345
+ const leagueUpper = league.toUpperCase();
346
+ const url = ESPN_URLS[leagueUpper];
347
+
348
+ if (!url) {
349
+ return res.status(400).json({
350
+ success: false,
351
+ error: `League "${league}" is not supported. Supported leagues: ${Object.keys(ESPN_URLS).join(', ')}`
352
+ });
353
+ }
354
+
355
+ try {
356
+ // Route UFC to isolated handler, all other sports use existing handler
357
+ let scores;
358
+ if (leagueUpper === 'UFC') {
359
+ scores = await fetchUFCScores(url);
360
+ } else {
361
+ scores = await fetchScoresForLeague(url, leagueUpper);
362
+ }
363
+ res.json({ success: true, data: scores });
364
+ } catch (error) {
365
+ console.error(`[LiveScores] Error fetching ${leagueUpper} scores:`, error);
366
+ res.status(500).json({ success: false, error: `Failed to fetch ${leagueUpper} scores` });
367
+ }
368
+ };
369
+
370
+ module.exports = {
371
+ getAllLivescores,
372
+ getLivescoresByLeague,
373
+ fetchScoresForLeague,
374
+ fetchUFCScores,
375
+ ESPN_URLS,
376
+ };