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,422 @@
1
+ -- ============================================
2
+ -- ANALYTICS TABLES MIGRATION
3
+ -- ============================================
4
+ -- Run this migration to set up the unified analytics system
5
+ -- This expands the basic audit_logs table into a full event-driven analytics system
6
+
7
+ -- ============================================
8
+ -- 1. CORE EVENTS TABLE
9
+ -- ============================================
10
+ -- This is the heart of the analytics system - every trackable action is an event
11
+
12
+ CREATE TABLE IF NOT EXISTS events (
13
+ -- Primary key
14
+ id BIGSERIAL PRIMARY KEY,
15
+
16
+ -- Event identification
17
+ event_id UUID NOT NULL DEFAULT gen_random_uuid(),
18
+ event_name VARCHAR(100) NOT NULL,
19
+ event_category VARCHAR(50) NOT NULL,
20
+
21
+ -- User context (nullable for anonymous events)
22
+ user_id VARCHAR(255), -- wallet_address or user identifier
23
+ session_id VARCHAR(100), -- Browser session ID
24
+ device_id VARCHAR(100), -- Device fingerprint (optional)
25
+
26
+ -- Event data
27
+ properties JSONB DEFAULT '{}', -- Event-specific data
28
+ context JSONB DEFAULT '{}', -- Environmental context (device, page, etc.)
29
+
30
+ -- Timing
31
+ client_timestamp TIMESTAMPTZ, -- When event occurred on client
32
+ server_timestamp TIMESTAMPTZ DEFAULT NOW(),
33
+
34
+ -- Source tracking
35
+ source VARCHAR(50) DEFAULT 'web', -- web, mobile, telegram, api, server
36
+ version VARCHAR(20), -- App version
37
+
38
+ -- Indexing helpers (denormalized for performance)
39
+ date_partition DATE GENERATED ALWAYS AS (DATE(server_timestamp)) STORED,
40
+ hour_partition INTEGER GENERATED ALWAYS AS (EXTRACT(HOUR FROM server_timestamp)::INTEGER) STORED,
41
+
42
+ -- For funnel analysis
43
+ funnel_step VARCHAR(100),
44
+ funnel_id VARCHAR(100)
45
+ );
46
+
47
+ -- Add comment
48
+ COMMENT ON TABLE events IS 'Unified event stream for analytics, logging, and auditing';
49
+
50
+ -- Performance indexes
51
+ CREATE INDEX IF NOT EXISTS idx_events_user_time ON events(user_id, server_timestamp DESC);
52
+ CREATE INDEX IF NOT EXISTS idx_events_category_time ON events(event_category, server_timestamp DESC);
53
+ CREATE INDEX IF NOT EXISTS idx_events_name_time ON events(event_name, server_timestamp DESC);
54
+ CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, server_timestamp);
55
+ CREATE INDEX IF NOT EXISTS idx_events_date_partition ON events(date_partition);
56
+ CREATE INDEX IF NOT EXISTS idx_events_funnel ON events(funnel_id, funnel_step) WHERE funnel_id IS NOT NULL;
57
+ CREATE INDEX IF NOT EXISTS idx_events_properties ON events USING gin(properties);
58
+ CREATE INDEX IF NOT EXISTS idx_events_context ON events USING gin(context);
59
+ CREATE INDEX IF NOT EXISTS idx_events_source ON events(source, server_timestamp DESC);
60
+
61
+ -- ============================================
62
+ -- 2. SESSION SUMMARIES TABLE
63
+ -- ============================================
64
+ -- Aggregated session data for faster querying
65
+
66
+ CREATE TABLE IF NOT EXISTS session_summaries (
67
+ id SERIAL PRIMARY KEY,
68
+ session_id VARCHAR(100) UNIQUE NOT NULL,
69
+ user_id VARCHAR(255),
70
+
71
+ -- Session timing
72
+ started_at TIMESTAMPTZ NOT NULL,
73
+ ended_at TIMESTAMPTZ,
74
+ last_activity_at TIMESTAMPTZ,
75
+ duration_seconds INTEGER,
76
+
77
+ -- Session metrics
78
+ event_count INTEGER DEFAULT 0,
79
+ page_views INTEGER DEFAULT 0,
80
+ games_viewed INTEGER DEFAULT 0,
81
+ games_created INTEGER DEFAULT 0,
82
+ games_joined INTEGER DEFAULT 0,
83
+ messages_sent INTEGER DEFAULT 0,
84
+
85
+ -- Conversion tracking
86
+ converted BOOLEAN DEFAULT FALSE,
87
+ conversion_type VARCHAR(50),
88
+ conversion_value NUMERIC(20, 9),
89
+
90
+ -- Device/context
91
+ device_type VARCHAR(50), -- mobile, tablet, desktop
92
+ platform VARCHAR(50), -- web, telegram, ios, android
93
+ browser VARCHAR(100),
94
+ os VARCHAR(100),
95
+ screen_resolution VARCHAR(20),
96
+
97
+ -- Traffic source
98
+ referrer VARCHAR(500),
99
+ landing_page VARCHAR(500),
100
+ utm_source VARCHAR(100),
101
+ utm_medium VARCHAR(100),
102
+ utm_campaign VARCHAR(100),
103
+ utm_term VARCHAR(100),
104
+ utm_content VARCHAR(100),
105
+
106
+ -- Geographic (optional - from IP lookup)
107
+ country_code VARCHAR(5),
108
+ region VARCHAR(100),
109
+ city VARCHAR(100),
110
+
111
+ created_at TIMESTAMPTZ DEFAULT NOW(),
112
+ updated_at TIMESTAMPTZ DEFAULT NOW()
113
+ );
114
+
115
+ COMMENT ON TABLE session_summaries IS 'Aggregated session data for analytics dashboards';
116
+
117
+ CREATE INDEX IF NOT EXISTS idx_session_user ON session_summaries(user_id);
118
+ CREATE INDEX IF NOT EXISTS idx_session_time ON session_summaries(started_at DESC);
119
+ CREATE INDEX IF NOT EXISTS idx_session_converted ON session_summaries(converted, conversion_type);
120
+ CREATE INDEX IF NOT EXISTS idx_session_platform ON session_summaries(platform, started_at DESC);
121
+
122
+ -- ============================================
123
+ -- 3. DAILY METRICS TABLE
124
+ -- ============================================
125
+ -- Pre-aggregated daily metrics for fast dashboard queries
126
+
127
+ CREATE TABLE IF NOT EXISTS daily_metrics (
128
+ id SERIAL PRIMARY KEY,
129
+ metric_date DATE NOT NULL,
130
+ metric_name VARCHAR(100) NOT NULL,
131
+
132
+ -- Dimensions (nullable - allows various groupings)
133
+ dimension_1 VARCHAR(100), -- e.g., sport type, platform, country
134
+ dimension_2 VARCHAR(100), -- e.g., device type, game mode
135
+ dimension_3 VARCHAR(100), -- e.g., utm_source
136
+
137
+ -- Metrics
138
+ count BIGINT DEFAULT 0,
139
+ sum_value NUMERIC(20, 9) DEFAULT 0,
140
+ avg_value NUMERIC(20, 9),
141
+ min_value NUMERIC(20, 9),
142
+ max_value NUMERIC(20, 9),
143
+ unique_users INTEGER DEFAULT 0,
144
+ unique_sessions INTEGER DEFAULT 0,
145
+
146
+ -- Metadata
147
+ created_at TIMESTAMPTZ DEFAULT NOW(),
148
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
149
+
150
+ UNIQUE(metric_date, metric_name, dimension_1, dimension_2, dimension_3)
151
+ );
152
+
153
+ COMMENT ON TABLE daily_metrics IS 'Pre-aggregated metrics for dashboard performance';
154
+
155
+ CREATE INDEX IF NOT EXISTS idx_daily_metrics_date ON daily_metrics(metric_date DESC);
156
+ CREATE INDEX IF NOT EXISTS idx_daily_metrics_name ON daily_metrics(metric_name, metric_date DESC);
157
+ CREATE INDEX IF NOT EXISTS idx_daily_metrics_dimensions ON daily_metrics(metric_name, dimension_1, dimension_2);
158
+
159
+ -- ============================================
160
+ -- 4. FUNNEL DEFINITIONS TABLE
161
+ -- ============================================
162
+ -- Store funnel configurations for analysis
163
+
164
+ CREATE TABLE IF NOT EXISTS funnel_definitions (
165
+ id SERIAL PRIMARY KEY,
166
+ funnel_id VARCHAR(100) UNIQUE NOT NULL,
167
+ funnel_name VARCHAR(200) NOT NULL,
168
+ description TEXT,
169
+
170
+ -- Steps as ordered JSON array
171
+ -- [{"step": 1, "event_name": "wallet_connected", "label": "Connected Wallet"}, ...]
172
+ steps JSONB NOT NULL,
173
+
174
+ -- Configuration
175
+ window_hours INTEGER DEFAULT 24, -- Max time between first and last step
176
+ require_order BOOLEAN DEFAULT TRUE, -- Must steps happen in order?
177
+ is_active BOOLEAN DEFAULT TRUE,
178
+
179
+ -- Ownership
180
+ created_by VARCHAR(255),
181
+
182
+ created_at TIMESTAMPTZ DEFAULT NOW(),
183
+ updated_at TIMESTAMPTZ DEFAULT NOW()
184
+ );
185
+
186
+ COMMENT ON TABLE funnel_definitions IS 'Funnel configurations for conversion analysis';
187
+
188
+ -- ============================================
189
+ -- 5. USER ANALYTICS PROFILES
190
+ -- ============================================
191
+ -- Aggregated user-level analytics (updated periodically)
192
+
193
+ CREATE TABLE IF NOT EXISTS user_analytics (
194
+ id SERIAL PRIMARY KEY,
195
+ user_id VARCHAR(255) UNIQUE NOT NULL,
196
+
197
+ -- Lifetime metrics
198
+ first_seen_at TIMESTAMPTZ,
199
+ last_seen_at TIMESTAMPTZ,
200
+ total_sessions INTEGER DEFAULT 0,
201
+ total_events INTEGER DEFAULT 0,
202
+
203
+ -- Engagement
204
+ total_page_views INTEGER DEFAULT 0,
205
+ total_games_created INTEGER DEFAULT 0,
206
+ total_games_joined INTEGER DEFAULT 0,
207
+ total_games_won INTEGER DEFAULT 0,
208
+ total_messages_sent INTEGER DEFAULT 0,
209
+
210
+ -- Financial
211
+ total_wagered_sol NUMERIC(20, 9) DEFAULT 0,
212
+ total_won_sol NUMERIC(20, 9) DEFAULT 0,
213
+ total_claimed_sol NUMERIC(20, 9) DEFAULT 0,
214
+
215
+ -- Referral metrics
216
+ referral_code VARCHAR(50),
217
+ referred_by VARCHAR(255),
218
+ total_referrals INTEGER DEFAULT 0,
219
+
220
+ -- Segmentation
221
+ is_paying_user BOOLEAN DEFAULT FALSE,
222
+ first_wager_at TIMESTAMPTZ,
223
+ user_tier VARCHAR(50) DEFAULT 'new', -- new, active, power, whale
224
+
225
+ -- Churn prediction
226
+ days_since_last_activity INTEGER,
227
+ churn_risk VARCHAR(20), -- low, medium, high
228
+
229
+ created_at TIMESTAMPTZ DEFAULT NOW(),
230
+ updated_at TIMESTAMPTZ DEFAULT NOW()
231
+ );
232
+
233
+ COMMENT ON TABLE user_analytics IS 'Aggregated user-level analytics for segmentation and analysis';
234
+
235
+ CREATE INDEX IF NOT EXISTS idx_user_analytics_tier ON user_analytics(user_tier);
236
+ CREATE INDEX IF NOT EXISTS idx_user_analytics_churn ON user_analytics(churn_risk);
237
+ CREATE INDEX IF NOT EXISTS idx_user_analytics_last_seen ON user_analytics(last_seen_at DESC);
238
+
239
+ -- ============================================
240
+ -- 6. INSERT DEFAULT FUNNELS
241
+ -- ============================================
242
+
243
+ INSERT INTO funnel_definitions (funnel_id, funnel_name, description, steps, window_hours)
244
+ VALUES
245
+ (
246
+ 'registration',
247
+ 'User Registration',
248
+ 'Track users from wallet connection to completed registration',
249
+ '[
250
+ {"step": 1, "event_name": "wallet_connected", "label": "Connected Wallet"},
251
+ {"step": 2, "event_name": "registration_started", "label": "Started Registration"},
252
+ {"step": 3, "event_name": "avatar_selected", "label": "Selected Avatar"},
253
+ {"step": 4, "event_name": "registration_completed", "label": "Completed Registration"}
254
+ ]'::jsonb,
255
+ 24
256
+ ),
257
+ (
258
+ 'first_bet',
259
+ 'First Bet',
260
+ 'Track users from viewing games to placing their first bet',
261
+ '[
262
+ {"step": 1, "event_name": "game_viewed", "label": "Viewed Game"},
263
+ {"step": 2, "event_name": "game_create_started", "label": "Started Creating Game"},
264
+ {"step": 3, "event_name": "team_selected", "label": "Selected Team"},
265
+ {"step": 4, "event_name": "wager_amount_set", "label": "Set Wager Amount"},
266
+ {"step": 5, "event_name": "transaction_signed", "label": "Signed Transaction"},
267
+ {"step": 6, "event_name": "game_created", "label": "Game Created"}
268
+ ]'::jsonb,
269
+ 2
270
+ ),
271
+ (
272
+ 'sports_betting',
273
+ 'Sports Betting Flow',
274
+ 'Track the sports betting user journey',
275
+ '[
276
+ {"step": 1, "event_name": "page_viewed", "label": "Viewed Sports Tab", "filter": {"pageName": "sports"}},
277
+ {"step": 2, "event_name": "game_card_clicked", "label": "Clicked Game Card"},
278
+ {"step": 3, "event_name": "bet_modal_opened", "label": "Opened Bet Modal"},
279
+ {"step": 4, "event_name": "team_selected", "label": "Selected Team"},
280
+ {"step": 5, "event_name": "bet_confirmed", "label": "Confirmed Bet"}
281
+ ]'::jsonb,
282
+ 1
283
+ ),
284
+ (
285
+ 'social_engagement',
286
+ 'Social Engagement',
287
+ 'Track users engaging with social features',
288
+ '[
289
+ {"step": 1, "event_name": "chat_opened", "label": "Opened Chat"},
290
+ {"step": 2, "event_name": "chat_message_sent", "label": "Sent Message"},
291
+ {"step": 3, "event_name": "friend_request_sent", "label": "Sent Friend Request"},
292
+ {"step": 4, "event_name": "friend_request_accepted", "label": "Friend Request Accepted"}
293
+ ]'::jsonb,
294
+ 168
295
+ ),
296
+ (
297
+ 'referral',
298
+ 'Referral Flow',
299
+ 'Track referral program engagement',
300
+ '[
301
+ {"step": 1, "event_name": "referral_code_generated", "label": "Generated Code"},
302
+ {"step": 2, "event_name": "referral_link_copied", "label": "Copied Link"},
303
+ {"step": 3, "event_name": "referral_code_used", "label": "Code Used by Friend"},
304
+ {"step": 4, "event_name": "referral_badge_earned", "label": "Badge Earned"}
305
+ ]'::jsonb,
306
+ 720
307
+ )
308
+ ON CONFLICT (funnel_id) DO NOTHING;
309
+
310
+ -- ============================================
311
+ -- 7. HELPER FUNCTIONS
312
+ -- ============================================
313
+
314
+ -- Function to update session summary on new event
315
+ CREATE OR REPLACE FUNCTION update_session_on_event()
316
+ RETURNS TRIGGER AS $$
317
+ BEGIN
318
+ INSERT INTO session_summaries (session_id, user_id, started_at, last_activity_at, event_count)
319
+ VALUES (NEW.session_id, NEW.user_id, NEW.server_timestamp, NEW.server_timestamp, 1)
320
+ ON CONFLICT (session_id) DO UPDATE SET
321
+ user_id = COALESCE(session_summaries.user_id, EXCLUDED.user_id),
322
+ last_activity_at = EXCLUDED.last_activity_at,
323
+ event_count = session_summaries.event_count + 1,
324
+ page_views = session_summaries.page_views + CASE WHEN NEW.event_name = 'page_viewed' THEN 1 ELSE 0 END,
325
+ games_viewed = session_summaries.games_viewed + CASE WHEN NEW.event_name = 'game_viewed' THEN 1 ELSE 0 END,
326
+ games_created = session_summaries.games_created + CASE WHEN NEW.event_name = 'game_created' THEN 1 ELSE 0 END,
327
+ games_joined = session_summaries.games_joined + CASE WHEN NEW.event_name = 'game_joined' THEN 1 ELSE 0 END,
328
+ messages_sent = session_summaries.messages_sent + CASE WHEN NEW.event_name = 'chat_message_sent' THEN 1 ELSE 0 END,
329
+ updated_at = NOW();
330
+
331
+ RETURN NEW;
332
+ END;
333
+ $$ LANGUAGE plpgsql;
334
+
335
+ -- Create trigger (only if session_id is provided)
336
+ DROP TRIGGER IF EXISTS trigger_update_session ON events;
337
+ CREATE TRIGGER trigger_update_session
338
+ AFTER INSERT ON events
339
+ FOR EACH ROW
340
+ WHEN (NEW.session_id IS NOT NULL)
341
+ EXECUTE FUNCTION update_session_on_event();
342
+
343
+ -- Function to update user analytics on event
344
+ CREATE OR REPLACE FUNCTION update_user_analytics_on_event()
345
+ RETURNS TRIGGER AS $$
346
+ BEGIN
347
+ INSERT INTO user_analytics (user_id, first_seen_at, last_seen_at, total_events)
348
+ VALUES (NEW.user_id, NEW.server_timestamp, NEW.server_timestamp, 1)
349
+ ON CONFLICT (user_id) DO UPDATE SET
350
+ last_seen_at = EXCLUDED.last_seen_at,
351
+ total_events = user_analytics.total_events + 1,
352
+ total_page_views = user_analytics.total_page_views + CASE WHEN NEW.event_name = 'page_viewed' THEN 1 ELSE 0 END,
353
+ total_games_created = user_analytics.total_games_created + CASE WHEN NEW.event_name = 'game_created' THEN 1 ELSE 0 END,
354
+ total_games_joined = user_analytics.total_games_joined + CASE WHEN NEW.event_name = 'game_joined' THEN 1 ELSE 0 END,
355
+ total_messages_sent = user_analytics.total_messages_sent + CASE WHEN NEW.event_name = 'chat_message_sent' THEN 1 ELSE 0 END,
356
+ days_since_last_activity = 0,
357
+ updated_at = NOW();
358
+
359
+ RETURN NEW;
360
+ END;
361
+ $$ LANGUAGE plpgsql;
362
+
363
+ -- Create trigger (only if user_id is provided)
364
+ DROP TRIGGER IF EXISTS trigger_update_user_analytics ON events;
365
+ CREATE TRIGGER trigger_update_user_analytics
366
+ AFTER INSERT ON events
367
+ FOR EACH ROW
368
+ WHEN (NEW.user_id IS NOT NULL)
369
+ EXECUTE FUNCTION update_user_analytics_on_event();
370
+
371
+ -- ============================================
372
+ -- 8. VIEWS FOR COMMON QUERIES
373
+ -- ============================================
374
+
375
+ -- Daily Active Users view
376
+ CREATE OR REPLACE VIEW v_daily_active_users AS
377
+ SELECT
378
+ date_partition as date,
379
+ COUNT(DISTINCT user_id) as dau,
380
+ COUNT(DISTINCT session_id) as sessions,
381
+ COUNT(*) as total_events
382
+ FROM events
383
+ WHERE user_id IS NOT NULL
384
+ GROUP BY date_partition
385
+ ORDER BY date_partition DESC;
386
+
387
+ -- Events summary by category
388
+ CREATE OR REPLACE VIEW v_events_by_category AS
389
+ SELECT
390
+ date_partition as date,
391
+ event_category,
392
+ event_name,
393
+ COUNT(*) as event_count,
394
+ COUNT(DISTINCT user_id) as unique_users
395
+ FROM events
396
+ GROUP BY date_partition, event_category, event_name
397
+ ORDER BY date_partition DESC, event_count DESC;
398
+
399
+ -- Session conversion summary
400
+ CREATE OR REPLACE VIEW v_session_conversions AS
401
+ SELECT
402
+ DATE(started_at) as date,
403
+ platform,
404
+ COUNT(*) as total_sessions,
405
+ COUNT(CASE WHEN converted THEN 1 END) as converted_sessions,
406
+ ROUND(100.0 * COUNT(CASE WHEN converted THEN 1 END) / NULLIF(COUNT(*), 0), 2) as conversion_rate,
407
+ AVG(duration_seconds) as avg_session_duration
408
+ FROM session_summaries
409
+ GROUP BY DATE(started_at), platform
410
+ ORDER BY DATE(started_at) DESC;
411
+
412
+ -- ============================================
413
+ -- MIGRATION COMPLETE
414
+ -- ============================================
415
+ -- Next steps:
416
+ -- 1. Create TypeScript types matching this schema
417
+ -- 2. Build AnalyticsService on frontend
418
+ -- 3. Build analytics routes on backend
419
+ -- 4. Start tracking events!
420
+
421
+
422
+
@@ -0,0 +1,14 @@
1
+ -- Migration 002: Add matchup_image_url column to games table
2
+ -- This column stores the S3 URL for pre-generated matchup images
3
+
4
+ ALTER TABLE games ADD COLUMN IF NOT EXISTS matchup_image_url TEXT;
5
+
6
+ -- Optional: Add an index if you'll be querying by this column
7
+ -- CREATE INDEX IF NOT EXISTS idx_games_matchup_image_url ON games(matchup_image_url);
8
+
9
+ -- Verify the column was added
10
+ SELECT column_name, data_type
11
+ FROM information_schema.columns
12
+ WHERE table_name = 'games' AND column_name = 'matchup_image_url';
13
+
14
+
@@ -0,0 +1,208 @@
1
+ -- ═══════════════════════════════════════════════════════════════════════════
2
+ -- REFERRAL EARNINGS SYSTEM
3
+ -- ═══════════════════════════════════════════════════════════════════════════
4
+ --
5
+ -- This migration creates the referral earnings tracking system.
6
+ --
7
+ -- ⚠️ IMPORTANT: Commission goes to the GAME CREATOR's referrer ONLY!
8
+ --
9
+ -- The person who referred the GAME CREATOR earns 1% of the ENTIRE POT.
10
+ -- Other players' referrers do NOT earn anything from the game.
11
+ --
12
+ -- Fee Structure:
13
+ -- Current: 5% Operator + 1% Oracle = 6% Total
14
+ -- Proposed: 4% Operator + 1% Referrer + 1% Oracle = 6% Total
15
+ --
16
+ -- If game creator has no referrer, the 1% goes to the operator (5% total).
17
+ --
18
+ -- Example (10-player game, 0.5 SOL buy-in each = 5 SOL pot):
19
+ -- - Game creator was referred by UserX
20
+ -- - UserX earns 0.05 SOL (1% of 5 SOL pot)
21
+ -- - Operator gets 0.20 SOL (4% of 5 SOL) instead of 0.25 SOL (5%)
22
+ --
23
+ -- ═══════════════════════════════════════════════════════════════════════════
24
+
25
+ -- Main earnings table - tracks each individual earning event
26
+ CREATE TABLE IF NOT EXISTS referral_earnings (
27
+ id SERIAL PRIMARY KEY,
28
+
29
+ -- Relationship: Referrer (who earns) and Referee (who played)
30
+ referrer_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
31
+ referrer_wallet VARCHAR(44) NOT NULL,
32
+ referee_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
33
+ referee_wallet VARCHAR(44) NOT NULL,
34
+
35
+ -- Game details
36
+ game_id VARCHAR(255) NOT NULL,
37
+ game_type VARCHAR(50) NOT NULL, -- 'sports', 'billiards', 'jackpot'
38
+
39
+ -- Financial details (all in lamports)
40
+ pot_size BIGINT NOT NULL, -- Total pot of the game
41
+ referee_buy_in BIGINT NOT NULL, -- Amount referee wagered
42
+ referee_won BOOLEAN NOT NULL, -- Did referee win?
43
+ referee_payout BIGINT DEFAULT 0, -- Amount referee won (0 if lost)
44
+
45
+ -- Commission details
46
+ commission_rate DECIMAL(6,4) NOT NULL DEFAULT 0.0100, -- 1% = 0.0100
47
+ commission_amount BIGINT NOT NULL, -- Actual commission in lamports
48
+
49
+ -- Status and payout tracking
50
+ status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')),
51
+ created_at TIMESTAMP DEFAULT NOW(),
52
+ paid_at TIMESTAMP,
53
+ payout_tx_signature VARCHAR(128),
54
+ payout_batch_id INTEGER,
55
+ notes TEXT, -- Optional notes (e.g., "Paid on-chain during game resolution")
56
+
57
+ -- Prevent duplicate entries for same referee in same game
58
+ UNIQUE(referee_wallet, game_id)
59
+ );
60
+
61
+ -- Indexes for efficient queries
62
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_referrer ON referral_earnings(referrer_user_id, status);
63
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_referrer_wallet ON referral_earnings(referrer_wallet);
64
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_referee ON referral_earnings(referee_user_id);
65
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_game ON referral_earnings(game_id);
66
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_status ON referral_earnings(status);
67
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_created ON referral_earnings(created_at DESC);
68
+ CREATE INDEX IF NOT EXISTS idx_referral_earnings_pending ON referral_earnings(referrer_wallet) WHERE status = 'pending';
69
+
70
+ -- Payout batches table - tracks batch payouts to referrers
71
+ CREATE TABLE IF NOT EXISTS referral_payout_batches (
72
+ id SERIAL PRIMARY KEY,
73
+
74
+ -- Batch details
75
+ total_amount BIGINT NOT NULL, -- Total lamports in this batch
76
+ num_referrers INTEGER NOT NULL, -- Number of unique referrers paid
77
+ num_earnings INTEGER NOT NULL, -- Number of individual earning records
78
+
79
+ -- Transaction details
80
+ tx_signature VARCHAR(128),
81
+ status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
82
+
83
+ -- Timing
84
+ created_at TIMESTAMP DEFAULT NOW(),
85
+ processed_at TIMESTAMP,
86
+ completed_at TIMESTAMP,
87
+
88
+ -- Error tracking
89
+ error_message TEXT,
90
+ retry_count INTEGER DEFAULT 0
91
+ );
92
+
93
+ CREATE INDEX IF NOT EXISTS idx_payout_batches_status ON referral_payout_batches(status);
94
+
95
+ -- Aggregated summary view for quick dashboard stats
96
+ CREATE OR REPLACE VIEW referral_earnings_summary AS
97
+ SELECT
98
+ u.id as user_id,
99
+ u.wallet_address,
100
+ u.username,
101
+ u.my_referral_code,
102
+ COALESCE(COUNT(re.id), 0) as total_referral_games,
103
+ COALESCE(SUM(re.commission_amount), 0) as total_earned_lamports,
104
+ COALESCE(SUM(CASE WHEN re.status = 'pending' THEN re.commission_amount ELSE 0 END), 0) as pending_lamports,
105
+ COALESCE(SUM(CASE WHEN re.status = 'paid' THEN re.commission_amount ELSE 0 END), 0) as paid_lamports,
106
+ MAX(re.created_at) as last_earning_at
107
+ FROM users u
108
+ LEFT JOIN referral_earnings re ON u.wallet_address = re.referrer_wallet
109
+ WHERE u.my_referral_code IS NOT NULL
110
+ GROUP BY u.id, u.wallet_address, u.username, u.my_referral_code;
111
+
112
+ -- Function to calculate referral commission for a game
113
+ -- Call this after game resolution to record earnings for all players with referrers
114
+ CREATE OR REPLACE FUNCTION process_game_referral_commissions(
115
+ p_game_id VARCHAR(255),
116
+ p_game_type VARCHAR(50),
117
+ p_pot_size BIGINT,
118
+ p_commission_rate DECIMAL DEFAULT 0.0100
119
+ )
120
+ RETURNS INTEGER AS $$
121
+ DECLARE
122
+ v_count INTEGER := 0;
123
+ v_player RECORD;
124
+ BEGIN
125
+ -- For each player in the game who has a referrer
126
+ FOR v_player IN
127
+ SELECT
128
+ ugr.wallet_address as referee_wallet,
129
+ ugr.buy_in,
130
+ ugr.won_game,
131
+ ugr.amount_claimed,
132
+ u.id as referee_user_id,
133
+ u.referral_code,
134
+ referrer.id as referrer_user_id,
135
+ referrer.wallet_address as referrer_wallet
136
+ FROM user_game_refs ugr
137
+ JOIN users u ON ugr.wallet_address = u.wallet_address
138
+ JOIN users referrer ON u.referral_code = referrer.my_referral_code
139
+ WHERE ugr.game_id = p_game_id
140
+ AND u.referral_code IS NOT NULL
141
+ AND referrer.my_referral_code IS NOT NULL
142
+ LOOP
143
+ -- Calculate commission (1% of pot size, proportional to player's contribution)
144
+ -- Each player's referrer gets 1% of THAT PLAYER's buy-in
145
+ DECLARE
146
+ v_commission BIGINT;
147
+ BEGIN
148
+ -- Commission = 1% of referee's buy-in (in lamports)
149
+ v_commission := FLOOR(v_player.buy_in * 1000000000 * p_commission_rate);
150
+
151
+ -- Skip if commission is 0
152
+ IF v_commission > 0 THEN
153
+ -- Insert or update earning record
154
+ INSERT INTO referral_earnings (
155
+ referrer_user_id,
156
+ referrer_wallet,
157
+ referee_user_id,
158
+ referee_wallet,
159
+ game_id,
160
+ game_type,
161
+ pot_size,
162
+ referee_buy_in,
163
+ referee_won,
164
+ referee_payout,
165
+ commission_rate,
166
+ commission_amount,
167
+ status
168
+ ) VALUES (
169
+ v_player.referrer_user_id,
170
+ v_player.referrer_wallet,
171
+ v_player.referee_user_id,
172
+ v_player.referee_wallet,
173
+ p_game_id,
174
+ p_game_type,
175
+ p_pot_size,
176
+ FLOOR(v_player.buy_in * 1000000000), -- Convert SOL to lamports
177
+ COALESCE(v_player.won_game, false),
178
+ COALESCE(FLOOR(v_player.amount_claimed * 1000000000), 0),
179
+ p_commission_rate,
180
+ v_commission,
181
+ 'pending'
182
+ )
183
+ ON CONFLICT (referee_wallet, game_id) DO NOTHING;
184
+
185
+ v_count := v_count + 1;
186
+ END IF;
187
+ END;
188
+ END LOOP;
189
+
190
+ RETURN v_count;
191
+ END;
192
+ $$ LANGUAGE plpgsql;
193
+
194
+ -- Grant necessary permissions (if using different roles)
195
+ -- GRANT ALL ON referral_earnings TO your_app_role;
196
+ -- GRANT ALL ON referral_payout_batches TO your_app_role;
197
+ -- GRANT SELECT ON referral_earnings_summary TO your_app_role;
198
+
199
+ -- Log migration
200
+ DO $$
201
+ BEGIN
202
+ RAISE NOTICE '✅ Referral earnings tables created successfully';
203
+ RAISE NOTICE ' - referral_earnings: tracks individual commission events';
204
+ RAISE NOTICE ' - referral_payout_batches: tracks batch payouts';
205
+ RAISE NOTICE ' - referral_earnings_summary: aggregated view for dashboards';
206
+ RAISE NOTICE ' - process_game_referral_commissions(): function to process game commissions';
207
+ END $$;
208
+