@opprs/db-prisma 2.2.1-canary.ccb79aa → 2.2.1-canary.cd8b178

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opprs/db-prisma",
3
- "version": "2.2.1-canary.ccb79aa",
3
+ "version": "2.2.1-canary.cd8b178",
4
4
  "description": "Database backend for OPPR (Open Pinball Player Ranking System) using Prisma and PostgreSQL",
5
5
  "keywords": [
6
6
  "oppr",
@@ -56,7 +56,7 @@
56
56
  "vitest": "^4.0.16"
57
57
  },
58
58
  "peerDependencies": {
59
- "@opprs/core": "^2.2.1-canary.ccb79aa"
59
+ "@opprs/core": "^2.2.1-canary.cd8b178"
60
60
  },
61
61
  "engines": {
62
62
  "node": ">=20.9.0"
@@ -0,0 +1,45 @@
1
+ -- CreateTable
2
+ CREATE TABLE "Location" (
3
+ "id" TEXT NOT NULL,
4
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
5
+ "updatedAt" TIMESTAMP(3) NOT NULL,
6
+ "externalId" TEXT,
7
+ "name" TEXT NOT NULL,
8
+ "address" TEXT,
9
+ "city" TEXT,
10
+ "state" TEXT,
11
+ "country" TEXT,
12
+
13
+ CONSTRAINT "Location_pkey" PRIMARY KEY ("id")
14
+ );
15
+
16
+ -- CreateIndex
17
+ CREATE UNIQUE INDEX "Location_externalId_key" ON "Location"("externalId");
18
+
19
+ -- CreateIndex
20
+ CREATE INDEX "Location_externalId_idx" ON "Location"("externalId");
21
+
22
+ -- CreateIndex
23
+ CREATE INDEX "Location_name_idx" ON "Location"("name");
24
+
25
+ -- CreateIndex
26
+ CREATE INDEX "Location_city_idx" ON "Location"("city");
27
+
28
+ -- AlterTable: Add new columns to Tournament
29
+ ALTER TABLE "Tournament" ADD COLUMN "description" VARCHAR(2000);
30
+ ALTER TABLE "Tournament" ADD COLUMN "locationId" TEXT;
31
+ ALTER TABLE "Tournament" ADD COLUMN "organizerId" TEXT;
32
+
33
+ -- DropColumn: Remove old location string column (replaced by Location relation)
34
+ ALTER TABLE "Tournament" DROP COLUMN IF EXISTS "location";
35
+
36
+ -- CreateIndex for new Tournament columns
37
+ CREATE INDEX "Tournament_locationId_idx" ON "Tournament"("locationId");
38
+
39
+ CREATE INDEX "Tournament_organizerId_idx" ON "Tournament"("organizerId");
40
+
41
+ -- AddForeignKey: Tournament -> Location
42
+ ALTER TABLE "Tournament" ADD CONSTRAINT "Tournament_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
43
+
44
+ -- AddForeignKey: Tournament -> Player (organizer)
45
+ ALTER TABLE "Tournament" ADD CONSTRAINT "Tournament_organizerId_fkey" FOREIGN KEY ("organizerId") REFERENCES "Player"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,19 @@
1
+ /*
2
+ Warnings:
3
+
4
+ - You are about to drop the column `email` on the `Player` table. All the data in the column will be lost.
5
+
6
+ */
7
+ -- DropIndex
8
+ DROP INDEX "Player_email_idx";
9
+
10
+ -- DropIndex
11
+ DROP INDEX "Player_email_key";
12
+
13
+ -- AlterTable
14
+ ALTER TABLE "Player" DROP COLUMN "email";
15
+
16
+ -- AlterTable
17
+ ALTER TABLE "User" ADD COLUMN "codeOfConductAcceptedAt" TIMESTAMP(3),
18
+ ADD COLUMN "privacyPolicyAcceptedAt" TIMESTAMP(3),
19
+ ADD COLUMN "tosAcceptedAt" TIMESTAMP(3);
@@ -0,0 +1,137 @@
1
+ /*
2
+ Warnings:
3
+
4
+ - You are about to drop the `TournamentResult` table. If the table is not empty, all the data it contains will be lost.
5
+
6
+ */
7
+ -- CreateEnum
8
+ CREATE TYPE "MatchResult" AS ENUM ('WIN', 'LOSS', 'TIE');
9
+
10
+ -- DropForeignKey
11
+ ALTER TABLE "TournamentResult" DROP CONSTRAINT "TournamentResult_playerId_fkey";
12
+
13
+ -- DropForeignKey
14
+ ALTER TABLE "TournamentResult" DROP CONSTRAINT "TournamentResult_tournamentId_fkey";
15
+
16
+ -- DropTable
17
+ DROP TABLE "TournamentResult";
18
+
19
+ -- CreateTable
20
+ CREATE TABLE "Round" (
21
+ "id" TEXT NOT NULL,
22
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
23
+ "updatedAt" TIMESTAMP(3) NOT NULL,
24
+ "tournamentId" TEXT NOT NULL,
25
+ "number" INTEGER NOT NULL,
26
+ "name" TEXT,
27
+ "isFinals" BOOLEAN NOT NULL DEFAULT false,
28
+
29
+ CONSTRAINT "Round_pkey" PRIMARY KEY ("id")
30
+ );
31
+
32
+ -- CreateTable
33
+ CREATE TABLE "Match" (
34
+ "id" TEXT NOT NULL,
35
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
36
+ "updatedAt" TIMESTAMP(3) NOT NULL,
37
+ "tournamentId" TEXT NOT NULL,
38
+ "roundId" TEXT,
39
+ "number" INTEGER,
40
+ "machineName" TEXT,
41
+
42
+ CONSTRAINT "Match_pkey" PRIMARY KEY ("id")
43
+ );
44
+
45
+ -- CreateTable
46
+ CREATE TABLE "Entry" (
47
+ "id" TEXT NOT NULL,
48
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
49
+ "updatedAt" TIMESTAMP(3) NOT NULL,
50
+ "matchId" TEXT NOT NULL,
51
+ "playerId" TEXT NOT NULL,
52
+ "result" "MatchResult" NOT NULL,
53
+ "position" INTEGER,
54
+
55
+ CONSTRAINT "Entry_pkey" PRIMARY KEY ("id")
56
+ );
57
+
58
+ -- CreateTable
59
+ CREATE TABLE "Standing" (
60
+ "id" TEXT NOT NULL,
61
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
62
+ "updatedAt" TIMESTAMP(3) NOT NULL,
63
+ "tournamentId" TEXT NOT NULL,
64
+ "playerId" TEXT NOT NULL,
65
+ "position" INTEGER NOT NULL,
66
+ "isFinals" BOOLEAN NOT NULL DEFAULT false,
67
+ "optedOut" BOOLEAN NOT NULL DEFAULT false,
68
+ "linearPoints" DOUBLE PRECISION DEFAULT 0,
69
+ "dynamicPoints" DOUBLE PRECISION DEFAULT 0,
70
+ "totalPoints" DOUBLE PRECISION,
71
+ "ageInDays" INTEGER DEFAULT 0,
72
+ "decayMultiplier" DOUBLE PRECISION DEFAULT 1.0,
73
+ "decayedPoints" DOUBLE PRECISION,
74
+ "efficiency" DOUBLE PRECISION,
75
+
76
+ CONSTRAINT "Standing_pkey" PRIMARY KEY ("id")
77
+ );
78
+
79
+ -- CreateIndex
80
+ CREATE INDEX "Round_tournamentId_idx" ON "Round"("tournamentId");
81
+
82
+ -- CreateIndex
83
+ CREATE INDEX "Round_tournamentId_isFinals_idx" ON "Round"("tournamentId", "isFinals");
84
+
85
+ -- CreateIndex
86
+ CREATE UNIQUE INDEX "Round_tournamentId_number_isFinals_key" ON "Round"("tournamentId", "number", "isFinals");
87
+
88
+ -- CreateIndex
89
+ CREATE INDEX "Match_tournamentId_idx" ON "Match"("tournamentId");
90
+
91
+ -- CreateIndex
92
+ CREATE INDEX "Match_roundId_idx" ON "Match"("roundId");
93
+
94
+ -- CreateIndex
95
+ CREATE INDEX "Entry_matchId_idx" ON "Entry"("matchId");
96
+
97
+ -- CreateIndex
98
+ CREATE INDEX "Entry_playerId_idx" ON "Entry"("playerId");
99
+
100
+ -- CreateIndex
101
+ CREATE UNIQUE INDEX "Entry_matchId_playerId_key" ON "Entry"("matchId", "playerId");
102
+
103
+ -- CreateIndex
104
+ CREATE INDEX "Standing_playerId_idx" ON "Standing"("playerId");
105
+
106
+ -- CreateIndex
107
+ CREATE INDEX "Standing_tournamentId_idx" ON "Standing"("tournamentId");
108
+
109
+ -- CreateIndex
110
+ CREATE INDEX "Standing_tournamentId_isFinals_idx" ON "Standing"("tournamentId", "isFinals");
111
+
112
+ -- CreateIndex
113
+ CREATE INDEX "Standing_position_idx" ON "Standing"("position");
114
+
115
+ -- CreateIndex
116
+ CREATE UNIQUE INDEX "Standing_playerId_tournamentId_isFinals_key" ON "Standing"("playerId", "tournamentId", "isFinals");
117
+
118
+ -- AddForeignKey
119
+ ALTER TABLE "Round" ADD CONSTRAINT "Round_tournamentId_fkey" FOREIGN KEY ("tournamentId") REFERENCES "Tournament"("id") ON DELETE CASCADE ON UPDATE CASCADE;
120
+
121
+ -- AddForeignKey
122
+ ALTER TABLE "Match" ADD CONSTRAINT "Match_tournamentId_fkey" FOREIGN KEY ("tournamentId") REFERENCES "Tournament"("id") ON DELETE CASCADE ON UPDATE CASCADE;
123
+
124
+ -- AddForeignKey
125
+ ALTER TABLE "Match" ADD CONSTRAINT "Match_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
126
+
127
+ -- AddForeignKey
128
+ ALTER TABLE "Entry" ADD CONSTRAINT "Entry_matchId_fkey" FOREIGN KEY ("matchId") REFERENCES "Match"("id") ON DELETE CASCADE ON UPDATE CASCADE;
129
+
130
+ -- AddForeignKey
131
+ ALTER TABLE "Entry" ADD CONSTRAINT "Entry_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE CASCADE ON UPDATE CASCADE;
132
+
133
+ -- AddForeignKey
134
+ ALTER TABLE "Standing" ADD CONSTRAINT "Standing_tournamentId_fkey" FOREIGN KEY ("tournamentId") REFERENCES "Tournament"("id") ON DELETE CASCADE ON UPDATE CASCADE;
135
+
136
+ -- AddForeignKey
137
+ ALTER TABLE "Standing" ADD CONSTRAINT "Standing_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,108 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "OpprRankingChangeType" AS ENUM ('INITIAL', 'TOURNAMENT_RESULT', 'RANKING_REFRESH', 'RD_DECAY', 'MANUAL_ADJUSTMENT');
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "OpprPlayerRanking" (
6
+ "id" TEXT NOT NULL,
7
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8
+ "updatedAt" TIMESTAMP(3) NOT NULL,
9
+ "playerId" TEXT NOT NULL,
10
+ "rating" DOUBLE PRECISION NOT NULL DEFAULT 1500,
11
+ "ratingDeviation" DOUBLE PRECISION NOT NULL DEFAULT 200,
12
+ "lastRatingUpdate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
+ "ranking" INTEGER,
14
+ "isRated" BOOLEAN NOT NULL DEFAULT false,
15
+
16
+ CONSTRAINT "OpprPlayerRanking_pkey" PRIMARY KEY ("id")
17
+ );
18
+
19
+ -- CreateTable
20
+ CREATE TABLE "OpprRankingHistory" (
21
+ "id" TEXT NOT NULL,
22
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
23
+ "opprPlayerRankingId" TEXT NOT NULL,
24
+ "rating" DOUBLE PRECISION NOT NULL,
25
+ "ratingDeviation" DOUBLE PRECISION NOT NULL,
26
+ "ranking" INTEGER,
27
+ "isRated" BOOLEAN NOT NULL,
28
+ "changeType" "OpprRankingChangeType" NOT NULL,
29
+ "tournamentId" TEXT,
30
+ "notes" VARCHAR(500),
31
+
32
+ CONSTRAINT "OpprRankingHistory_pkey" PRIMARY KEY ("id")
33
+ );
34
+
35
+ -- CreateIndex
36
+ CREATE UNIQUE INDEX "OpprPlayerRanking_playerId_key" ON "OpprPlayerRanking"("playerId");
37
+
38
+ -- CreateIndex
39
+ CREATE INDEX "OpprPlayerRanking_playerId_idx" ON "OpprPlayerRanking"("playerId");
40
+
41
+ -- CreateIndex
42
+ CREATE INDEX "OpprPlayerRanking_rating_idx" ON "OpprPlayerRanking"("rating");
43
+
44
+ -- CreateIndex
45
+ CREATE INDEX "OpprPlayerRanking_ranking_idx" ON "OpprPlayerRanking"("ranking");
46
+
47
+ -- CreateIndex
48
+ CREATE INDEX "OpprPlayerRanking_isRated_idx" ON "OpprPlayerRanking"("isRated");
49
+
50
+ -- CreateIndex
51
+ CREATE INDEX "OpprRankingHistory_opprPlayerRankingId_idx" ON "OpprRankingHistory"("opprPlayerRankingId");
52
+
53
+ -- CreateIndex
54
+ CREATE INDEX "OpprRankingHistory_createdAt_idx" ON "OpprRankingHistory"("createdAt");
55
+
56
+ -- CreateIndex
57
+ CREATE INDEX "OpprRankingHistory_tournamentId_idx" ON "OpprRankingHistory"("tournamentId");
58
+
59
+ -- CreateIndex
60
+ CREATE INDEX "OpprRankingHistory_opprPlayerRankingId_createdAt_idx" ON "OpprRankingHistory"("opprPlayerRankingId", "createdAt");
61
+
62
+ -- AddForeignKey
63
+ ALTER TABLE "OpprPlayerRanking" ADD CONSTRAINT "OpprPlayerRanking_playerId_fkey" FOREIGN KEY ("playerId") REFERENCES "Player"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64
+
65
+ -- AddForeignKey
66
+ ALTER TABLE "OpprRankingHistory" ADD CONSTRAINT "OpprRankingHistory_opprPlayerRankingId_fkey" FOREIGN KEY ("opprPlayerRankingId") REFERENCES "OpprPlayerRanking"("id") ON DELETE CASCADE ON UPDATE CASCADE;
67
+
68
+ -- AddForeignKey
69
+ ALTER TABLE "OpprRankingHistory" ADD CONSTRAINT "OpprRankingHistory_tournamentId_fkey" FOREIGN KEY ("tournamentId") REFERENCES "Tournament"("id") ON DELETE SET NULL ON UPDATE CASCADE;
70
+
71
+ -- Migrate existing Player rating data to OpprPlayerRanking
72
+ INSERT INTO "OpprPlayerRanking" ("id", "createdAt", "updatedAt", "playerId", "rating", "ratingDeviation", "lastRatingUpdate", "ranking", "isRated")
73
+ SELECT
74
+ gen_random_uuid()::text,
75
+ NOW(),
76
+ NOW(),
77
+ "id",
78
+ "rating",
79
+ "ratingDeviation",
80
+ "lastRatingUpdate",
81
+ "ranking",
82
+ "isRated"
83
+ FROM "Player";
84
+
85
+ -- Create initial history records for all migrated rankings
86
+ INSERT INTO "OpprRankingHistory" ("id", "createdAt", "opprPlayerRankingId", "rating", "ratingDeviation", "ranking", "isRated", "changeType", "notes")
87
+ SELECT
88
+ gen_random_uuid()::text,
89
+ NOW(),
90
+ opr."id",
91
+ opr."rating",
92
+ opr."ratingDeviation",
93
+ opr."ranking",
94
+ opr."isRated",
95
+ 'INITIAL',
96
+ 'Migrated from Player model'
97
+ FROM "OpprPlayerRanking" opr;
98
+
99
+ -- Drop old indexes from Player table
100
+ DROP INDEX IF EXISTS "Player_rating_idx";
101
+ DROP INDEX IF EXISTS "Player_ranking_idx";
102
+
103
+ -- Remove old columns from Player table
104
+ ALTER TABLE "Player" DROP COLUMN "rating";
105
+ ALTER TABLE "Player" DROP COLUMN "ratingDeviation";
106
+ ALTER TABLE "Player" DROP COLUMN "ranking";
107
+ ALTER TABLE "Player" DROP COLUMN "isRated";
108
+ ALTER TABLE "Player" DROP COLUMN "lastRatingUpdate";
@@ -21,25 +21,19 @@ model Player {
21
21
  playerNumber Int @unique // 5-digit unique identifier (10000-99999)
22
22
  name String?
23
23
 
24
- // OPPR Rating fields
25
- rating Float @default(1500) // Glicko rating
26
- ratingDeviation Float @default(200) // Rating uncertainty (RD)
27
- ranking Int? // World ranking position
28
- isRated Boolean @default(false) // Has 5+ events
24
+ // General player statistics
29
25
  eventCount Int @default(0) // Number of events participated
30
-
31
- // Timestamps for rating calculations
32
- lastRatingUpdate DateTime @default(now())
33
26
  lastEventDate DateTime?
34
27
 
35
28
  // Relations
36
- tournamentResults TournamentResult[]
37
- user User?
29
+ standings Standing[]
30
+ entries Entry[]
31
+ user User?
32
+ organizedTournaments Tournament[] @relation("OrganizedTournaments")
33
+ opprRanking OpprPlayerRanking?
38
34
 
39
35
  @@index([externalId])
40
36
  @@index([playerNumber])
41
- @@index([rating])
42
- @@index([ranking])
43
37
  }
44
38
 
45
39
  // Tournament model - represents a pinball tournament event
@@ -51,9 +45,17 @@ model Tournament {
51
45
  // Tournament identification
52
46
  externalId String? @unique // External ID from OPPR or other systems
53
47
  name String
54
- location String?
48
+ description String? @db.VarChar(2000)
55
49
  date DateTime
56
50
 
51
+ // Location relation
52
+ locationId String?
53
+ location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
54
+
55
+ // Organizer relation
56
+ organizerId String?
57
+ organizer Player? @relation("OrganizedTournaments", fields: [organizerId], references: [id], onDelete: SetNull)
58
+
57
59
  // Tournament configuration (stored as JSON)
58
60
  // Contains TGPConfig structure from OPPR
59
61
  tgpConfig Json?
@@ -72,48 +74,117 @@ model Tournament {
72
74
  firstPlaceValue Float?
73
75
 
74
76
  // Relations
75
- results TournamentResult[]
77
+ rounds Round[]
78
+ matches Match[]
79
+ standings Standing[]
80
+ rankingHistoryRecords OpprRankingHistory[]
76
81
 
77
82
  @@index([date])
78
83
  @@index([eventBooster])
79
84
  @@index([externalId])
85
+ @@index([locationId])
86
+ @@index([organizerId])
80
87
  }
81
88
 
82
- // Tournament Result - junction table linking players to tournaments
83
- model TournamentResult {
84
- id String @id @default(cuid())
85
- createdAt DateTime @default(now())
86
- updatedAt DateTime @updatedAt
89
+ // Round model - groups matches within a tournament stage
90
+ model Round {
91
+ id String @id @default(cuid())
92
+ createdAt DateTime @default(now())
93
+ updatedAt DateTime @updatedAt
94
+
95
+ tournamentId String
96
+ tournament Tournament @relation(fields: [tournamentId], references: [id], onDelete: Cascade)
97
+
98
+ number Int // Round number within the stage (1, 2, 3...)
99
+ name String? // Optional name (e.g., "Quarterfinals", "Semifinal")
100
+ isFinals Boolean @default(false)
87
101
 
88
102
  // Relations
89
- playerId String
90
- player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
91
- tournamentId String
92
- tournament Tournament @relation(fields: [tournamentId], references: [id], onDelete: Cascade)
103
+ matches Match[]
93
104
 
94
- // Result data
95
- position Int // Finishing position (1 = first place)
96
- optedOut Boolean @default(false)
105
+ @@unique([tournamentId, number, isFinals])
106
+ @@index([tournamentId])
107
+ @@index([tournamentId, isFinals])
108
+ }
97
109
 
98
- // Points awarded
99
- linearPoints Float? @default(0) // Linear distribution points
100
- dynamicPoints Float? @default(0) // Dynamic distribution points
101
- totalPoints Float? // Total points (linear + dynamic)
110
+ // Match model - a single game with 1-4 players
111
+ model Match {
112
+ id String @id @default(cuid())
113
+ createdAt DateTime @default(now())
114
+ updatedAt DateTime @updatedAt
102
115
 
103
- // Time decay tracking
104
- ageInDays Int? @default(0) // Age of event in days at calculation time
105
- decayMultiplier Float? @default(1.0) // Time decay multiplier (1.0, 0.75, 0.5, or 0.0)
106
- decayedPoints Float? // Points after applying decay (defaults to totalPoints via application logic)
116
+ tournamentId String
117
+ tournament Tournament @relation(fields: [tournamentId], references: [id], onDelete: Cascade)
118
+ roundId String?
119
+ round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
107
120
 
108
- // Efficiency tracking
109
- efficiency Float? // Performance efficiency percentage
121
+ number Int? // Match number within the round
122
+ machineName String? // Machine played on
123
+
124
+ // Relations
125
+ entries Entry[]
110
126
 
111
- @@unique([playerId, tournamentId])
127
+ @@index([tournamentId])
128
+ @@index([roundId])
129
+ }
130
+
131
+ // Entry model - a player's participation in a match
132
+ model Entry {
133
+ id String @id @default(cuid())
134
+ createdAt DateTime @default(now())
135
+ updatedAt DateTime @updatedAt
136
+
137
+ matchId String
138
+ match Match @relation(fields: [matchId], references: [id], onDelete: Cascade)
139
+ playerId String
140
+ player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
141
+
142
+ result MatchResult // WIN, LOSS, TIE
143
+ position Int? // Position within the match (1st, 2nd, 3rd, 4th for group games)
144
+
145
+ @@unique([matchId, playerId])
146
+ @@index([matchId])
147
+ @@index([playerId])
148
+ }
149
+
150
+ // Standing model - final position for qualifying or finals
151
+ model Standing {
152
+ id String @id @default(cuid())
153
+ createdAt DateTime @default(now())
154
+ updatedAt DateTime @updatedAt
155
+
156
+ tournamentId String
157
+ tournament Tournament @relation(fields: [tournamentId], references: [id], onDelete: Cascade)
158
+ playerId String
159
+ player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
160
+
161
+ position Int // Finishing position (1 = first place)
162
+ isFinals Boolean @default(false)
163
+ optedOut Boolean @default(false)
164
+
165
+ // Points (calculated from merged standings)
166
+ linearPoints Float? @default(0)
167
+ dynamicPoints Float? @default(0)
168
+ totalPoints Float?
169
+ ageInDays Int? @default(0)
170
+ decayMultiplier Float? @default(1.0)
171
+ decayedPoints Float?
172
+ efficiency Float?
173
+
174
+ @@unique([playerId, tournamentId, isFinals])
112
175
  @@index([playerId])
113
176
  @@index([tournamentId])
177
+ @@index([tournamentId, isFinals])
114
178
  @@index([position])
115
179
  }
116
180
 
181
+ // Enum for match results
182
+ enum MatchResult {
183
+ WIN
184
+ LOSS
185
+ TIE
186
+ }
187
+
117
188
  // Enum for event booster types
118
189
  enum EventBoosterType {
119
190
  NONE
@@ -149,5 +220,122 @@ model User {
149
220
  // Session management (for token revocation)
150
221
  refreshTokenHash String?
151
222
 
223
+ // Policy acceptance timestamps (null = not accepted)
224
+ tosAcceptedAt DateTime?
225
+ privacyPolicyAcceptedAt DateTime?
226
+ codeOfConductAcceptedAt DateTime?
227
+
228
+ // API Keys
229
+ apiKeys ApiKey[]
230
+
152
231
  @@index([email])
153
232
  }
233
+
234
+ // ApiKey model - represents an API key for programmatic access
235
+ model ApiKey {
236
+ id String @id @default(cuid())
237
+ createdAt DateTime @default(now())
238
+ updatedAt DateTime @updatedAt
239
+
240
+ // Key identification
241
+ name String // User-provided name (e.g., "CI Pipeline", "Mobile App")
242
+ keyPrefix String // First 14 characters of the key for display/lookup
243
+ keyHash String // bcrypt hash of the full key
244
+
245
+ // Ownership
246
+ userId String
247
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
248
+
249
+ // Lifecycle
250
+ expiresAt DateTime? // Optional expiration date
251
+ lastUsedAt DateTime? // Updated on each successful authentication
252
+
253
+ @@index([userId])
254
+ @@index([keyPrefix])
255
+ }
256
+
257
+ // Location model - represents a venue where tournaments are held
258
+ model Location {
259
+ id String @id @default(cuid())
260
+ createdAt DateTime @default(now())
261
+ updatedAt DateTime @updatedAt
262
+
263
+ externalId String? @unique
264
+ name String
265
+ address String?
266
+ city String?
267
+ state String?
268
+ country String?
269
+
270
+ // Relations
271
+ tournaments Tournament[]
272
+
273
+ @@index([externalId])
274
+ @@index([name])
275
+ @@index([city])
276
+ }
277
+
278
+ // OPPR Player Ranking - OPPR-specific rating and ranking data
279
+ // Separated from Player to allow for future alternative ranking systems
280
+ model OpprPlayerRanking {
281
+ id String @id @default(cuid())
282
+ createdAt DateTime @default(now())
283
+ updatedAt DateTime @updatedAt
284
+
285
+ // Relation to Player (one-to-one)
286
+ playerId String @unique
287
+ player Player @relation(fields: [playerId], references: [id], onDelete: Cascade)
288
+
289
+ // Glicko Rating fields
290
+ rating Float @default(1500) // Glicko rating
291
+ ratingDeviation Float @default(200) // Rating uncertainty (RD)
292
+ lastRatingUpdate DateTime @default(now())
293
+
294
+ // World Ranking fields
295
+ ranking Int? // World ranking position (1 = best)
296
+ isRated Boolean @default(false) // Has 5+ events (eligible for ranking)
297
+
298
+ // Relations
299
+ history OpprRankingHistory[]
300
+
301
+ @@index([playerId])
302
+ @@index([rating])
303
+ @@index([ranking])
304
+ @@index([isRated])
305
+ }
306
+
307
+ // OPPR Ranking History - Historical record of ranking/rating changes
308
+ model OpprRankingHistory {
309
+ id String @id @default(cuid())
310
+ createdAt DateTime @default(now())
311
+
312
+ // Relation to OpprPlayerRanking
313
+ opprPlayerRankingId String
314
+ opprPlayerRanking OpprPlayerRanking @relation(fields: [opprPlayerRankingId], references: [id], onDelete: Cascade)
315
+
316
+ // Snapshot of values at this point in time
317
+ rating Float
318
+ ratingDeviation Float
319
+ ranking Int?
320
+ isRated Boolean
321
+
322
+ // Context for the change
323
+ changeType OpprRankingChangeType
324
+ tournamentId String?
325
+ tournament Tournament? @relation(fields: [tournamentId], references: [id], onDelete: SetNull)
326
+ notes String? @db.VarChar(500)
327
+
328
+ @@index([opprPlayerRankingId])
329
+ @@index([createdAt])
330
+ @@index([tournamentId])
331
+ @@index([opprPlayerRankingId, createdAt])
332
+ }
333
+
334
+ // Enum for tracking what caused the ranking change
335
+ enum OpprRankingChangeType {
336
+ INITIAL // First ranking record created
337
+ TOURNAMENT_RESULT // Rating updated after tournament
338
+ RANKING_REFRESH // Periodic ranking recalculation
339
+ RD_DECAY // RD increased due to inactivity
340
+ MANUAL_ADJUSTMENT // Administrative correction
341
+ }