@goscribe/server 1.2.0 → 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 (48) hide show
  1. package/check-difficulty.cjs +14 -0
  2. package/check-questions.cjs +14 -0
  3. package/db-summary.cjs +22 -0
  4. package/mcq-test.cjs +36 -0
  5. package/package.json +9 -2
  6. package/prisma/migrations/20260413143206_init/migration.sql +873 -0
  7. package/prisma/schema.prisma +471 -324
  8. package/src/context.ts +4 -1
  9. package/src/lib/activity_human_description.test.ts +28 -0
  10. package/src/lib/activity_human_description.ts +239 -0
  11. package/src/lib/activity_log_service.test.ts +37 -0
  12. package/src/lib/activity_log_service.ts +353 -0
  13. package/src/lib/ai-session.ts +79 -51
  14. package/src/lib/email.ts +213 -29
  15. package/src/lib/env.ts +23 -6
  16. package/src/lib/inference.ts +2 -2
  17. package/src/lib/notification-service.test.ts +106 -0
  18. package/src/lib/notification-service.ts +677 -0
  19. package/src/lib/prisma.ts +6 -1
  20. package/src/lib/pusher.ts +86 -2
  21. package/src/lib/stripe.ts +39 -0
  22. package/src/lib/subscription_service.ts +722 -0
  23. package/src/lib/usage_service.ts +74 -0
  24. package/src/lib/worksheet-generation.test.ts +31 -0
  25. package/src/lib/worksheet-generation.ts +139 -0
  26. package/src/routers/_app.ts +9 -0
  27. package/src/routers/admin.ts +710 -0
  28. package/src/routers/annotations.ts +41 -0
  29. package/src/routers/auth.ts +338 -28
  30. package/src/routers/copilot.ts +719 -0
  31. package/src/routers/flashcards.ts +201 -68
  32. package/src/routers/members.ts +280 -80
  33. package/src/routers/notifications.ts +142 -0
  34. package/src/routers/payment.ts +448 -0
  35. package/src/routers/podcast.ts +112 -83
  36. package/src/routers/studyguide.ts +12 -0
  37. package/src/routers/worksheets.ts +289 -66
  38. package/src/routers/workspace.ts +329 -122
  39. package/src/scripts/purge-deleted-users.ts +167 -0
  40. package/src/server.ts +137 -11
  41. package/src/services/flashcard-progress.service.ts +49 -37
  42. package/src/trpc.ts +184 -5
  43. package/test-generate.js +30 -0
  44. package/test-ratio.cjs +9 -0
  45. package/zod-test.cjs +22 -0
  46. package/prisma/migrations/20250826124819_add_worksheet_difficulty_and_estimated_time/migration.sql +0 -213
  47. package/prisma/migrations/20250826133236_add_worksheet_question_progress/migration.sql +0 -31
  48. package/prisma/seed.mjs +0 -135
@@ -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,60 +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
- // Study guide annotations
66
- highlights StudyGuideHighlight[] @relation("UserHighlights")
67
- studyGuideComments StudyGuideComment[] @relation("UserStudyGuideComments")
121
+ @@index([userId])
122
+ }
68
123
 
69
- notifications Notification[]
70
- chats Chat[]
71
- createdAt DateTime @default(now())
72
- updatedAt DateTime @updatedAt
73
- fileAssetId String?
124
+ model Role {
125
+ id String @id @default(cuid())
126
+ name String @unique
127
+ users User[]
74
128
  }
75
129
 
76
130
  model Notification {
77
- id String @id @default(cuid())
78
- userId String
79
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
80
- content String
81
- read Boolean @default(false)
82
- createdAt DateTime @default(now())
83
- 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])
84
157
  }
85
158
 
86
159
  model Session {
87
160
  id String @id @default(cuid())
88
161
  userId String
89
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
90
162
  expires DateTime
163
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
91
164
  }
92
165
 
93
166
  model VerificationToken {
@@ -98,66 +171,49 @@ model VerificationToken {
98
171
  @@unique([identifier, token])
99
172
  }
100
173
 
101
- //
102
- // Filesystem-like structure
103
- //
104
174
  model Folder {
105
- id String @id @default(cuid())
106
- name String
107
- ownerId String
108
- owner User @relation("UserFolders", fields: [ownerId], references: [id], onDelete: Cascade)
109
-
110
- // Nested folders
111
- parentId String?
112
- parent Folder? @relation("FolderChildren", fields: [parentId], references: [id], onDelete: Cascade)
113
- children Folder[] @relation("FolderChildren")
114
- color String @default("#9D00FF")
115
-
116
- // Files (workspaces) inside folders
117
- workspaces Workspace[]
118
-
119
- // Metadata
120
- createdAt DateTime @default(now())
121
- 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[]
122
187
 
123
- // Helpful composite index: folders per owner + parent
124
188
  @@index([ownerId, parentId])
125
189
  }
126
190
 
127
191
  model Workspace {
128
- id String @id @default(cuid())
129
- title String
130
- description String? // optional notes/description for the "file"
131
- ownerId String
132
- owner User @relation("UserWorkspaces", fields: [ownerId], references: [id], onDelete: Cascade)
133
- icon String @default("📄")
134
- color String @default("#9D00FF")
135
-
136
- // A workspace (file) lives in a folder (nullable = root)
137
- folderId String?
138
- folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull)
139
-
140
- channels Channel[]
141
-
142
- sharedWith User[] @relation("WorkspaceSharedWith") // many-to-many for sharing (deprecated)
143
- members WorkspaceMember[] // proper member management with roles
144
- fileBeingAnalyzed Boolean @default(false)
145
-
146
- analysisProgress Json?
147
-
148
- needsAnalysis Boolean @default(false)
149
-
150
- // Raw uploads attached to this workspace
151
- uploads FileAsset[]
152
-
153
- // AI outputs for this workspace (study guides, flashcards, etc.)
154
- artifacts Artifact[]
155
-
156
- // Invitations
157
- invitations WorkspaceInvitation[]
158
-
159
- createdAt DateTime @default(now())
160
- 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
161
217
 
162
218
  @@index([ownerId, folderId])
163
219
  }
@@ -165,144 +221,132 @@ model Workspace {
165
221
  model Channel {
166
222
  id String @id @default(cuid())
167
223
  workspaceId String
224
+ name String
225
+ createdAt DateTime @default(now())
168
226
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
169
-
170
- name String
171
-
172
- createdAt DateTime @default(now())
173
- chats Chat[]
227
+ chats Chat[]
174
228
 
175
229
  @@index([workspaceId])
176
230
  }
177
231
 
178
232
  model Chat {
179
- id String @id @default(cuid())
233
+ id String @id @default(cuid())
180
234
  channelId String
181
- channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
182
-
183
- user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
184
- userId String?
185
- message String // chat message content
186
-
235
+ userId String?
236
+ message String
187
237
  updatedAt DateTime @updatedAt
188
238
  createdAt DateTime @default(now())
239
+ channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
240
+ user User? @relation(fields: [userId], references: [id])
189
241
 
190
242
  @@index([channelId, createdAt])
191
243
  }
192
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
+
193
270
  //
194
271
  // User uploads (source materials for AI)
195
272
  //
196
273
  model FileAsset {
197
- id String @id @default(cuid())
198
- workspaceId String?
199
- workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
200
-
201
- userId String?
202
- user User? @relation("UserUploads", fields: [userId], references: [id], onDelete: Cascade)
203
-
274
+ id String @id @default(cuid())
275
+ workspaceId String?
276
+ userId String?
204
277
  name String
205
278
  mimeType String
206
279
  size Int
207
280
  bucket String?
208
281
  objectKey String?
209
- url String? // optional if serving via signed GET per-view
210
- checksum String? // optional server-side integrity
211
- aiTranscription Json? @default("{}")
212
-
213
- meta Json? // arbitrary metadata
214
-
215
- createdAt DateTime @default(now())
216
- 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[]
217
290
 
218
291
  @@index([workspaceId])
219
292
  @@index([userId, createdAt])
220
293
  }
221
294
 
222
- //
223
- // AI Outputs (Artifacts) with Versioning
224
- // - One Artifact per output stream (e.g., one Study Guide, with many versions)
225
- // - Some artifact types (flashcards, worksheet) have child rows
226
- //
227
295
  model Artifact {
228
- id String @id @default(cuid())
229
- workspaceId String
230
- workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
231
-
232
- type ArtifactType
233
- title String
234
- isArchived Boolean @default(false)
235
-
236
- 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)
237
308
  generatingMetadata Json?
238
-
239
- // Worksheet-specific fields
240
- difficulty Difficulty? // only meaningful for WORKSHEET
241
- estimatedTime String? // only meaningful for WORKSHEET
242
-
243
- imageObjectKey String?
244
- description String?
245
-
246
- createdById String?
247
- createdBy User? @relation("UserArtifacts", fields: [createdById], references: [id], onDelete: SetNull)
248
-
249
- versions ArtifactVersion[] // text/transcript versions etc.
250
- flashcards Flashcard[] // only meaningful for FLASHCARD_SET
251
- questions WorksheetQuestion[] // only meaningful for WORKSHEET
252
- podcastSegments PodcastSegment[] // only meaningful for PODCAST_EPISODE
253
-
254
- createdAt DateTime @default(now())
255
- 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[]
256
317
 
257
318
  @@index([workspaceId, type])
258
319
  }
259
320
 
260
321
  model ArtifactVersion {
261
- id String @id @default(cuid())
262
- artifactId String
263
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
264
-
265
- // Plain text content (e.g., Study Guide body, Meeting Summary text, Podcast transcript)
266
- content String // rich text serialized as markdown/HTML stored as TEXT
267
-
268
- // For Podcast episodes or other media, store URLs / durations / etc. in data
269
- data Json? // e.g., { "audioUrl": "...", "durationSec": 312, "voice": "..." }
270
-
271
- // Version sequencing (auto-increment per artifact)
272
- version Int
273
-
322
+ id String @id @default(cuid())
323
+ artifactId String
324
+ content String
325
+ data Json?
326
+ version Int
274
327
  createdById String?
275
- createdBy User? @relation("UserArtifactVersions", fields: [createdById], references: [id], onDelete: SetNull)
276
-
277
- // Highlights on this version
278
- highlights StudyGuideHighlight[]
279
-
280
- 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[]
281
332
 
282
- @@unique([artifactId, version]) // each artifact has 1,2,3...
333
+ @@unique([artifactId, version])
283
334
  @@index([artifactId])
284
335
  }
285
336
 
286
- //
287
- // Study Guide Highlights and Comments
288
- //
289
337
  model StudyGuideHighlight {
290
- id String @id @default(cuid())
338
+ id String @id @default(cuid())
291
339
  artifactVersionId String
292
- artifactVersion ArtifactVersion @relation(fields: [artifactVersionId], references: [id], onDelete: Cascade)
293
-
294
- userId String
295
- user User @relation("UserHighlights", fields: [userId], references: [id], onDelete: Cascade)
296
-
297
- startOffset Int // character offset in the content
298
- endOffset Int
299
- selectedText String // the highlighted text (preserved even if content changes)
300
- color String @default("#FBBF24") // highlight color
301
-
302
- comments StudyGuideComment[]
303
-
304
- createdAt DateTime @default(now())
305
- updatedAt DateTime @updatedAt
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)
306
350
 
307
351
  @@index([artifactVersionId, userId])
308
352
  }
@@ -310,199 +354,302 @@ model StudyGuideHighlight {
310
354
  model StudyGuideComment {
311
355
  id String @id @default(cuid())
312
356
  highlightId String
357
+ userId String
358
+ content String
359
+ createdAt DateTime @default(now())
360
+ updatedAt DateTime @updatedAt
313
361
  highlight StudyGuideHighlight @relation(fields: [highlightId], references: [id], onDelete: Cascade)
314
-
315
- userId String
316
- user User @relation("UserStudyGuideComments", fields: [userId], references: [id], onDelete: Cascade)
317
- content String
318
-
319
- createdAt DateTime @default(now())
320
- updatedAt DateTime @updatedAt
362
+ user User @relation("UserStudyGuideComments", fields: [userId], references: [id], onDelete: Cascade)
321
363
 
322
364
  @@index([highlightId])
323
365
  }
324
366
 
325
- //
326
- // Flashcards (child items of a FLASHCARD_SET Artifact)
327
- //
328
367
  model Flashcard {
329
- id String @id @default(cuid())
330
- artifactId String
331
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
332
-
333
- front String // question/term
334
- back String // answer/definition
335
- tags String[] // optional keywords
336
- order Int @default(0)
337
-
338
- // User progress tracking
339
- progress FlashcardProgress[]
340
-
341
- createdAt DateTime @default(now())
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[]
342
378
 
343
379
  @@index([artifactId])
344
380
  }
345
381
 
346
- //
347
- // User Progress on Flashcards (spaced repetition, mastery tracking)
348
- //
349
382
  model FlashcardProgress {
350
- id String @id @default(cuid())
351
- userId String
352
- user User @relation("UserFlashcardProgress", fields: [userId], references: [id], onDelete: Cascade)
353
-
354
- flashcardId String
355
- flashcard Flashcard @relation(fields: [flashcardId], references: [id], onDelete: Cascade)
356
-
357
- // Study statistics
358
- timesStudied Int @default(0)
359
- timesCorrect Int @default(0)
360
- timesIncorrect Int @default(0)
361
- timesIncorrectConsecutive Int @default(0) // Track consecutive failures
362
-
363
- // Spaced repetition data
364
- easeFactor Float @default(2.5) // SM-2 algorithm ease factor
365
- interval Int @default(0) // Days until next review
366
- repetitions Int @default(0) // Consecutive correct answers
367
-
368
- // Mastery level (0-100)
369
- masteryLevel Int @default(0)
370
-
371
- // Timestamps
372
- lastStudiedAt DateTime?
373
- nextReviewAt DateTime?
374
-
375
- createdAt DateTime @default(now())
376
- 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)
377
400
 
378
401
  @@unique([userId, flashcardId])
379
402
  @@index([userId, nextReviewAt])
380
403
  @@index([flashcardId])
381
404
  }
382
405
 
383
- //
384
- // Worksheet Questions (child items of a WORKSHEET Artifact)
385
- //
386
406
  model WorksheetQuestion {
387
- id String @id @default(cuid())
407
+ id String @id @default(cuid())
388
408
  artifactId String
389
- artifact Artifact @relation(fields: [artifactId], references: [id], onDelete: Cascade)
390
-
391
409
  prompt String
392
410
  answer String?
393
- type QuestionType @default(TEXT)
394
- difficulty Difficulty @default(MEDIUM)
395
- order Int @default(0)
396
-
397
- meta Json? // e.g., { "choices": ["A","B","C","D"], "correct": 1, "options": [...] }
398
-
399
- createdAt DateTime @default(now())
400
- 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[]
401
418
 
402
419
  @@index([artifactId])
403
420
  }
404
421
 
405
- //
406
- // Per-user progress for Worksheet Questions
407
- //
408
422
  model WorksheetQuestionProgress {
409
423
  id String @id @default(cuid())
410
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)
411
436
  worksheetQuestion WorksheetQuestion @relation(fields: [worksheetQuestionId], references: [id], onDelete: Cascade)
412
437
 
413
- userId String
414
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
415
-
416
- modified Boolean @default(false)
417
- userAnswer String?
418
- correct Boolean? @default(false)
419
- completedAt DateTime?
420
- attempts Int @default(0)
421
- timeSpentSec Int?
422
- meta Json?
423
-
424
- createdAt DateTime @default(now())
425
- updatedAt DateTime @updatedAt
426
-
427
438
  @@unique([worksheetQuestionId, userId])
428
439
  @@index([userId])
429
440
  }
430
441
 
431
- //
432
- // Workspace Members (with roles)
433
- //
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
+
434
458
  model WorkspaceMember {
435
459
  id String @id @default(cuid())
436
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)
437
466
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
438
467
 
439
- userId String
440
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
441
-
442
- role String @default("member") // "owner", "admin", "member"
443
-
444
- joinedAt DateTime @default(now())
445
- updatedAt DateTime @updatedAt
446
-
447
- @@unique([workspaceId, userId]) // One membership per user per workspace
468
+ @@unique([workspaceId, userId])
448
469
  @@index([workspaceId])
449
470
  @@index([userId])
450
471
  }
451
472
 
452
- //
453
- // Workspace Invitations
454
- //
455
473
  model WorkspaceInvitation {
456
474
  id String @id @default(cuid())
457
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)
458
485
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
459
486
 
460
- email String
461
- role String @default("member") // "owner", "admin", "member"
462
- token String @unique @default(cuid()) // UUID for invitation link
487
+ @@unique([workspaceId, email])
488
+ @@index([token])
489
+ @@index([workspaceId])
490
+ }
463
491
 
464
- invitedById String
465
- 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)
466
509
 
467
- acceptedAt DateTime?
468
- expiresAt DateTime @default(dbgenerated("NOW() + INTERVAL '7 days'"))
510
+ @@index([artifactId, order])
511
+ @@index([artifactId, startTime])
512
+ }
469
513
 
470
- createdAt DateTime @default(now())
471
- 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])
472
528
 
473
- @@unique([workspaceId, email]) // One invitation per email per workspace
474
- @@index([token])
475
- @@index([workspaceId])
529
+ @@index([userId])
476
530
  }
477
531
 
478
- //
479
- // Podcast Segments (child items of a PODCAST_EPISODE Artifact)
480
- //
481
- model PodcastSegment {
482
- id String @id @default(cuid())
483
- artifactId String
484
- 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
+ }
545
+
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
+ }
558
+
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[]
485
574
 
486
- title String
487
- content String // Full text content of the segment
488
- startTime Int // Start time in seconds
489
- duration Int // Duration in seconds
490
- order Int // Display order within the episode
575
+ @@index([userId])
576
+ @@index([planId])
577
+ }
491
578
 
492
- // Audio file reference
493
- objectKey String? // Google Cloud Storage object key
494
- audioUrl String? // Cached signed URL (temporary)
579
+ model ResourcePrice {
580
+ id String @id @default(cuid())
581
+ resourceType ArtifactType @unique
582
+ priceCents Int
583
+ updatedAt DateTime @updatedAt
584
+ }
495
585
 
496
- // Metadata
497
- keyPoints String[] // Array of key points
498
- meta Json? // Additional metadata (voice settings, etc.)
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)
499
594
 
500
- generating Boolean @default(false)
501
- generatingMetadata Json? // Additional metadata (voice settings, etc.)
595
+ @@index([userId])
596
+ }
502
597
 
503
- createdAt DateTime @default(now())
504
- updatedAt DateTime @updatedAt
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])
613
+ }
505
614
 
506
- @@index([artifactId, order]) // For efficient ordering
507
- @@index([artifactId, startTime]) // For time-based queries
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])
508
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
+