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,1271 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const axios = require('axios');
4
+ const theSportsDB = require('thesportsdb');
5
+
6
+ // Set the API key
7
+ const API_KEY = '819154';
8
+ theSportsDB.setApiKey(API_KEY);
9
+
10
+ // League IDs for our sports
11
+ const LEAGUE_IDS = {
12
+ NBA: 4387,
13
+ NHL: 4380,
14
+ MLB: 4424,
15
+ NFL: 4391,
16
+ EPL: 4328,
17
+ UFC: 4443,
18
+ NCAAF: 4479, // NCAA Division 1 College Football
19
+ NCAAB: 4607 // NCAA Division I Basketball Mens
20
+ };
21
+
22
+ // ESPN Rankings URLs
23
+ const ESPN_RANKINGS_URLS = {
24
+ NCAAF: 'http://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings',
25
+ NCAAB: 'http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/rankings'
26
+ };
27
+
28
+ /**
29
+ * @route GET /api/sports/leagues
30
+ * @desc Get all available leagues
31
+ * @access Public
32
+ */
33
+ router.get('/leagues', async (req, res) => {
34
+ try {
35
+ const data = await theSportsDB.getLeagueList();
36
+ res.json({ success: true, data });
37
+ } catch (error) {
38
+ console.error('Error fetching leagues:', error);
39
+ res.status(500).json({ success: false, error: 'Failed to fetch leagues' });
40
+ }
41
+ });
42
+
43
+ /**
44
+ * @route GET /api/sports/league/:id
45
+ * @desc Get details for a specific league
46
+ * @access Public
47
+ */
48
+ router.get('/league/:id', async (req, res) => {
49
+ try {
50
+ const { id } = req.params;
51
+ const data = await theSportsDB.getLeagueDetailsById(id);
52
+ res.json({ success: true, data });
53
+ } catch (error) {
54
+ console.error(`Error fetching league ${req.params.id}:`, error);
55
+ res.status(500).json({ success: false, error: 'Failed to fetch league details' });
56
+ }
57
+ });
58
+
59
+ /**
60
+ * @route GET /api/sports/teams/:league
61
+ * @desc Get teams for a specific league (NBA, NHL, MLB)
62
+ * @access Public
63
+ */
64
+ router.get('/teams/:league', async (req, res) => {
65
+ try {
66
+ const { league } = req.params;
67
+ const leagueId = LEAGUE_IDS[league.toUpperCase()];
68
+
69
+ if (!leagueId) {
70
+ return res.status(400).json({
71
+ success: false,
72
+ error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
73
+ });
74
+ }
75
+
76
+ // Get league details first to get the league name
77
+ const leagueDetails = await theSportsDB.getLeagueDetailsById(leagueId);
78
+ if (!leagueDetails.leagues || leagueDetails.leagues.length === 0) {
79
+ return res.status(404).json({ success: false, error: 'League not found' });
80
+ }
81
+
82
+ const leagueName = leagueDetails.leagues[0].strLeague;
83
+ const teams = await theSportsDB.getTeamsByLeagueName(leagueName);
84
+
85
+ res.json({ success: true, data: teams });
86
+ } catch (error) {
87
+ console.error(`Error fetching teams for ${req.params.league}:`, error);
88
+ res.status(500).json({ success: false, error: 'Failed to fetch teams' });
89
+ }
90
+ });
91
+
92
+ /**
93
+ * ═══════════════════════════════════════════════════════════════════════════
94
+ * 🥊 UFC STUB EVENTS - ISOLATED DATA SOURCE
95
+ * ═══════════════════════════════════════════════════════════════════════════
96
+ * TheSportsDB has unreliable/empty UFC data, so we maintain curated stub data.
97
+ * This function is completely isolated from other sports data sources.
98
+ *
99
+ * To update UFC events:
100
+ * 1. Add new events to the array below
101
+ * 2. Upload fighter images to S3 (dubs-avatars-prod/UFC/)
102
+ * 3. Update strHomeTeamBadge and strAwayTeamBadge URLs
103
+ *
104
+ * Structure matches TheSportsDB format exactly for oracle compatibility.
105
+ * ═══════════════════════════════════════════════════════════════════════════
106
+ */
107
+ function getUFCStubEvents() {
108
+ // Filter out events that have already passed
109
+ const now = new Date();
110
+
111
+ const allEvents = [];
112
+
113
+ // ═══════════════════════════════════════════════════════════════════════
114
+ // 🧪 MOCK UFC EVENT - Development Testing (dynamic date, 5 min from now)
115
+ // ═══════════════════════════════════════════════════════════════════════
116
+ if (process.env.NODE_ENV === 'development') {
117
+ const startTime = new Date(now.getTime() + 5 * 60000); // 5 minutes from now
118
+ const formattedDate = startTime.toISOString().split('T')[0];
119
+ const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
120
+ const timestamp = `${formattedDate}T${formattedTime}`;
121
+
122
+ allEvents.push({
123
+ "idEvent": "mock-ufc-gaethje-pimblett",
124
+ "idAPIfootball": null,
125
+ "strEvent": "Justin Gaethje vs Paddy Pimblett",
126
+ "strEventAlternate": "[MOCK] UFC 324: Gaethje vs. Pimblett",
127
+ "strFilename": `UFC ${formattedDate} UFC 324 Gaethje vs Pimblett`,
128
+ "strSport": "Fighting",
129
+ "idLeague": "4443",
130
+ "strLeague": "UFC",
131
+ "strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
132
+ "strSeason": "2026",
133
+ "strDescriptionEN": "[MOCK EVENT] UFC 324 Main Event - Lightweight bout",
134
+ "strHomeTeam": "Justin Gaethje",
135
+ "strAwayTeam": "Paddy Pimblett",
136
+ "intHomeScore": null,
137
+ "intRound": "1",
138
+ "intAwayScore": null,
139
+ "intSpectators": null,
140
+ "strOfficial": null,
141
+ "strTimestamp": timestamp,
142
+ "dateEvent": formattedDate,
143
+ "dateEventLocal": formattedDate,
144
+ "strTime": formattedTime,
145
+ "strTimeLocal": formattedTime,
146
+ "strGroup": "Main Card",
147
+ "idHomeTeam": "3022345",
148
+ "strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/justin_gaethje.png",
149
+ "idAwayTeam": "4008549",
150
+ "strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/paddy_pimblett.png",
151
+ "intScore": null,
152
+ "intScoreVotes": null,
153
+ "strResult": "",
154
+ "idVenue": "16132",
155
+ "strVenue": "T-Mobile Arena",
156
+ "strCountry": "United States",
157
+ "strCity": "Las Vegas, NV",
158
+ "strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
159
+ "strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
160
+ "strFanart": "",
161
+ "strThumb": "",
162
+ "strBanner": "",
163
+ "strMap": "",
164
+ "strTweet1": "",
165
+ "strVideo": "",
166
+ "strStatus": "Not Started",
167
+ "strPostponed": "no",
168
+ "strLocked": "unlocked"
169
+ });
170
+
171
+ console.log(`[UFC] Added mock event for ${formattedDate} at ${formattedTime}`);
172
+ }
173
+
174
+ // Real UFC events (will be filtered out if in the past)
175
+ allEvents.push({
176
+ "idEvent": "2389036",
177
+ "idAPIfootball": null,
178
+ "strEvent": "Justin Gaethje vs Paddy Pimblett",
179
+ "strEventAlternate": "UFC 324: Gaethje vs. Pimblett",
180
+ "strFilename": "UFC 2026-01-24 UFC 324 Gaethje vs Pimblett",
181
+ "strSport": "Fighting",
182
+ "idLeague": "4443",
183
+ "strLeague": "UFC",
184
+ "strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
185
+ "strSeason": "2026",
186
+ "strDescriptionEN": "UFC 324 Main Event - Lightweight bout",
187
+ "strHomeTeam": "Justin Gaethje",
188
+ "strAwayTeam": "Paddy Pimblett",
189
+ "intHomeScore": null,
190
+ "intRound": "1",
191
+ "intAwayScore": null,
192
+ "intSpectators": null,
193
+ "strOfficial": null,
194
+ "strTimestamp": "2026-01-25T02:00:00",
195
+ "dateEvent": "2026-01-25",
196
+ "dateEventLocal": "2026-01-24",
197
+ "strTime": "02:00:00",
198
+ "strTimeLocal": "21:00:00",
199
+ "strGroup": "Main Card",
200
+ "idHomeTeam": "3022345",
201
+ "strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/justin_gaethje.png",
202
+ "idAwayTeam": "4008549",
203
+ "strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/paddy_pimblett.png",
204
+ "intScore": null,
205
+ "intScoreVotes": null,
206
+ "strResult": "",
207
+ "idVenue": "16132",
208
+ "strVenue": "T-Mobile Arena",
209
+ "strCountry": "United States",
210
+ "strCity": "Las Vegas, NV",
211
+ "strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
212
+ "strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
213
+ "strFanart": "",
214
+ "strThumb": "",
215
+ "strBanner": "",
216
+ "strMap": "",
217
+ "strTweet1": "",
218
+ "strVideo": "",
219
+ "strStatus": "Not Started",
220
+ "strPostponed": "no",
221
+ "strLocked": "unlocked"
222
+ });
223
+
224
+ allEvents.push({
225
+ "idEvent": "2389037",
226
+ "idAPIfootball": null,
227
+ "strEvent": "Alexander Volkanovski vs Diego Lopes",
228
+ "strEventAlternate": "UFC 325: Volkanovski vs Lopes 2",
229
+ "strFilename": "UFC 2026-01-31 UFC 325 Volkanovski vs Lopes 2",
230
+ "strSport": "Fighting",
231
+ "idLeague": "4443",
232
+ "strLeague": "UFC",
233
+ "strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
234
+ "strSeason": "2026",
235
+ "strDescriptionEN": "UFC 325 Main Event - Featherweight bout",
236
+ "strHomeTeam": "Alexander Volkanovski",
237
+ "strAwayTeam": "Diego Lopes",
238
+ "intHomeScore": null,
239
+ "intRound": "2",
240
+ "intAwayScore": null,
241
+ "intSpectators": null,
242
+ "strOfficial": null,
243
+ "strTimestamp": "2026-02-01T02:00:00",
244
+ "dateEvent": "2026-02-01",
245
+ "dateEventLocal": "2026-02-01",
246
+ "strTime": "02:00:00",
247
+ "strTimeLocal": "13:00:00",
248
+ "strGroup": "Main Card",
249
+ "idHomeTeam": null,
250
+ "strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/alexander_volkanovski.png",
251
+ "idAwayTeam": null,
252
+ "strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/deigo_lopes.png",
253
+ "intScore": null,
254
+ "intScoreVotes": null,
255
+ "strResult": "",
256
+ "idVenue": "29731",
257
+ "strVenue": "Qudos Bank Arena",
258
+ "strCountry": "Australia",
259
+ "strCity": "Sydney, NSW",
260
+ "strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/volkanovski_lopes.png",
261
+ "strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/volkanovski_lopes.png",
262
+ "strFanart": "",
263
+ "strThumb": "",
264
+ "strBanner": "",
265
+ "strMap": "",
266
+ "strTweet1": "",
267
+ "strVideo": "",
268
+ "strStatus": "Not Started",
269
+ "strPostponed": "no",
270
+ "strLocked": "unlocked"
271
+ });
272
+
273
+ allEvents.push({
274
+ "idEvent": "2391879",
275
+ "idAPIfootball": null,
276
+ "strEvent": "Mario Bautista vs Vinicius Oliveira",
277
+ "strEventAlternate": "UFC Fight Night: Bautista vs Oliveira",
278
+ "strFilename": "UFC 2026-02-07 UFC Fight Night 266 Bautista vs Oliveira",
279
+ "strSport": "Fighting",
280
+ "idLeague": "4443",
281
+ "strLeague": "UFC",
282
+ "strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
283
+ "strSeason": "2026",
284
+ "strDescriptionEN": "UFC Fight Night Main Event - Bantamweight bout",
285
+ "strHomeTeam": "Mario Bautista",
286
+ "strAwayTeam": "Vinicius Oliveira",
287
+ "intHomeScore": null,
288
+ "intRound": "3",
289
+ "intAwayScore": null,
290
+ "intSpectators": null,
291
+ "strOfficial": null,
292
+ "strTimestamp": "2026-02-08T02:00:00",
293
+ "dateEvent": "2026-02-08",
294
+ "dateEventLocal": "2026-02-07",
295
+ "strTime": "02:00:00",
296
+ "strTimeLocal": "21:00:00",
297
+ "strGroup": "Main Card",
298
+ "idHomeTeam": null,
299
+ "strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/mario_bautista.png",
300
+ "idAwayTeam": null,
301
+ "strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/vinicius_oliveira.png",
302
+ "intScore": null,
303
+ "intScoreVotes": null,
304
+ "strResult": "",
305
+ "idVenue": "18567",
306
+ "strVenue": "UFC APEX",
307
+ "strCountry": "United States",
308
+ "strCity": "Las Vegas, Nevada",
309
+ "strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/bautista_oliveira.png",
310
+ "strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/bautista_oliveira.png",
311
+ "strFanart": "",
312
+ "strThumb": "",
313
+ "strBanner": "",
314
+ "strMap": "",
315
+ "strTweet1": "",
316
+ "strVideo": "",
317
+ "strStatus": "Not Started",
318
+ "strPostponed": "no",
319
+ "strLocked": "unlocked"
320
+ });
321
+
322
+ // Return only future events (filter out past events), sorted by date (soonest first)
323
+ return allEvents
324
+ .filter(event => {
325
+ const eventDate = new Date(event.strTimestamp + 'Z');
326
+ return eventDate > now;
327
+ })
328
+ .sort((a, b) => {
329
+ const dateA = new Date(a.strTimestamp + 'Z');
330
+ const dateB = new Date(b.strTimestamp + 'Z');
331
+ return dateA.getTime() - dateB.getTime();
332
+ });
333
+ }
334
+
335
+ /**
336
+ * ═══════════════════════════════════════════════════════════════════════════
337
+ * 🏀 NCAAB EVENTS - ESPN DATA SOURCE
338
+ * ═══════════════════════════════════════════════════════════════════════════
339
+ * TheSportsDB has unreliable NCAAB data (placeholder times, missing games),
340
+ * so we fetch from ESPN and transform to TheSportsDB format.
341
+ * ═══════════════════════════════════════════════════════════════════════════
342
+ */
343
+ async function getNCAABEventsFromESPN() {
344
+ const events = [];
345
+ const now = new Date();
346
+
347
+ // Query ESPN for the next 7 days
348
+ for (let i = 0; i < 7; i++) {
349
+ const date = new Date(now);
350
+ date.setDate(date.getDate() + i);
351
+ const dateStr = date.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD
352
+
353
+ try {
354
+ const response = await axios.get(
355
+ `https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=${dateStr}`
356
+ );
357
+
358
+ if (response.data?.events) {
359
+ for (const game of response.data.events) {
360
+ // Only include scheduled games (not finished or in progress)
361
+ if (game.status?.type?.name !== 'STATUS_SCHEDULED') continue;
362
+
363
+ // Extract home and away teams
364
+ const homeTeam = game.competitions?.[0]?.competitors?.find(c => c.homeAway === 'home');
365
+ const awayTeam = game.competitions?.[0]?.competitors?.find(c => c.homeAway === 'away');
366
+
367
+ if (!homeTeam || !awayTeam) continue;
368
+
369
+ // Parse ESPN date (already has Z) and convert to TheSportsDB format
370
+ const gameDate = new Date(game.date);
371
+ const dateEvent = gameDate.toISOString().split('T')[0];
372
+ const strTime = gameDate.toISOString().split('T')[1].substring(0, 8);
373
+ const strTimestamp = `${dateEvent}T${strTime}`;
374
+
375
+ // Transform to TheSportsDB format
376
+ events.push({
377
+ idEvent: `espn-ncaab-${game.id}`,
378
+ strEvent: `${homeTeam.team.displayName} vs ${awayTeam.team.displayName}`,
379
+ strEventAlternate: game.name,
380
+ strSport: "Basketball",
381
+ idLeague: "4607",
382
+ strLeague: "NCAAB",
383
+ strLeagueBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/ncaa.png",
384
+ strSeason: "2025-2026",
385
+ strHomeTeam: homeTeam.team.displayName,
386
+ strAwayTeam: awayTeam.team.displayName,
387
+ intHomeScore: null,
388
+ intAwayScore: null,
389
+ strTimestamp: strTimestamp,
390
+ dateEvent: dateEvent,
391
+ strTime: strTime,
392
+ idHomeTeam: homeTeam.team.id,
393
+ strHomeTeamBadge: homeTeam.team.logo,
394
+ idAwayTeam: awayTeam.team.id,
395
+ strAwayTeamBadge: awayTeam.team.logo,
396
+ strVenue: game.competitions?.[0]?.venue?.fullName || "",
397
+ strCity: game.competitions?.[0]?.venue?.address?.city || "",
398
+ strStatus: "NS",
399
+ strPostponed: "no",
400
+ strLocked: "unlocked"
401
+ });
402
+ }
403
+ }
404
+ } catch (error) {
405
+ console.error(`[NCAAB ESPN] Error fetching games for ${dateStr}:`, error.message);
406
+ }
407
+ }
408
+
409
+ // Sort by date (soonest first)
410
+ events.sort((a, b) => {
411
+ const dateA = new Date(a.strTimestamp + 'Z');
412
+ const dateB = new Date(b.strTimestamp + 'Z');
413
+ return dateA.getTime() - dateB.getTime();
414
+ });
415
+
416
+ console.log(`[NCAAB ESPN] Fetched ${events.length} upcoming games`);
417
+ return events;
418
+ }
419
+
420
+ /**
421
+ * ═══════════════════════════════════════════════════════════════════════════
422
+ * 🥊 UFC EVENT ID MAPPING - BACKWARD COMPATIBILITY
423
+ * ═══════════════════════════════════════════════════════════════════════════
424
+ * Maps fighter combinations to original stub event IDs to maintain backward
425
+ * compatibility with existing bets. The frontend matches bets by idEvent,
426
+ * so we must preserve these IDs for events that already have bets.
427
+ *
428
+ * Key format: "homeTeam|awayTeam" (lowercase, trimmed)
429
+ * ═══════════════════════════════════════════════════════════════════════════
430
+ */
431
+ const UFC_EVENT_ID_MAP = {
432
+ 'mario bautista|vinicius oliveira': '2391879',
433
+ };
434
+
435
+ /**
436
+ * Gets the original event ID for a UFC fight if it exists in our mapping
437
+ */
438
+ function getOriginalUFCEventId(homeTeam, awayTeam) {
439
+ const key = `${homeTeam.toLowerCase().trim()}|${awayTeam.toLowerCase().trim()}`;
440
+ return UFC_EVENT_ID_MAP[key] || null;
441
+ }
442
+
443
+ /**
444
+ * ═══════════════════════════════════════════════════════════════════════════
445
+ * 🥊 UFC EVENTS FROM ESPN
446
+ * ═══════════════════════════════════════════════════════════════════════════
447
+ * Fetch real UFC events from ESPN and transform to TheSportsDB format.
448
+ * ESPN provides scheduled fights with athlete IDs that we can use to construct
449
+ * headshot URLs using the pattern:
450
+ * https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/{id}.png
451
+ * ═══════════════════════════════════════════════════════════════════════════
452
+ */
453
+ async function getUFCEventsFromESPN() {
454
+ const events = [];
455
+ const now = new Date();
456
+
457
+ try {
458
+ // Query ESPN for UFC events in the next 60 days
459
+ const startDate = now.toISOString().split('T')[0].replace(/-/g, '');
460
+ const endDate = new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000)
461
+ .toISOString().split('T')[0].replace(/-/g, '');
462
+
463
+ const response = await axios.get(
464
+ `https://site.api.espn.com/apis/site/v2/sports/mma/ufc/scoreboard?dates=${startDate}-${endDate}`
465
+ );
466
+
467
+ if (response.data?.events) {
468
+ for (const card of response.data.events) {
469
+ // Get the main event (usually the last competition on the card)
470
+ const competitions = card.competitions || [];
471
+ if (competitions.length === 0) continue;
472
+
473
+ // Main event is the last fight on the card
474
+ const mainEvent = competitions[competitions.length - 1];
475
+ const competitors = mainEvent.competitors || [];
476
+ if (competitors.length < 2) continue;
477
+
478
+ // Find red corner (order 1) and blue corner (order 2)
479
+ const fighter1 = competitors.find(c => c.order === 1) || competitors[0];
480
+ const fighter2 = competitors.find(c => c.order === 2) || competitors[1];
481
+
482
+ // Construct headshot URLs from athlete IDs
483
+ const getHeadshotUrl = (athleteId) => {
484
+ if (!athleteId) return null;
485
+ return `https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/${athleteId}.png&w=350&h=254`;
486
+ };
487
+
488
+ // Parse date - use mainEvent.date (main card time) instead of card.date (prelims)
489
+ const eventDate = new Date(mainEvent.date || card.date);
490
+ const dateEvent = eventDate.toISOString().split('T')[0];
491
+ const strTime = eventDate.toISOString().split('T')[1].substring(0, 8);
492
+ const strTimestamp = `${dateEvent}T${strTime}`;
493
+
494
+ // Get venue info
495
+ const venue = mainEvent.venue || card.venues?.[0] || {};
496
+
497
+ // Weight class / fight type
498
+ const weightClass = mainEvent.type?.abbreviation || "Main Event";
499
+
500
+ // Check if this fight has an existing event ID (for backward compatibility with bets)
501
+ const homeTeam = fighter1.athlete?.fullName || 'TBA';
502
+ const awayTeam = fighter2.athlete?.fullName || 'TBA';
503
+ const originalEventId = getOriginalUFCEventId(homeTeam, awayTeam);
504
+ const eventId = originalEventId || `espn-ufc-${card.id}`;
505
+
506
+ events.push({
507
+ idEvent: eventId,
508
+ strEvent: `${homeTeam} vs ${awayTeam}`,
509
+ strEventAlternate: card.name,
510
+ strFilename: `UFC ${dateEvent} ${card.shortName || card.name}`,
511
+ strSport: "Fighting",
512
+ idLeague: "4443",
513
+ strLeague: "UFC",
514
+ strLeagueBadge: "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
515
+ strSeason: eventDate.getFullYear().toString(),
516
+ strDescriptionEN: `${card.name} - ${weightClass}`,
517
+ strHomeTeam: homeTeam,
518
+ strAwayTeam: awayTeam,
519
+ intHomeScore: null,
520
+ intAwayScore: null,
521
+ strTimestamp: strTimestamp,
522
+ dateEvent: dateEvent,
523
+ strTime: strTime,
524
+ strGroup: "Main Card",
525
+ idHomeTeam: fighter1.id,
526
+ strHomeTeamBadge: getHeadshotUrl(fighter1.id),
527
+ idAwayTeam: fighter2.id,
528
+ strAwayTeamBadge: getHeadshotUrl(fighter2.id),
529
+ strVenue: venue.fullName || "TBA",
530
+ strCountry: venue.address?.country || "",
531
+ strCity: venue.address?.city ? `${venue.address.city}, ${venue.address.state || ''}` : "",
532
+ strPoster: null, // ESPN doesn't provide card posters
533
+ strSquare: null,
534
+ strStatus: "NS",
535
+ strPostponed: "no",
536
+ strLocked: "unlocked",
537
+ // Extra ESPN data
538
+ espnEventId: card.id,
539
+ weightClass: weightClass,
540
+ fighterRecords: {
541
+ home: fighter1.records?.[0]?.summary || "",
542
+ away: fighter2.records?.[0]?.summary || ""
543
+ }
544
+ });
545
+ }
546
+ }
547
+ } catch (error) {
548
+ console.error(`[UFC ESPN] Error fetching events:`, error.message);
549
+ }
550
+
551
+ // Sort by date (soonest first)
552
+ events.sort((a, b) => {
553
+ const dateA = new Date(a.strTimestamp + 'Z');
554
+ const dateB = new Date(b.strTimestamp + 'Z');
555
+ return dateA.getTime() - dateB.getTime();
556
+ });
557
+
558
+ console.log(`[UFC ESPN] Fetched ${events.length} upcoming events`);
559
+ return events;
560
+ }
561
+
562
+ router.get('/events/:league', async (req, res) => {
563
+ try {
564
+ const { league } = req.params;
565
+ const leagueId = LEAGUE_IDS[league.toUpperCase()];
566
+
567
+ if (!leagueId) {
568
+ return res.status(400).json({
569
+ success: false,
570
+ error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
571
+ });
572
+ }
573
+
574
+ // ═══════════════════════════════════════════════════════════════════════
575
+ // 🥊 UFC EVENTS - ESPN DATA SOURCE
576
+ // ═══════════════════════════════════════════════════════════════════════
577
+ // Fetch real UFC events from ESPN with fighter headshots.
578
+ // Falls back to stub data if ESPN fails or returns no events.
579
+ // ═══════════════════════════════════════════════════════════════════════
580
+ if (league.toUpperCase() === 'UFC') {
581
+ let ufcEvents = await getUFCEventsFromESPN();
582
+
583
+ // Add mock event in development (for testing)
584
+ // Uses real fighter names/IDs so headshots work and oracle can resolve
585
+ if (process.env.NODE_ENV === 'development') {
586
+ const now = new Date();
587
+ const startTime = new Date(now.getTime() + 5 * 60000); // 5 minutes from now
588
+ const formattedDate = startTime.toISOString().split('T')[0];
589
+ const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
590
+ const timestamp = `${formattedDate}T${formattedTime}`;
591
+
592
+ ufcEvents.unshift({
593
+ idEvent: "mock-ufc-dev-test",
594
+ strEvent: "Mario Bautista vs Vinicius Oliveira",
595
+ strEventAlternate: "[MOCK] UFC Fight Night: Bautista vs Oliveira",
596
+ strSport: "Fighting",
597
+ idLeague: "4443",
598
+ strLeague: "UFC",
599
+ strLeagueBadge: "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
600
+ strSeason: "2026",
601
+ strDescriptionEN: "[MOCK EVENT] Development testing - Bantamweight bout",
602
+ strHomeTeam: "Mario Bautista",
603
+ strAwayTeam: "Vinicius Oliveira",
604
+ intHomeScore: null,
605
+ intAwayScore: null,
606
+ strTimestamp: timestamp,
607
+ dateEvent: formattedDate,
608
+ strTime: formattedTime,
609
+ strGroup: "Main Card",
610
+ idHomeTeam: "4410868",
611
+ strHomeTeamBadge: "https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/4410868.png&w=350&h=254",
612
+ idAwayTeam: "4884877",
613
+ strAwayTeamBadge: "https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/4884877.png&w=350&h=254",
614
+ strVenue: "UFC APEX",
615
+ strCountry: "United States",
616
+ strCity: "Las Vegas, NV",
617
+ strStatus: "NS",
618
+ strPostponed: "no",
619
+ strLocked: "unlocked"
620
+ });
621
+ console.log(`[UFC] Added mock event for development testing`);
622
+ }
623
+
624
+ // Fall back to stub data if ESPN returns no events
625
+ if (ufcEvents.length === 0) {
626
+ console.log(`[UFC] No ESPN events, falling back to stub data`);
627
+ ufcEvents = getUFCStubEvents();
628
+ }
629
+
630
+ return res.json({ success: true, data: { events: ufcEvents } });
631
+ }
632
+
633
+ // ═══════════════════════════════════════════════════════════════════════
634
+ // 🏀 NCAAB EVENTS - ESPN DATA SOURCE
635
+ // ═══════════════════════════════════════════════════════════════════════
636
+ // TheSportsDB has unreliable NCAAB data (placeholder times, missing games),
637
+ // so we fetch from ESPN and transform to TheSportsDB format.
638
+ // ═══════════════════════════════════════════════════════════════════════
639
+ if (league.toUpperCase() === 'NCAAB') {
640
+ const ncaabEvents = await getNCAABEventsFromESPN();
641
+
642
+ // Add mock event in development
643
+ if (process.env.NODE_ENV === 'development') {
644
+ const now = new Date();
645
+ const startTime = new Date(now.getTime() + 5 * 60000);
646
+ const formattedDate = startTime.toISOString().split('T')[0];
647
+ const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
648
+ const timestamp = `${formattedDate}T${formattedTime}`;
649
+
650
+ ncaabEvents.unshift({
651
+ idEvent: "mock-ncaab-duke-unc-123",
652
+ strEvent: "Duke vs North Carolina",
653
+ strEventAlternate: "North Carolina @ Duke",
654
+ strSport: "Basketball",
655
+ idLeague: "4607",
656
+ strLeague: "NCAAB",
657
+ strLeagueBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/ncaa.png",
658
+ strSeason: "2025-2026",
659
+ strDescriptionEN: "[MOCK EVENT] Tobacco Road Rivalry",
660
+ strHomeTeam: "Duke",
661
+ strAwayTeam: "North Carolina",
662
+ intHomeScore: null,
663
+ intAwayScore: null,
664
+ strTimestamp: timestamp,
665
+ dateEvent: formattedDate,
666
+ strTime: formattedTime,
667
+ idHomeTeam: "150",
668
+ strHomeTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/150.png",
669
+ idAwayTeam: "153",
670
+ strAwayTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/153.png",
671
+ strVenue: "Cameron Indoor Stadium",
672
+ strCity: "Durham, NC",
673
+ strStatus: "NS",
674
+ strPostponed: "no",
675
+ strLocked: "unlocked"
676
+ });
677
+ }
678
+
679
+ return res.json({ success: true, data: { events: ncaabEvents } });
680
+ }
681
+
682
+ const config = {
683
+ method: 'get',
684
+ maxBodyLength: Infinity,
685
+ url: `https://www.thesportsdb.com/api/v1/json/${API_KEY}/eventsnextleague.php?id=${leagueId}`,
686
+ headers: { }
687
+ };
688
+
689
+ const response = await axios.request(config);
690
+
691
+ // Add mock NBA event in development environment
692
+ if (process.env.NODE_ENV === 'development' && league.toUpperCase() === 'NBA') {
693
+ // If events array doesn't exist, create it
694
+ if (!response.data.events) {
695
+ response.data.events = [];
696
+ }
697
+
698
+ // Get current date for base
699
+ const today = new Date();
700
+
701
+ // Set time to current time + 2 minutes
702
+ const startTime = new Date(today.getTime() + 5 * 60000);
703
+
704
+ // Format date and time properly
705
+ const formattedDate = startTime.toISOString().split('T')[0]; // YYYY-MM-DD format
706
+ const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8); // HH:MM:SS format
707
+ const timestamp = `${formattedDate}T${formattedTime}`;
708
+
709
+ // Create a simple mock NBA event
710
+ const mockNBAEvent = {
711
+ idEvent: "mock-123456",
712
+ idAPIfootball: "mock-415061",
713
+ strEvent: "Adam vs. Amy",
714
+ strEventAlternate: "Amy @ Adams",
715
+ strFilename: `NBA ${formattedDate} Philadelphia 76ers vs Chicago Bulls`,
716
+ strSport: "Basketball",
717
+ idLeague: "4387",
718
+ strLeague: "NBA",
719
+ strLeagueBadge: "https://www.thesportsdb.com/images/media/league/badge/frdjqy1536585083.png",
720
+ strSeason: "2024-2025",
721
+ strDescriptionEN: "[MOCK EVENT] This is a mock event for development purposes.",
722
+ strHomeTeam: "Adam",
723
+ strAwayTeam: "Amy",
724
+ intHomeScore: null,
725
+ intRound: "0",
726
+ intAwayScore: null,
727
+ intSpectators: null,
728
+ strOfficial: null,
729
+ strTimestamp: timestamp,
730
+ dateEvent: formattedDate,
731
+ dateEventLocal: formattedDate,
732
+ strTime: formattedTime,
733
+ strTimeLocal: formattedTime,
734
+ strGroup: "",
735
+ idHomeTeam: "134863",
736
+ strHomeTeamBadge: "https://dubs-api-dev-f14bd1509129.herokuapp.com/images/adam.png",
737
+ idAwayTeam: "134870",
738
+ strAwayTeamBadge: "https://dubs-api-dev-f14bd1509129.herokuapp.com/images/amy.png",
739
+ intScore: null,
740
+ intScoreVotes: null,
741
+ strResult: "",
742
+ idVenue: "19333",
743
+ strVenue: "Wells Fargo Center",
744
+ strCountry: "United States",
745
+ strCity: "",
746
+ strPoster: "https://r2.thesportsdb.com/images/media/event/poster/omiq4k1738097128.jpg",
747
+ strSquare: "",
748
+ strFanart: null,
749
+ strThumb: "https://r2.thesportsdb.com/images/media/event/thumb/yl7lte1728123518.jpg",
750
+ strBanner: "",
751
+ strMap: null,
752
+ strTweet1: "",
753
+ strTweet2: "",
754
+ strTweet3: "",
755
+ strVideo: "",
756
+ strStatus: "NS",
757
+ strPostponed: "no",
758
+ strLocked: "unlocked"
759
+ };
760
+
761
+ // Add mock event to the beginning of the array
762
+ response.data.events.unshift(mockNBAEvent);
763
+ }
764
+
765
+ // Add mock EPL event in development environment (for testing draw betting)
766
+ if (process.env.NODE_ENV === 'development' && league.toUpperCase() === 'EPL') {
767
+ // If events array doesn't exist, create it
768
+ if (!response.data.events) {
769
+ response.data.events = [];
770
+ }
771
+
772
+ // Get current date for base
773
+ const today = new Date();
774
+
775
+ // Set time to current time + 5 minutes
776
+ const startTime = new Date(today.getTime() + 5 * 60000);
777
+
778
+ // Format date and time properly
779
+ const formattedDate = startTime.toISOString().split('T')[0];
780
+ const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
781
+ const timestamp = `${formattedDate}T${formattedTime}`;
782
+
783
+ // Create a mock EPL event (Chelsea @ Arsenal - will end in draw)
784
+ const mockEPLEvent = {
785
+ idEvent: "mock-epl-draw-123",
786
+ idAPIfootball: "mock-epl-415061",
787
+ strEvent: "Arsenal vs Chelsea",
788
+ strEventAlternate: "Chelsea @ Arsenal",
789
+ strFilename: `EPL ${formattedDate} Arsenal vs Chelsea`,
790
+ strSport: "Soccer",
791
+ idLeague: "4328",
792
+ strLeague: "English Premier League",
793
+ strLeagueBadge: "https://www.thesportsdb.com/images/media/league/badge/i6o0kh1549879062.png",
794
+ strSeason: "2024-2025",
795
+ strDescriptionEN: "[MOCK EVENT] Draw test - This game will end 1-1 for testing draw betting.",
796
+ strHomeTeam: "Arsenal",
797
+ strAwayTeam: "Chelsea",
798
+ intHomeScore: null,
799
+ intRound: "0",
800
+ intAwayScore: null,
801
+ intSpectators: null,
802
+ strOfficial: null,
803
+ strTimestamp: timestamp,
804
+ dateEvent: formattedDate,
805
+ dateEventLocal: formattedDate,
806
+ strTime: formattedTime,
807
+ strTimeLocal: formattedTime,
808
+ strGroup: "",
809
+ idHomeTeam: "133604",
810
+ strHomeTeamBadge: "https://www.thesportsdb.com/images/media/team/badge/uyhbfe1612467038.png",
811
+ idAwayTeam: "133610",
812
+ strAwayTeamBadge: "https://www.thesportsdb.com/images/media/team/badge/fbb0lh1617619204.png",
813
+ intScore: null,
814
+ intScoreVotes: null,
815
+ strResult: "",
816
+ idVenue: "12345",
817
+ strVenue: "Emirates Stadium",
818
+ strCountry: "England",
819
+ strCity: "London",
820
+ strPoster: "",
821
+ strSquare: "",
822
+ strFanart: null,
823
+ strThumb: "",
824
+ strBanner: "",
825
+ strMap: null,
826
+ strTweet1: "",
827
+ strTweet2: "",
828
+ strTweet3: "",
829
+ strVideo: "",
830
+ strStatus: "NS",
831
+ strPostponed: "no",
832
+ strLocked: "unlocked"
833
+ };
834
+
835
+ // Add mock EPL event to the beginning of the array
836
+ response.data.events.unshift(mockEPLEvent);
837
+ }
838
+
839
+ // Add mock NCAAF event in development environment
840
+ if (process.env.NODE_ENV === 'development' && league.toUpperCase() === 'NCAAF') {
841
+ // If events array doesn't exist, create it
842
+ if (!response.data.events) {
843
+ response.data.events = [];
844
+ }
845
+
846
+ // Get current date for base
847
+ const today = new Date();
848
+
849
+ // Set time to current time + 5 minutes
850
+ const startTime = new Date(today.getTime() + 5 * 60000);
851
+
852
+ // Format date and time properly
853
+ const formattedDate = startTime.toISOString().split('T')[0];
854
+ const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
855
+ const timestamp = `${formattedDate}T${formattedTime}`;
856
+
857
+ // Create a mock NCAAF event (Ohio State @ Michigan)
858
+ const mockNCAAFEvent = {
859
+ idEvent: "mock-ncaaf-osu-mich-123",
860
+ idAPIfootball: "mock-ncaaf-415061",
861
+ strEvent: "Ohio State vs Michigan",
862
+ strEventAlternate: "Michigan @ Ohio State",
863
+ strFilename: `NCAAF ${formattedDate} Ohio State vs Michigan`,
864
+ strSport: "American Football",
865
+ idLeague: "4479",
866
+ strLeague: "NCAA Division 1",
867
+ strLeagueAlternate: "NCAA Football",
868
+ strLeagueBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/ncaa.png",
869
+ strSeason: "2025-2026",
870
+ strDescriptionEN: "[MOCK EVENT] The Game - Classic Big Ten rivalry matchup",
871
+ strHomeTeam: "Ohio State",
872
+ strAwayTeam: "Michigan",
873
+ intHomeScore: null,
874
+ intRound: "0",
875
+ intAwayScore: null,
876
+ intSpectators: null,
877
+ strOfficial: null,
878
+ strTimestamp: timestamp,
879
+ dateEvent: formattedDate,
880
+ dateEventLocal: formattedDate,
881
+ strTime: formattedTime,
882
+ strTimeLocal: formattedTime,
883
+ strGroup: "",
884
+ idHomeTeam: "4522",
885
+ strHomeTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/194.png",
886
+ idAwayTeam: "4523",
887
+ strAwayTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/130.png",
888
+ intScore: null,
889
+ intScoreVotes: null,
890
+ strResult: "",
891
+ idVenue: "3856",
892
+ strVenue: "Ohio Stadium",
893
+ strCountry: "United States",
894
+ strCity: "Columbus, OH",
895
+ strPoster: "",
896
+ strSquare: "",
897
+ strFanart: null,
898
+ strThumb: "",
899
+ strBanner: "",
900
+ strMap: null,
901
+ strTweet1: "",
902
+ strTweet2: "",
903
+ strTweet3: "",
904
+ strVideo: "",
905
+ strStatus: "NS",
906
+ strPostponed: "no",
907
+ strLocked: "unlocked"
908
+ };
909
+
910
+ // Add mock NCAAF event to the beginning of the array
911
+ response.data.events.unshift(mockNCAAFEvent);
912
+ }
913
+
914
+ res.json({ success: true, data: response.data });
915
+ } catch (error) {
916
+ console.error(`Error fetching events for ${req.params.league}:`, error);
917
+ res.status(500).json({ success: false, error: 'Failed to fetch events' });
918
+ }
919
+ });
920
+
921
+ /**
922
+ * @route GET /api/sports/ncaab/teams
923
+ * @desc Get all NCAAB teams with pagination (from ESPN)
924
+ * @access Public
925
+ * @query page - Page number (default: 1)
926
+ * @query limit - Items per page (default: 25, max: 100)
927
+ */
928
+ router.get('/ncaab/teams', async (req, res) => {
929
+ try {
930
+ const page = Math.max(1, parseInt(req.query.page) || 1);
931
+ const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 25));
932
+
933
+ const response = await axios.get('http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams');
934
+
935
+ // ESPN returns teams nested under sports[0].leagues[0].teams
936
+ const leagueData = response.data?.sports?.[0]?.leagues?.[0];
937
+ const rawTeams = leagueData?.teams || [];
938
+
939
+ // Extract team objects (ESPN wraps each in a { team: {...} } object)
940
+ const allTeams = rawTeams.map(item => {
941
+ const team = item.team || item;
942
+ return {
943
+ id: team.id,
944
+ name: team.displayName,
945
+ shortName: team.shortDisplayName,
946
+ nickname: team.nickname,
947
+ abbreviation: team.abbreviation,
948
+ location: team.location,
949
+ color: team.color,
950
+ alternateColor: team.alternateColor,
951
+ logo: team.logos?.[0]?.href || null,
952
+ isActive: team.isActive
953
+ };
954
+ });
955
+
956
+ // Calculate pagination
957
+ const totalTeams = allTeams.length;
958
+ const totalPages = Math.ceil(totalTeams / limit);
959
+ const startIndex = (page - 1) * limit;
960
+ const endIndex = startIndex + limit;
961
+ const paginatedTeams = allTeams.slice(startIndex, endIndex);
962
+
963
+ res.json({
964
+ success: true,
965
+ data: {
966
+ teams: paginatedTeams,
967
+ pagination: {
968
+ page,
969
+ limit,
970
+ totalTeams,
971
+ totalPages,
972
+ hasNextPage: page < totalPages,
973
+ hasPrevPage: page > 1
974
+ }
975
+ }
976
+ });
977
+ } catch (error) {
978
+ console.error('Error fetching NCAAB teams:', error);
979
+ res.status(500).json({ success: false, error: 'Failed to fetch NCAAB teams' });
980
+ }
981
+ });
982
+
983
+ /**
984
+ * @route GET /api/sports/ncaab/conferences
985
+ * @desc Get all NCAAB conferences with their teams (from ESPN)
986
+ * @access Public
987
+ * @query includeTeams - Include teams in each conference (default: false)
988
+ */
989
+ router.get('/ncaab/conferences', async (req, res) => {
990
+ try {
991
+ const includeTeams = req.query.includeTeams === 'true';
992
+
993
+ const response = await axios.get('http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/groups');
994
+
995
+ // ESPN returns conferences nested under groups[0].children
996
+ const topLevelGroups = response.data?.groups || [];
997
+ const conferences = [];
998
+
999
+ for (const group of topLevelGroups) {
1000
+ const children = group.children || [];
1001
+ for (const conf of children) {
1002
+ const conferenceData = {
1003
+ id: conf.id,
1004
+ name: conf.name,
1005
+ abbreviation: conf.abbreviation,
1006
+ teamCount: conf.teams?.length || 0
1007
+ };
1008
+
1009
+ if (includeTeams && conf.teams) {
1010
+ conferenceData.teams = conf.teams.map(team => ({
1011
+ id: team.id,
1012
+ name: team.displayName,
1013
+ shortName: team.shortDisplayName,
1014
+ nickname: team.name,
1015
+ abbreviation: team.abbreviation,
1016
+ logo: team.logos?.[0]?.href || null
1017
+ }));
1018
+ }
1019
+
1020
+ conferences.push(conferenceData);
1021
+ }
1022
+ }
1023
+
1024
+ // Sort conferences alphabetically by name
1025
+ conferences.sort((a, b) => a.name.localeCompare(b.name));
1026
+
1027
+ res.json({
1028
+ success: true,
1029
+ data: {
1030
+ conferences,
1031
+ totalConferences: conferences.length
1032
+ }
1033
+ });
1034
+ } catch (error) {
1035
+ console.error('Error fetching NCAAB conferences:', error);
1036
+ res.status(500).json({ success: false, error: 'Failed to fetch NCAAB conferences' });
1037
+ }
1038
+ });
1039
+
1040
+ /**
1041
+ * @route GET /api/sports/ncaab/standings
1042
+ * @desc Get NCAAB standings by conference (from ESPN)
1043
+ * @access Public
1044
+ * @query conference - Filter by conference abbreviation (e.g., 'acc', 'big12')
1045
+ */
1046
+ router.get('/ncaab/standings', async (req, res) => {
1047
+ try {
1048
+ const conferenceFilter = req.query.conference?.toLowerCase();
1049
+
1050
+ const response = await axios.get('http://site.api.espn.com/apis/v2/sports/basketball/mens-college-basketball/standings');
1051
+
1052
+ // ESPN returns standings grouped by conference under children array
1053
+ const division = response.data;
1054
+ const conferenceStandings = division?.children || [];
1055
+
1056
+ // Helper to extract stat value by name
1057
+ const getStatValue = (stats, name) => {
1058
+ const stat = stats?.find(s => s.name === name || s.abbreviation === name);
1059
+ return stat?.displayValue || stat?.value || null;
1060
+ };
1061
+
1062
+ const standings = [];
1063
+
1064
+ for (const conf of conferenceStandings) {
1065
+ // Skip if filtering by conference and this isn't it
1066
+ if (conferenceFilter && conf.abbreviation?.toLowerCase() !== conferenceFilter) {
1067
+ continue;
1068
+ }
1069
+
1070
+ const conferenceData = {
1071
+ id: conf.id,
1072
+ name: conf.name,
1073
+ abbreviation: conf.abbreviation,
1074
+ teams: []
1075
+ };
1076
+
1077
+ // Get standings entries (teams)
1078
+ const entries = conf.standings?.entries || [];
1079
+
1080
+ for (const entry of entries) {
1081
+ const team = entry.team || {};
1082
+ const stats = entry.stats || [];
1083
+
1084
+ conferenceData.teams.push({
1085
+ id: team.id,
1086
+ name: team.displayName,
1087
+ shortName: team.shortDisplayName,
1088
+ abbreviation: team.abbreviation,
1089
+ logo: team.logos?.[0]?.href || null,
1090
+ stats: {
1091
+ wins: getStatValue(stats, 'wins'),
1092
+ losses: getStatValue(stats, 'losses'),
1093
+ winPercent: getStatValue(stats, 'winPercent'),
1094
+ conferenceWins: getStatValue(stats, 'leagueWinPercent'),
1095
+ pointsFor: getStatValue(stats, 'pointsFor'),
1096
+ pointsAgainst: getStatValue(stats, 'pointsAgainst'),
1097
+ pointDifferential: getStatValue(stats, 'pointDifferential'),
1098
+ streak: getStatValue(stats, 'streak'),
1099
+ playoffSeed: getStatValue(stats, 'playoffSeed')
1100
+ }
1101
+ });
1102
+ }
1103
+
1104
+ // Sort teams by wins descending, then by win percentage
1105
+ conferenceData.teams.sort((a, b) => {
1106
+ const winsA = parseInt(a.stats.wins) || 0;
1107
+ const winsB = parseInt(b.stats.wins) || 0;
1108
+ if (winsB !== winsA) return winsB - winsA;
1109
+ const pctA = parseFloat(a.stats.winPercent) || 0;
1110
+ const pctB = parseFloat(b.stats.winPercent) || 0;
1111
+ return pctB - pctA;
1112
+ });
1113
+
1114
+ standings.push(conferenceData);
1115
+ }
1116
+
1117
+ // Sort conferences alphabetically
1118
+ standings.sort((a, b) => a.name.localeCompare(b.name));
1119
+
1120
+ res.json({
1121
+ success: true,
1122
+ data: {
1123
+ standings,
1124
+ totalConferences: standings.length
1125
+ }
1126
+ });
1127
+ } catch (error) {
1128
+ console.error('Error fetching NCAAB standings:', error);
1129
+ res.status(500).json({ success: false, error: 'Failed to fetch NCAAB standings' });
1130
+ }
1131
+ });
1132
+
1133
+ /**
1134
+ * @route GET /api/sports/rankings/:league
1135
+ * @desc Get AP Top 25 rankings for NCAAF or NCAAB
1136
+ * @access Public
1137
+ * @query limit - Number of teams to return (default: 25, max: 25)
1138
+ */
1139
+ router.get('/rankings/:league', async (req, res) => {
1140
+ try {
1141
+ const { league } = req.params;
1142
+ const leagueUpper = league.toUpperCase();
1143
+ const url = ESPN_RANKINGS_URLS[leagueUpper];
1144
+
1145
+ if (!url) {
1146
+ return res.status(400).json({
1147
+ success: false,
1148
+ error: 'Invalid league. Rankings are available for NCAAF and NCAAB only.'
1149
+ });
1150
+ }
1151
+
1152
+ const limit = Math.min(25, Math.max(1, parseInt(req.query.limit) || 25));
1153
+
1154
+ const response = await axios.get(url);
1155
+ const rankingsData = response.data.rankings || [];
1156
+
1157
+ // Find AP Top 25 ranking (type: 'ap')
1158
+ const apRanking = rankingsData.find(r => r.type === 'ap') || rankingsData[0];
1159
+
1160
+ if (!apRanking) {
1161
+ return res.status(404).json({
1162
+ success: false,
1163
+ error: 'No rankings data available'
1164
+ });
1165
+ }
1166
+
1167
+ const ranks = (apRanking.ranks || []).slice(0, limit);
1168
+ const others = apRanking.others || [];
1169
+
1170
+ // Transform to a cleaner format
1171
+ const transformedRanks = ranks.map(rank => ({
1172
+ rank: rank.current,
1173
+ previousRank: rank.previous,
1174
+ trend: rank.trend,
1175
+ points: rank.points,
1176
+ firstPlaceVotes: rank.firstPlaceVotes,
1177
+ record: rank.recordSummary,
1178
+ team: {
1179
+ id: rank.team?.id,
1180
+ name: rank.team?.name || rank.team?.location,
1181
+ nickname: rank.team?.nickname,
1182
+ abbreviation: rank.team?.abbreviation,
1183
+ location: rank.team?.location,
1184
+ logo: rank.team?.logos?.[0]?.href || null,
1185
+ color: rank.team?.color,
1186
+ alternateColor: rank.team?.alternateColor
1187
+ }
1188
+ }));
1189
+
1190
+ const transformedOthers = others.map(rank => ({
1191
+ points: rank.points,
1192
+ trend: rank.trend,
1193
+ record: rank.recordSummary,
1194
+ team: {
1195
+ id: rank.team?.id,
1196
+ name: rank.team?.name || rank.team?.location,
1197
+ nickname: rank.team?.nickname,
1198
+ abbreviation: rank.team?.abbreviation,
1199
+ location: rank.team?.location,
1200
+ logo: rank.team?.logos?.[0]?.href || null
1201
+ }
1202
+ }));
1203
+
1204
+ res.json({
1205
+ success: true,
1206
+ data: {
1207
+ name: apRanking.name,
1208
+ type: apRanking.type,
1209
+ season: apRanking.season?.year,
1210
+ lastUpdated: apRanking.date,
1211
+ ranks: transformedRanks,
1212
+ othersReceivingVotes: transformedOthers
1213
+ }
1214
+ });
1215
+ } catch (error) {
1216
+ console.error(`Error fetching rankings for ${req.params.league}:`, error);
1217
+ res.status(500).json({ success: false, error: 'Failed to fetch rankings' });
1218
+ }
1219
+ });
1220
+
1221
+ /**
1222
+ * @route GET /api/sports/standings/:league/:season
1223
+ * @desc Get standings for a specific league and season
1224
+ * @access Public
1225
+ */
1226
+ router.get('/standings/:league/:season', async (req, res) => {
1227
+ try {
1228
+ const { league, season } = req.params;
1229
+ const leagueId = LEAGUE_IDS[league.toUpperCase()];
1230
+
1231
+ if (!leagueId) {
1232
+ return res.status(400).json({
1233
+ success: false,
1234
+ error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
1235
+ });
1236
+ }
1237
+
1238
+ const data = await theSportsDB.getLookupTableByLeagueIdAndSeason(leagueId, season);
1239
+ res.json({ success: true, data });
1240
+ } catch (error) {
1241
+ console.error(`Error fetching standings for ${req.params.league}:`, error);
1242
+ res.status(500).json({ success: false, error: 'Failed to fetch standings' });
1243
+ }
1244
+ });
1245
+
1246
+ /**
1247
+ * @route GET /api/sports/seasons/:league
1248
+ * @desc Get available seasons for a league
1249
+ * @access Public
1250
+ */
1251
+ router.get('/seasons/:league', async (req, res) => {
1252
+ try {
1253
+ const { league } = req.params;
1254
+ const leagueId = LEAGUE_IDS[league.toUpperCase()];
1255
+
1256
+ if (!leagueId) {
1257
+ return res.status(400).json({
1258
+ success: false,
1259
+ error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
1260
+ });
1261
+ }
1262
+
1263
+ const data = await theSportsDB.getSeasonsInLeagueById(leagueId);
1264
+ res.json({ success: true, data });
1265
+ } catch (error) {
1266
+ console.error(`Error fetching seasons for ${req.params.league}:`, error);
1267
+ res.status(500).json({ success: false, error: 'Failed to fetch seasons' });
1268
+ }
1269
+ });
1270
+
1271
+ module.exports = router;