@goscribe/server 1.1.7 → 1.3.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 (56) hide show
  1. package/.env.example +43 -0
  2. package/check-difficulty.cjs +14 -0
  3. package/check-questions.cjs +14 -0
  4. package/db-summary.cjs +22 -0
  5. package/dist/routers/auth.js +1 -1
  6. package/mcq-test.cjs +36 -0
  7. package/package.json +10 -2
  8. package/prisma/migrations/20260413143206_init/migration.sql +873 -0
  9. package/prisma/schema.prisma +485 -292
  10. package/src/context.ts +4 -1
  11. package/src/lib/activity_human_description.test.ts +28 -0
  12. package/src/lib/activity_human_description.ts +239 -0
  13. package/src/lib/activity_log_service.test.ts +37 -0
  14. package/src/lib/activity_log_service.ts +353 -0
  15. package/src/lib/ai-session.ts +194 -112
  16. package/src/lib/constants.ts +14 -0
  17. package/src/lib/email.ts +230 -0
  18. package/src/lib/env.ts +23 -6
  19. package/src/lib/inference.ts +3 -3
  20. package/src/lib/logger.ts +26 -9
  21. package/src/lib/notification-service.test.ts +106 -0
  22. package/src/lib/notification-service.ts +677 -0
  23. package/src/lib/prisma.ts +6 -1
  24. package/src/lib/pusher.ts +90 -6
  25. package/src/lib/retry.ts +61 -0
  26. package/src/lib/storage.ts +2 -2
  27. package/src/lib/stripe.ts +39 -0
  28. package/src/lib/subscription_service.ts +722 -0
  29. package/src/lib/usage_service.ts +74 -0
  30. package/src/lib/worksheet-generation.test.ts +31 -0
  31. package/src/lib/worksheet-generation.ts +139 -0
  32. package/src/lib/workspace-access.ts +13 -0
  33. package/src/routers/_app.ts +11 -0
  34. package/src/routers/admin.ts +710 -0
  35. package/src/routers/annotations.ts +227 -0
  36. package/src/routers/auth.ts +432 -33
  37. package/src/routers/copilot.ts +719 -0
  38. package/src/routers/flashcards.ts +207 -80
  39. package/src/routers/members.ts +280 -80
  40. package/src/routers/notifications.ts +142 -0
  41. package/src/routers/payment.ts +448 -0
  42. package/src/routers/podcast.ts +133 -108
  43. package/src/routers/studyguide.ts +80 -74
  44. package/src/routers/worksheets.ts +300 -80
  45. package/src/routers/workspace.ts +538 -328
  46. package/src/scripts/purge-deleted-users.ts +167 -0
  47. package/src/server.ts +140 -12
  48. package/src/services/flashcard-progress.service.ts +52 -43
  49. package/src/trpc.ts +184 -5
  50. package/test-generate.js +30 -0
  51. package/test-ratio.cjs +9 -0
  52. package/zod-test.cjs +22 -0
  53. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
  54. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
  55. package/prisma/seed.mjs +0 -135
  56. package/src/routers/meetingsummary.ts +0 -416
@@ -5,11 +5,11 @@ generator client {
5
5
  datasource db {
6
6
  provider = "postgresql"
7
7
  url = env("DATABASE_URL")
8
- directUrl = env("DIRECT_URL") // for shadow db in migrations
8
+ directUrl = env("DIRECT_URL")
9
9
  }
10
10
 
11
11
  //
12
- // Enums
12
+ // Enums (match prisma1/migrations enum DDL)
13
13
  //
14
14
  enum ArtifactType {
15
15
  STUDY_GUIDE
@@ -17,6 +17,7 @@ enum ArtifactType {
17
17
  WORKSHEET
18
18
  MEETING_SUMMARY
19
19
  PODCAST_EPISODE
20
+ STORAGE
20
21
  }
21
22
 
22
23
  enum Difficulty {
@@ -34,56 +35,132 @@ enum QuestionType {
34
35
  FILL_IN_THE_BLANK
35
36
  }
36
37
 
38
+ enum CopilotMessageRole {
39
+ USER
40
+ ASSISTANT
41
+ }
42
+
43
+ enum NotificationPriority {
44
+ LOW
45
+ NORMAL
46
+ HIGH
47
+ }
48
+
49
+ enum InvoiceType {
50
+ SUBSCRIPTION
51
+ TOPUP
52
+ }
53
+
54
+ enum ActivityLogCategory {
55
+ AUTH
56
+ WORKSPACE
57
+ BILLING
58
+ ADMIN
59
+ CONTENT
60
+ SYSTEM
61
+ }
62
+
63
+ enum ActivityLogStatus {
64
+ SUCCESS
65
+ FAILURE
66
+ }
67
+
37
68
  //
38
69
  // NextAuth-compatible auth models (minimal)
39
70
  //
40
71
  model User {
41
- id String @id @default(cuid())
42
- name String?
43
- email String? @unique
44
- emailVerified DateTime?
45
- passwordHash String? // for credentials login
46
- profilePicture FileAsset? @relation(fields: [fileAssetId], references: [id])
47
- session Session[]
48
-
49
- // Ownership
50
- folders Folder[] @relation("UserFolders")
51
- workspaces Workspace[] @relation("UserWorkspaces")
52
- invitedInWorkspaces Workspace[] @relation("WorkspaceSharedWith") // many-to-many (deprecated)
53
- workspaceMemberships WorkspaceMember[] // proper member management
54
- uploads FileAsset[] @relation("UserUploads")
55
- artifacts Artifact[] @relation("UserArtifacts")
56
- versions ArtifactVersion[] @relation("UserArtifactVersions")
57
-
58
- // Progress tracking
72
+ id String @id @default(cuid())
73
+ name String?
74
+ email String? @unique
75
+ emailVerified DateTime?
76
+ passwordHash String?
77
+ createdAt DateTime @default(now())
78
+ updatedAt DateTime @updatedAt
79
+ fileAssetId String?
80
+ roleId String?
81
+ deletedAt DateTime?
82
+ stripe_customer_id String? @unique
83
+ artifacts Artifact[] @relation("UserArtifacts")
84
+ versions ArtifactVersion[] @relation("UserArtifactVersions")
85
+ chats Chat[]
86
+ uploads FileAsset[] @relation("UserUploads")
59
87
  flashcardProgress FlashcardProgress[] @relation("UserFlashcardProgress")
88
+ folders Folder[] @relation("UserFolders")
89
+ workspaces Workspace[] @relation("UserWorkspaces")
90
+ workspaceMemberships WorkspaceMember[]
91
+ invitedInWorkspaces Workspace[] @relation("WorkspaceSharedWith")
92
+ notifications Notification[]
93
+ actorNotifications Notification[] @relation("NotificationActor")
94
+ session Session[]
95
+ studyGuideComments StudyGuideComment[] @relation("UserStudyGuideComments")
96
+ highlights StudyGuideHighlight[] @relation("UserHighlights")
97
+ profilePicture FileAsset? @relation(fields: [fileAssetId], references: [id])
98
+ role Role? @relation(fields: [roleId], references: [id])
60
99
  worksheetQuestionProgress WorksheetQuestionProgress[]
100
+ worksheetPresets WorksheetPreset[]
101
+ copilotConversations CopilotConversation[]
102
+ sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
103
+ subscriptions Subscription[]
104
+ invoices Invoice[]
105
+ credits UserCredit[]
106
+ idempotencyRecords IdempotencyRecord[]
107
+ activityLogs ActivityLog[] @relation("ActivityLogActor")
108
+ passwordResetTokens PasswordResetToken[]
109
+ }
61
110
 
62
- // Invitations
63
- sentInvitations WorkspaceInvitation[] @relation("UserInvitations")
111
+ /// One-time tokens for forgot-password flow (token stored as SHA-256 hash of the secret from the email link).
112
+ model PasswordResetToken {
113
+ id String @id @default(cuid())
114
+ userId String
115
+ tokenHash String @unique
116
+ expiresAt DateTime
117
+ usedAt DateTime?
118
+ createdAt DateTime @default(now())
119
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
64
120
 
65
- notifications Notification[]
66
- chats Chat[]
67
- createdAt DateTime @default(now())
68
- updatedAt DateTime @updatedAt
69
- fileAssetId String?
121
+ @@index([userId])
122
+ }
123
+
124
+ model Role {
125
+ id String @id @default(cuid())
126
+ name String @unique
127
+ users User[]
70
128
  }
71
129
 
72
130
  model Notification {
73
- id String @id @default(cuid())
74
- userId String
75
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
76
- content String
77
- read Boolean @default(false)
78
- createdAt DateTime @default(now())
79
- updatedAt DateTime @updatedAt
131
+ id String @id @default(cuid())
132
+ userId String
133
+ actorUserId String?
134
+ workspaceId String?
135
+ type String
136
+ title String
137
+ body String
138
+ content String?
139
+ actionUrl String?
140
+ metadata Json?
141
+ priority NotificationPriority @default(NORMAL)
142
+ sourceId String?
143
+ read Boolean @default(false)
144
+ readAt DateTime?
145
+ deliveredAt DateTime?
146
+ createdAt DateTime @default(now())
147
+ updatedAt DateTime @updatedAt
148
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
149
+ actor User? @relation("NotificationActor", fields: [actorUserId], references: [id], onDelete: SetNull)
150
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: SetNull)
151
+
152
+ @@index([userId, read, createdAt(sort: Desc)])
153
+ @@index([userId, createdAt(sort: Desc)])
154
+ @@index([type, createdAt(sort: Desc)])
155
+ @@index([workspaceId])
156
+ @@unique([userId, type, sourceId])
80
157
  }
81
158
 
82
159
  model Session {
83
160
  id String @id @default(cuid())
84
161
  userId String
85
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
86
162
  expires DateTime
163
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
87
164
  }
88
165
 
89
166
  model VerificationToken {
@@ -94,66 +171,49 @@ model VerificationToken {
94
171
  @@unique([identifier, token])
95
172
  }
96
173
 
97
- //
98
- // Filesystem-like structure
99
- //
100
174
  model Folder {
101
- id String @id @default(cuid())
102
- name String
103
- ownerId String
104
- owner User @relation("UserFolders", fields: [ownerId], references: [id], onDelete: Cascade)
105
-
106
- // Nested folders
107
- parentId String?
108
- parent Folder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
109
- children Folder[] @relation("FolderChildren")
110
- color String @default("#9D00FF")
111
-
112
- // Files (workspaces) inside folders
113
- workspaces Workspace[]
114
-
115
- // Metadata
116
- createdAt DateTime @default(now())
117
- updatedAt DateTime @updatedAt
175
+ id String @id @default(cuid())
176
+ name String
177
+ ownerId String
178
+ parentId String?
179
+ createdAt DateTime @default(now())
180
+ updatedAt DateTime @updatedAt
181
+ color String @default("#9D00FF")
182
+ markerColor String?
183
+ owner User @relation("UserFolders", fields: [ownerId], references: [id], onDelete: Cascade)
184
+ parent Folder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
185
+ children Folder[] @relation("FolderChildren")
186
+ workspaces Workspace[]
118
187
 
119
- // Helpful composite index: folders per owner + parent
120
188
  @@index([ownerId, parentId])
121
189
  }
122
190
 
123
191
  model Workspace {
124
- id String @id @default(cuid())
125
- title String
126
- description String? // optional notes/description for the "file"
127
- ownerId String
128
- owner User @relation("UserWorkspaces", fields: [ownerId], references: [id], onDelete: Cascade)
129
- icon String @default("📄")
130
- color String @default("#9D00FF")
131
-
132
- // A workspace (file) lives in a folder (nullable = root)
133
- folderId String?
134
- folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
135
-
136
- channels Channel[]
137
-
138
- sharedWith User[] @relation("WorkspaceSharedWith") // many-to-many for sharing (deprecated)
139
- members WorkspaceMember[] // proper member management with roles
140
- fileBeingAnalyzed Boolean @default(false)
141
-
142
- analysisProgress Json?
143
-
144
- needsAnalysis Boolean @default(false)
145
-
146
- // Raw uploads attached to this workspace
147
- uploads FileAsset[]
148
-
149
- // AI outputs for this workspace (study guides, flashcards, etc.)
150
- artifacts Artifact[]
151
-
152
- // Invitations
153
- invitations WorkspaceInvitation[]
154
-
155
- createdAt DateTime @default(now())
156
- updatedAt DateTime @updatedAt
192
+ id String @id @default(cuid())
193
+ title String
194
+ description String?
195
+ ownerId String
196
+ owner User @relation("UserWorkspaces", fields: [ownerId], references: [id], onDelete: Cascade)
197
+ icon String @default("📄")
198
+ color String @default("#9D00FF")
199
+ markerColor String?
200
+ folderId String?
201
+ folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
202
+ channels Channel[]
203
+ copilotConversations CopilotConversation[]
204
+ sharedWith User[] @relation("WorkspaceSharedWith")
205
+ members WorkspaceMember[]
206
+ fileBeingAnalyzed Boolean @default(false)
207
+ analysisProgress Json?
208
+ needsAnalysis Boolean @default(false)
209
+ uploads FileAsset[]
210
+ artifacts Artifact[]
211
+ worksheetPresets WorksheetPreset[]
212
+ invitations WorkspaceInvitation[]
213
+ notifications Notification[]
214
+ activityLogs ActivityLog[]
215
+ createdAt DateTime @default(now())
216
+ updatedAt DateTime @updatedAt
157
217
 
158
218
  @@index([ownerId, folderId])
159
219
  }
@@ -161,302 +221,435 @@ model Workspace {
161
221
  model Channel {
162
222
  id String @id @default(cuid())
163
223
  workspaceId String
224
+ name String
225
+ createdAt DateTime @default(now())
164
226
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
165
-
166
- name String
167
-
168
- createdAt DateTime @default(now())
169
- chats Chat[]
227
+ chats Chat[]
170
228
 
171
229
  @@index([workspaceId])
172
230
  }
173
231
 
174
232
  model Chat {
175
- id String @id @default(cuid())
233
+ id String @id @default(cuid())
176
234
  channelId String
177
- channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
178
-
179
- user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
180
- userId String?
181
- message String // chat message content
182
-
235
+ userId String?
236
+ message String
183
237
  updatedAt DateTime @updatedAt
184
238
  createdAt DateTime @default(now())
239
+ channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
240
+ user User? @relation(fields: [userId], references: [id])
185
241
 
186
242
  @@index([channelId, createdAt])
187
243
  }
188
244
 
245
+ model CopilotConversation {
246
+ id String @id @default(cuid())
247
+ workspaceId String
248
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
249
+ userId String
250
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
251
+ title String @default("New Chat")
252
+ messages CopilotMessage[]
253
+ createdAt DateTime @default(now())
254
+ updatedAt DateTime @updatedAt
255
+
256
+ @@index([workspaceId, userId, updatedAt])
257
+ }
258
+
259
+ model CopilotMessage {
260
+ id String @id @default(cuid())
261
+ conversationId String
262
+ conversation CopilotConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
263
+ role CopilotMessageRole
264
+ content String
265
+ createdAt DateTime @default(now())
266
+
267
+ @@index([conversationId, createdAt])
268
+ }
269
+
189
270
  //
190
271
  // User uploads (source materials for AI)
191
272
  //
192
273
  model FileAsset {
193
- id String @id @default(cuid())
194
- workspaceId String?
195
- workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
196
-
197
- userId String?
198
- user User? @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
199
-
274
+ id String @id @default(cuid())
275
+ workspaceId String?
276
+ userId String?
200
277
  name String
201
278
  mimeType String
202
279
  size Int
203
280
  bucket String?
204
281
  objectKey String?
205
- url String? // optional if serving via signed GET per-view
206
- checksum String? // optional server-side integrity
207
- aiTranscription Json? @default("{}")
208
-
209
- meta Json? // arbitrary metadata
210
-
211
- createdAt DateTime @default(now())
212
- User User[]
282
+ url String?
283
+ checksum String?
284
+ meta Json?
285
+ createdAt DateTime @default(now())
286
+ aiTranscription Json? @default("{}")
287
+ user User? @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
288
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
289
+ User User[]
213
290
 
214
291
  @@index([workspaceId])
215
292
  @@index([userId, createdAt])
216
293
  }
217
294
 
218
- //
219
- // AI Outputs (Artifacts) with Versioning
220
- // - One Artifact per output stream (e.g., one Study Guide, with many versions)
221
- // - Some artifact types (flashcards, worksheet) have child rows
222
- //
223
295
  model Artifact {
224
- id String @id @default(cuid())
225
- workspaceId String
226
- workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
227
-
228
- type ArtifactType
229
- title String
230
- isArchived Boolean @default(false)
231
-
232
- generating Boolean @default(false)
296
+ id String @id @default(cuid())
297
+ workspaceId String
298
+ type ArtifactType
299
+ title String
300
+ isArchived Boolean @default(false)
301
+ difficulty Difficulty?
302
+ estimatedTime String?
303
+ createdById String?
304
+ createdAt DateTime @default(now())
305
+ updatedAt DateTime @updatedAt
306
+ description String?
307
+ generating Boolean @default(false)
233
308
  generatingMetadata Json?
234
-
235
- // Worksheet-specific fields
236
- difficulty Difficulty? // only meaningful for WORKSHEET
237
- estimatedTime String? // only meaningful for WORKSHEET
238
-
239
- imageObjectKey String?
240
- description String?
241
-
242
- createdById String?
243
- createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id], onDelete: SetNull)
244
-
245
- versions ArtifactVersion[] // text/transcript versions etc.
246
- flashcards Flashcard[] // only meaningful for FLASHCARD_SET
247
- questions WorksheetQuestion[] // only meaningful for WORKSHEET
248
- podcastSegments PodcastSegment[] // only meaningful for PODCAST_EPISODE
249
-
250
- createdAt DateTime @default(now())
251
- updatedAt DateTime @updatedAt
309
+ worksheetConfig Json?
310
+ imageObjectKey String?
311
+ createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id])
312
+ workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
313
+ versions ArtifactVersion[]
314
+ flashcards Flashcard[]
315
+ podcastSegments PodcastSegment[]
316
+ questions WorksheetQuestion[]
252
317
 
253
318
  @@index([workspaceId, type])
254
319
  }
255
320
 
256
321
  model ArtifactVersion {
257
- id String @id @default(cuid())
258
- artifactId String
259
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
260
-
261
- // Plain text content (e.g., Study Guide body, Meeting Summary text, Podcast transcript)
262
- content String // rich text serialized as markdown/HTML stored as TEXT
263
-
264
- // For Podcast episodes or other media, store URLs / durations / etc. in data
265
- data Json? // e.g., { "audioUrl": "...", "durationSec": 312, "voice": "..." }
266
-
267
- // Version sequencing (auto-increment per artifact)
268
- version Int
269
-
322
+ id String @id @default(cuid())
323
+ artifactId String
324
+ content String
325
+ data Json?
326
+ version Int
270
327
  createdById String?
271
- createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id], onDelete: SetNull)
272
-
273
- createdAt DateTime @default(now())
328
+ createdAt DateTime @default(now())
329
+ artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
330
+ createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id])
331
+ highlights StudyGuideHighlight[]
274
332
 
275
- @@unique([artifactId, version]) // each artifact has 1,2,3...
333
+ @@unique([artifactId, version])
276
334
  @@index([artifactId])
277
335
  }
278
336
 
279
- //
280
- // Flashcards (child items of a FLASHCARD_SET Artifact)
281
- //
282
- model Flashcard {
283
- id String @id @default(cuid())
284
- artifactId String
285
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
286
-
287
- front String // question/term
288
- back String // answer/definition
289
- tags String[] // optional keywords
290
- order Int @default(0)
337
+ model StudyGuideHighlight {
338
+ id String @id @default(cuid())
339
+ artifactVersionId String
340
+ userId String
341
+ startOffset Int
342
+ endOffset Int
343
+ selectedText String
344
+ color String @default("#FBBF24")
345
+ createdAt DateTime @default(now())
346
+ updatedAt DateTime @updatedAt
347
+ comments StudyGuideComment[]
348
+ artifactVersion ArtifactVersion @relation(fields: [artifactVersionId], references: [id], onDelete: Cascade)
349
+ user User @relation("UserHighlights", fields: [userId], references: [id], onDelete: Cascade)
350
+
351
+ @@index([artifactVersionId, userId])
352
+ }
291
353
 
292
- // User progress tracking
293
- progress FlashcardProgress[]
354
+ model StudyGuideComment {
355
+ id String @id @default(cuid())
356
+ highlightId String
357
+ userId String
358
+ content String
359
+ createdAt DateTime @default(now())
360
+ updatedAt DateTime @updatedAt
361
+ highlight StudyGuideHighlight @relation(fields: [highlightId], references: [id], onDelete: Cascade)
362
+ user User @relation("UserStudyGuideComments", fields: [userId], references: [id], onDelete: Cascade)
363
+
364
+ @@index([highlightId])
365
+ }
294
366
 
295
- createdAt DateTime @default(now())
367
+ model Flashcard {
368
+ id String @id @default(cuid())
369
+ artifactId String
370
+ front String
371
+ back String
372
+ tags String[]
373
+ order Int @default(0)
374
+ createdAt DateTime @default(now())
375
+ acceptedAnswers String[] @default([])
376
+ artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
377
+ progress FlashcardProgress[]
296
378
 
297
379
  @@index([artifactId])
298
380
  }
299
381
 
300
- //
301
- // User Progress on Flashcards (spaced repetition, mastery tracking)
302
- //
303
382
  model FlashcardProgress {
304
- id String @id @default(cuid())
305
- userId String
306
- user User @relation("UserFlashcardProgress", fields: [userId], references: [id], onDelete: Cascade)
307
-
308
- flashcardId String
309
- flashcard Flashcard @relation(fields: [flashcardId], references: [id], onDelete: Cascade)
310
-
311
- // Study statistics
312
- timesStudied Int @default(0)
313
- timesCorrect Int @default(0)
314
- timesIncorrect Int @default(0)
315
- timesIncorrectConsecutive Int @default(0) // Track consecutive failures
316
-
317
- // Spaced repetition data
318
- easeFactor Float @default(2.5) // SM-2 algorithm ease factor
319
- interval Int @default(0) // Days until next review
320
- repetitions Int @default(0) // Consecutive correct answers
321
-
322
- // Mastery level (0-100)
323
- masteryLevel Int @default(0)
324
-
325
- // Timestamps
326
- lastStudiedAt DateTime?
327
- nextReviewAt DateTime?
328
-
329
- createdAt DateTime @default(now())
330
- updatedAt DateTime @updatedAt
383
+ id String @id @default(cuid())
384
+ userId String
385
+ flashcardId String
386
+ timesStudied Int @default(0)
387
+ timesCorrect Int @default(0)
388
+ timesIncorrect Int @default(0)
389
+ timesIncorrectConsecutive Int @default(0)
390
+ easeFactor Float @default(2.5)
391
+ interval Int @default(0)
392
+ repetitions Int @default(0)
393
+ masteryLevel Int @default(0)
394
+ lastStudiedAt DateTime?
395
+ nextReviewAt DateTime?
396
+ createdAt DateTime @default(now())
397
+ updatedAt DateTime @updatedAt
398
+ flashcard Flashcard @relation(fields: [flashcardId], references: [id], onDelete: Cascade)
399
+ user User @relation("UserFlashcardProgress", fields: [userId], references: [id], onDelete: Cascade)
331
400
 
332
401
  @@unique([userId, flashcardId])
333
402
  @@index([userId, nextReviewAt])
334
403
  @@index([flashcardId])
335
404
  }
336
405
 
337
- //
338
- // Worksheet Questions (child items of a WORKSHEET Artifact)
339
- //
340
406
  model WorksheetQuestion {
341
- id String @id @default(cuid())
407
+ id String @id @default(cuid())
342
408
  artifactId String
343
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
344
-
345
409
  prompt String
346
410
  answer String?
347
- type QuestionType @default(TEXT)
348
- difficulty Difficulty @default(MEDIUM)
349
- order Int @default(0)
350
-
351
- meta Json? // e.g., { "choices": ["A","B","C","D"], "correct": 1, "options": [...] }
352
-
353
- createdAt DateTime @default(now())
354
- progress WorksheetQuestionProgress[]
411
+ type QuestionType @default(TEXT)
412
+ difficulty Difficulty @default(MEDIUM)
413
+ order Int @default(0)
414
+ meta Json?
415
+ createdAt DateTime @default(now())
416
+ artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
417
+ progress WorksheetQuestionProgress[]
355
418
 
356
419
  @@index([artifactId])
357
420
  }
358
421
 
359
- //
360
- // Per-user progress for Worksheet Questions
361
- //
362
422
  model WorksheetQuestionProgress {
363
423
  id String @id @default(cuid())
364
424
  worksheetQuestionId String
425
+ userId String
426
+ userAnswer String?
427
+ completedAt DateTime?
428
+ attempts Int @default(0)
429
+ timeSpentSec Int?
430
+ meta Json?
431
+ createdAt DateTime @default(now())
432
+ updatedAt DateTime @updatedAt
433
+ correct Boolean? @default(false)
434
+ modified Boolean @default(false)
435
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
365
436
  worksheetQuestion WorksheetQuestion @relation(fields: [worksheetQuestionId], references: [id], onDelete: Cascade)
366
437
 
367
- userId String
368
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
369
-
370
- modified Boolean @default(false)
371
- userAnswer String?
372
- correct Boolean? @default(false)
373
- completedAt DateTime?
374
- attempts Int @default(0)
375
- timeSpentSec Int?
376
- meta Json?
377
-
378
- createdAt DateTime @default(now())
379
- updatedAt DateTime @updatedAt
380
-
381
438
  @@unique([worksheetQuestionId, userId])
382
439
  @@index([userId])
383
440
  }
384
441
 
385
- //
386
- // Workspace Members (with roles)
387
- //
442
+ model WorksheetPreset {
443
+ id String @id @default(cuid())
444
+ userId String?
445
+ workspaceId String?
446
+ name String
447
+ isSystem Boolean @default(false)
448
+ config Json
449
+ createdAt DateTime @default(now())
450
+ updatedAt DateTime @updatedAt
451
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
452
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
453
+
454
+ @@index([userId, workspaceId])
455
+ @@index([isSystem])
456
+ }
457
+
388
458
  model WorkspaceMember {
389
459
  id String @id @default(cuid())
390
460
  workspaceId String
461
+ userId String
462
+ role String @default("member")
463
+ joinedAt DateTime @default(now())
464
+ updatedAt DateTime @updatedAt
465
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
391
466
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
392
467
 
393
- userId String
394
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
395
-
396
- role String @default("member") // "owner", "admin", "member"
397
-
398
- joinedAt DateTime @default(now())
399
- updatedAt DateTime @updatedAt
400
-
401
- @@unique([workspaceId, userId]) // One membership per user per workspace
468
+ @@unique([workspaceId, userId])
402
469
  @@index([workspaceId])
403
470
  @@index([userId])
404
471
  }
405
472
 
406
- //
407
- // Workspace Invitations
408
- //
409
473
  model WorkspaceInvitation {
410
474
  id String @id @default(cuid())
411
475
  workspaceId String
476
+ email String
477
+ role String @default("member")
478
+ token String @unique @default(cuid())
479
+ invitedById String
480
+ acceptedAt DateTime?
481
+ expiresAt DateTime @default(dbgenerated("(now() + '7 days'::interval)"))
482
+ createdAt DateTime @default(now())
483
+ updatedAt DateTime @updatedAt
484
+ invitedBy User @relation("UserInvitations", fields: [invitedById], references: [id], onDelete: Cascade)
412
485
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
413
486
 
414
- email String
415
- role String @default("member") // "owner", "admin", "member"
416
- token String @unique @default(cuid()) // UUID for invitation link
487
+ @@unique([workspaceId, email])
488
+ @@index([token])
489
+ @@index([workspaceId])
490
+ }
417
491
 
418
- invitedById String
419
- invitedBy User @relation("UserInvitations", fields: [invitedById], references: [id], onDelete: Cascade)
492
+ model PodcastSegment {
493
+ id String @id @default(cuid())
494
+ artifactId String
495
+ title String
496
+ content String
497
+ startTime Int
498
+ duration Int
499
+ order Int
500
+ objectKey String?
501
+ audioUrl String?
502
+ keyPoints String[]
503
+ meta Json?
504
+ createdAt DateTime @default(now())
505
+ updatedAt DateTime @updatedAt
506
+ generating Boolean @default(false)
507
+ generatingMetadata Json?
508
+ artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
420
509
 
421
- acceptedAt DateTime?
422
- expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '7 days'"))
510
+ @@index([artifactId, order])
511
+ @@index([artifactId, startTime])
512
+ }
423
513
 
424
- createdAt DateTime @default(now())
425
- updatedAt DateTime @updatedAt
514
+ model Invoice {
515
+ id String @id @default(cuid())
516
+ userId String
517
+ subscriptionId String?
518
+ stripeInvoiceId String @unique
519
+ amountPaid Int
520
+ status String
521
+ invoicePdfUrl String?
522
+ hostedInvoiceUrl String?
523
+ paidAt DateTime?
524
+ type InvoiceType @default(SUBSCRIPTION)
525
+ createdAt DateTime @default(now())
526
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
527
+ subscription Subscription? @relation(fields: [subscriptionId], references: [id])
426
528
 
427
- @@unique([workspaceId, email]) // One invitation per email per workspace
428
- @@index([token])
429
- @@index([workspaceId])
529
+ @@index([userId])
430
530
  }
431
531
 
432
- //
433
- // Podcast Segments (child items of a PODCAST_EPISODE Artifact)
434
- //
435
- model PodcastSegment {
436
- id String @id @default(cuid())
437
- artifactId String
438
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
532
+ model Plan {
533
+ id String @id @default(cuid())
534
+ name String
535
+ description String?
536
+ type String
537
+ price Int
538
+ stripePriceId String
539
+ interval String?
540
+ active Boolean @default(true)
541
+ createdAt DateTime @default(now())
542
+ limit PlanLimit?
543
+ subscriptions Subscription[]
544
+ }
439
545
 
440
- title String
441
- content String // Full text content of the segment
442
- startTime Int // Start time in seconds
443
- duration Int // Duration in seconds
444
- order Int // Display order within the episode
546
+ model PlanLimit {
547
+ id String @id @default(cuid())
548
+ planId String @unique
549
+ maxStorageBytes BigInt @default(0)
550
+ maxWorksheets Int @default(0)
551
+ maxFlashcards Int @default(0)
552
+ maxPodcasts Int @default(0)
553
+ maxStudyGuides Int @default(0)
554
+ createdAt DateTime @default(now())
555
+ updatedAt DateTime @updatedAt
556
+ plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
557
+ }
445
558
 
446
- // Audio file reference
447
- objectKey String? // Google Cloud Storage object key
448
- audioUrl String? // Cached signed URL (temporary)
559
+ model Subscription {
560
+ id String @id @default(cuid())
561
+ userId String
562
+ planId String
563
+ stripeSubscriptionId String @unique
564
+ stripeCustomerId String?
565
+ status String
566
+ currentPeriodStart DateTime
567
+ currentPeriodEnd DateTime
568
+ cancelAt DateTime?
569
+ canceledAt DateTime?
570
+ createdAt DateTime @default(now())
571
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
572
+ plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
573
+ invoices Invoice[]
449
574
 
450
- // Metadata
451
- keyPoints String[] // Array of key points
452
- meta Json? // Additional metadata (voice settings, etc.)
575
+ @@index([userId])
576
+ @@index([planId])
577
+ }
453
578
 
454
- generating Boolean @default(false)
455
- generatingMetadata Json? // Additional metadata (voice settings, etc.)
579
+ model ResourcePrice {
580
+ id String @id @default(cuid())
581
+ resourceType ArtifactType @unique
582
+ priceCents Int
583
+ updatedAt DateTime @updatedAt
584
+ }
456
585
 
457
- createdAt DateTime @default(now())
458
- updatedAt DateTime @updatedAt
586
+ model UserCredit {
587
+ id String @id @default(cuid())
588
+ userId String
589
+ resourceType ArtifactType
590
+ amount Int @default(1)
591
+ stripeSessionId String @unique
592
+ createdAt DateTime @default(now())
593
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
594
+
595
+ @@index([userId])
596
+ }
459
597
 
460
- @@index([artifactId, order]) // For efficient ordering
461
- @@index([artifactId, startTime]) // For time-based queries
598
+ model IdempotencyRecord {
599
+ id String @id @default(cuid())
600
+ userId String
601
+ planId String? // The plan being purchased
602
+ resourceType ArtifactType? // The resource for top-ups
603
+ stripeSessionId String? // The Stripe Checkout Session ID
604
+ status String @default("pending") // pending, completed, failed, expired
605
+ activeLockKey String? @unique // Unique lock: "user_id_plan_id" (Nullified when done)
606
+
607
+ createdAt DateTime @default(now())
608
+ updatedAt DateTime @updatedAt
609
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
610
+
611
+ @@index([userId, status])
612
+ @@index([activeLockKey])
462
613
  }
614
+
615
+ model StripeEvent {
616
+ id String @id @default(cuid())
617
+ stripeEventId String @unique // Stripe event ID (e.g. evt_...)
618
+ type String // Event type (e.g. checkout.session.completed)
619
+ status String @default("pending") // pending, processed, failed
620
+ error String? // Error message if processing fails
621
+ processedAt DateTime?
622
+ createdAt DateTime @default(now())
623
+ updatedAt DateTime @updatedAt
624
+
625
+ @@index([stripeEventId])
626
+ }
627
+
628
+ /// Append-only system / user activity (tRPC and explicit events). Retention via admin or scheduled job.
629
+ model ActivityLog {
630
+ id String @id @default(cuid())
631
+ createdAt DateTime @default(now())
632
+ actorUserId String?
633
+ actor User? @relation("ActivityLogActor", fields: [actorUserId], references: [id], onDelete: SetNull)
634
+ actorEmailSnapshot String?
635
+ action String
636
+ category ActivityLogCategory
637
+ resourceType String?
638
+ resourceId String?
639
+ workspaceId String?
640
+ workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: SetNull)
641
+ trpcPath String?
642
+ httpMethod String?
643
+ status ActivityLogStatus
644
+ errorCode String?
645
+ durationMs Int
646
+ ipAddress String?
647
+ userAgent String?
648
+ metadata Json?
649
+
650
+ @@index([createdAt(sort: Desc)])
651
+ @@index([actorUserId, createdAt(sort: Desc)])
652
+ @@index([workspaceId, createdAt(sort: Desc)])
653
+ @@index([category, createdAt(sort: Desc)])
654
+ }
655
+