@goscribe/server 1.3.0 → 1.3.1

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 (79) hide show
  1. package/dist/context.d.ts +5 -1
  2. package/dist/lib/activity_human_description.d.ts +13 -0
  3. package/dist/lib/activity_human_description.js +221 -0
  4. package/dist/lib/activity_human_description.test.d.ts +1 -0
  5. package/dist/lib/activity_human_description.test.js +16 -0
  6. package/dist/lib/activity_log_service.d.ts +87 -0
  7. package/dist/lib/activity_log_service.js +276 -0
  8. package/dist/lib/activity_log_service.test.d.ts +1 -0
  9. package/dist/lib/activity_log_service.test.js +27 -0
  10. package/dist/lib/ai-session.d.ts +15 -2
  11. package/dist/lib/ai-session.js +147 -85
  12. package/dist/lib/constants.d.ts +13 -0
  13. package/dist/lib/constants.js +12 -0
  14. package/dist/lib/email.d.ts +11 -0
  15. package/dist/lib/email.js +193 -0
  16. package/dist/lib/env.d.ts +13 -0
  17. package/dist/lib/env.js +16 -0
  18. package/dist/lib/inference.d.ts +4 -1
  19. package/dist/lib/inference.js +3 -3
  20. package/dist/lib/logger.d.ts +4 -4
  21. package/dist/lib/logger.js +30 -8
  22. package/dist/lib/notification-service.d.ts +152 -0
  23. package/dist/lib/notification-service.js +473 -0
  24. package/dist/lib/notification-service.test.d.ts +1 -0
  25. package/dist/lib/notification-service.test.js +87 -0
  26. package/dist/lib/prisma.d.ts +2 -1
  27. package/dist/lib/prisma.js +5 -1
  28. package/dist/lib/pusher.d.ts +23 -0
  29. package/dist/lib/pusher.js +69 -5
  30. package/dist/lib/retry.d.ts +15 -0
  31. package/dist/lib/retry.js +37 -0
  32. package/dist/lib/storage.js +2 -2
  33. package/dist/lib/stripe.d.ts +9 -0
  34. package/dist/lib/stripe.js +36 -0
  35. package/dist/lib/subscription_service.d.ts +37 -0
  36. package/dist/lib/subscription_service.js +654 -0
  37. package/dist/lib/usage_service.d.ts +26 -0
  38. package/dist/lib/usage_service.js +59 -0
  39. package/dist/lib/worksheet-generation.d.ts +91 -0
  40. package/dist/lib/worksheet-generation.js +95 -0
  41. package/dist/lib/worksheet-generation.test.d.ts +1 -0
  42. package/dist/lib/worksheet-generation.test.js +20 -0
  43. package/dist/lib/workspace-access.d.ts +18 -0
  44. package/dist/lib/workspace-access.js +13 -0
  45. package/dist/routers/_app.d.ts +1349 -253
  46. package/dist/routers/_app.js +10 -0
  47. package/dist/routers/admin.d.ts +361 -0
  48. package/dist/routers/admin.js +633 -0
  49. package/dist/routers/annotations.d.ts +219 -0
  50. package/dist/routers/annotations.js +187 -0
  51. package/dist/routers/auth.d.ts +88 -7
  52. package/dist/routers/auth.js +339 -19
  53. package/dist/routers/chat.d.ts +6 -12
  54. package/dist/routers/copilot.d.ts +199 -0
  55. package/dist/routers/copilot.js +571 -0
  56. package/dist/routers/flashcards.d.ts +47 -81
  57. package/dist/routers/flashcards.js +143 -27
  58. package/dist/routers/members.d.ts +36 -7
  59. package/dist/routers/members.js +200 -19
  60. package/dist/routers/notifications.d.ts +99 -0
  61. package/dist/routers/notifications.js +127 -0
  62. package/dist/routers/payment.d.ts +89 -0
  63. package/dist/routers/payment.js +403 -0
  64. package/dist/routers/podcast.d.ts +8 -13
  65. package/dist/routers/podcast.js +54 -31
  66. package/dist/routers/studyguide.d.ts +1 -29
  67. package/dist/routers/studyguide.js +80 -71
  68. package/dist/routers/worksheets.d.ts +105 -38
  69. package/dist/routers/worksheets.js +258 -68
  70. package/dist/routers/workspace.d.ts +139 -60
  71. package/dist/routers/workspace.js +455 -315
  72. package/dist/scripts/purge-deleted-users.d.ts +1 -0
  73. package/dist/scripts/purge-deleted-users.js +149 -0
  74. package/dist/server.js +130 -10
  75. package/dist/services/flashcard-progress.service.d.ts +18 -66
  76. package/dist/services/flashcard-progress.service.js +51 -42
  77. package/dist/trpc.d.ts +20 -21
  78. package/dist/trpc.js +150 -1
  79. package/package.json +1 -1
@@ -2,6 +2,9 @@ import { z } from 'zod';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import { router, publicProcedure, authedProcedure } from '../trpc.js';
4
4
  import { logger } from '../lib/logger.js';
5
+ import { sendInvitationEmail } from '../lib/email.js';
6
+ import PusherService from '../lib/pusher.js';
7
+ import { notifyInviteAccepted, notifyInviteRecipient, notifyWorkspaceMembershipRemoved, notifyWorkspaceRoleChanged, } from '../lib/notification-service.js';
5
8
  /**
6
9
  * Members router for workspace member management
7
10
  *
@@ -72,8 +75,7 @@ export const members = router({
72
75
  message: 'Workspace not found or access denied'
73
76
  });
74
77
  }
75
- // Format members with roles
76
- const members = [
78
+ const workspaceMembers = [
77
79
  {
78
80
  id: workspace.owner.id,
79
81
  name: workspace.owner.name || 'Unknown',
@@ -89,7 +91,10 @@ export const members = router({
89
91
  joinedAt: membership.joinedAt,
90
92
  }))
91
93
  ];
92
- return members;
94
+ logger.info(`👥 Fetched ${workspaceMembers.length} members for workspace ${input.workspaceId}`, 'WORKSPACE', {
95
+ memberEmails: workspaceMembers.map(m => m.email)
96
+ });
97
+ return workspaceMembers;
93
98
  }),
94
99
  /**
95
100
  * Get current user's role in a workspace
@@ -202,6 +207,21 @@ export const members = router({
202
207
  }
203
208
  }
204
209
  });
210
+ const invitedExistingUser = await ctx.db.user.findUnique({
211
+ where: { email: input.email },
212
+ select: { id: true, name: true },
213
+ });
214
+ if (invitedExistingUser) {
215
+ await notifyInviteRecipient(ctx.db, {
216
+ invitedUserId: invitedExistingUser.id,
217
+ inviterUserId: ctx.session.user.id,
218
+ workspaceId: input.workspaceId,
219
+ workspaceTitle: invitation.workspace.title,
220
+ invitationId: invitation.id,
221
+ invitationToken: invitation.token,
222
+ inviterName: invitation.workspace.owner.name || invitation.workspace.owner.email,
223
+ });
224
+ }
205
225
  logger.info(`🎫 Invitation created for ${input.email} to workspace ${input.workspaceId} with role ${input.role}`, 'WORKSPACE', {
206
226
  invitationId: invitation.id,
207
227
  workspaceId: input.workspaceId,
@@ -209,8 +229,14 @@ export const members = router({
209
229
  role: input.role,
210
230
  invitedBy: ctx.session.user.id
211
231
  });
212
- // TODO: Send email notification here
213
- // await sendInvitationEmail(invitation);
232
+ // Send email notification
233
+ await sendInvitationEmail({
234
+ email: invitation.email,
235
+ token: invitation.token,
236
+ role: invitation.role,
237
+ workspaceTitle: invitation.workspace.title,
238
+ invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email || 'Someone',
239
+ });
214
240
  return {
215
241
  invitationId: invitation.id,
216
242
  token: invitation.token,
@@ -251,6 +277,7 @@ export const members = router({
251
277
  select: {
252
278
  id: true,
253
279
  title: true,
280
+ ownerId: true,
254
281
  owner: {
255
282
  select: {
256
283
  name: true,
@@ -277,8 +304,10 @@ export const members = router({
277
304
  const user = await ctx.db.user.findFirst({ where: { id: ctx.session.user.id } });
278
305
  if (!user || !user.email)
279
306
  throw new TRPCError({ code: 'NOT_FOUND' });
280
- // Check if the email matches the user's email
281
- if (user.email !== invitation.email) {
307
+ logger.info(`🔍 Verification check for ${user.email} accepting invite for ${invitation.email}`, 'WORKSPACE');
308
+ // Check if the email matches the user's email (case-insensitive)
309
+ if (user.email.toLowerCase() !== invitation.email.toLowerCase()) {
310
+ logger.warn(`❌ Invitation email mismatch: user ${user.email} vs invite ${invitation.email}`, 'WORKSPACE');
282
311
  throw new TRPCError({
283
312
  code: 'BAD_REQUEST',
284
313
  message: 'This invitation was sent to a different email address'
@@ -295,40 +324,81 @@ export const members = router({
295
324
  }
296
325
  });
297
326
  if (isAlreadyMember) {
327
+ logger.info(`ℹ️ User ${ctx.session.user.id} is already a member of workspace ${invitation.workspaceId}. Marking invite as accepted.`, 'WORKSPACE');
298
328
  // Mark invitation as accepted even if already a member
299
329
  await ctx.db.workspaceInvitation.update({
300
330
  where: { id: invitation.id },
301
331
  data: { acceptedAt: new Date() }
302
332
  });
303
- throw new TRPCError({
304
- code: 'BAD_REQUEST',
333
+ return {
334
+ workspaceId: invitation.workspaceId,
335
+ workspaceTitle: invitation.workspace.title,
336
+ role: invitation.role,
337
+ ownerName: invitation.workspace.owner.name || invitation.workspace.owner.email,
305
338
  message: 'You are already a member of this workspace'
339
+ };
340
+ }
341
+ // Add user to workspace with proper role.
342
+ // This can race if accept is triggered twice (e.g. redirects/retries),
343
+ // so treat duplicate membership as a successful, idempotent accept.
344
+ let memberAdded = false;
345
+ try {
346
+ await ctx.db.workspaceMember.create({
347
+ data: {
348
+ workspaceId: invitation.workspaceId,
349
+ userId: ctx.session.user.id,
350
+ role: invitation.role,
351
+ }
306
352
  });
353
+ memberAdded = true;
307
354
  }
308
- // Add user to workspace with proper role
309
- await ctx.db.workspaceMember.create({
310
- data: {
311
- workspaceId: invitation.workspaceId,
312
- userId: ctx.session.user.id,
313
- role: invitation.role,
355
+ catch (error) {
356
+ if (error?.code !== 'P2002') {
357
+ throw error;
314
358
  }
315
- });
359
+ logger.info(`ℹ️ Duplicate invite accept handled for user ${ctx.session.user.id} in workspace ${invitation.workspaceId}`, 'WORKSPACE');
360
+ }
316
361
  // Mark invitation as accepted
317
362
  await ctx.db.workspaceInvitation.update({
318
363
  where: { id: invitation.id },
319
364
  data: { acceptedAt: new Date() }
320
365
  });
321
- logger.info(`✅ Invitation accepted by ${ctx.session.user.id} for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
366
+ if (memberAdded) {
367
+ await notifyInviteAccepted(ctx.db, {
368
+ recipientUserIds: [invitation.invitedById, invitation.workspace.ownerId],
369
+ actorUserId: ctx.session.user.id,
370
+ workspaceId: invitation.workspaceId,
371
+ workspaceTitle: invitation.workspace.title,
372
+ memberName: user.name || user.email,
373
+ invitationId: invitation.id,
374
+ });
375
+ }
376
+ logger.info(`✅ Invitation accepted by ${ctx.session.user.id} (${user.email}) for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
322
377
  invitationId: invitation.id,
323
378
  workspaceId: invitation.workspaceId,
324
379
  userId: ctx.session.user.id,
325
380
  email: invitation.email
326
381
  });
382
+ // Try to emit a Pusher event if possible
383
+ try {
384
+ if (memberAdded) {
385
+ await PusherService.emitMemberJoined(invitation.workspaceId, {
386
+ id: ctx.session.user.id,
387
+ name: user.name || 'Member',
388
+ email: user.email,
389
+ role: invitation.role,
390
+ });
391
+ }
392
+ }
393
+ catch (e) {
394
+ logger.error('Failed to emit member joined event', e);
395
+ }
327
396
  return {
328
397
  workspaceId: invitation.workspaceId,
329
398
  workspaceTitle: invitation.workspace.title,
330
399
  role: invitation.role,
331
400
  ownerName: invitation.workspace.owner.name || invitation.workspace.owner.email,
401
+ ...(memberAdded ? {} : { message: 'You are already a member of this workspace' }),
332
402
  };
333
403
  }),
334
404
  /**
@@ -346,7 +416,8 @@ export const members = router({
346
416
  where: {
347
417
  id: input.workspaceId,
348
418
  ownerId: ctx.session.user.id
349
- }
419
+ },
420
+ select: { id: true, title: true, ownerId: true },
350
421
  });
351
422
  if (!workspace) {
352
423
  throw new TRPCError({
@@ -394,6 +465,19 @@ export const members = router({
394
465
  newRole: input.role,
395
466
  changedBy: ctx.session.user.id
396
467
  });
468
+ const actor = await ctx.db.user.findUnique({
469
+ where: { id: ctx.session.user.id },
470
+ select: { name: true, email: true },
471
+ });
472
+ await notifyWorkspaceRoleChanged(ctx.db, {
473
+ memberUserId: input.memberId,
474
+ workspaceId: input.workspaceId,
475
+ workspaceTitle: workspace.title,
476
+ newRole: input.role,
477
+ oldRole: member.role,
478
+ actorUserId: ctx.session.user.id,
479
+ actorName: actor?.name || actor?.email || 'A workspace admin',
480
+ }).catch(() => { });
397
481
  return {
398
482
  memberId: input.memberId,
399
483
  role: input.role,
@@ -415,7 +499,8 @@ export const members = router({
415
499
  where: {
416
500
  id: input.workspaceId,
417
501
  ownerId: ctx.session.user.id
418
- }
502
+ },
503
+ select: { id: true, title: true, ownerId: true },
419
504
  });
420
505
  if (!workspace) {
421
506
  throw new TRPCError({
@@ -451,6 +536,17 @@ export const members = router({
451
536
  message: 'Member not found in this workspace'
452
537
  });
453
538
  }
539
+ const actor = await ctx.db.user.findUnique({
540
+ where: { id: ctx.session.user.id },
541
+ select: { name: true, email: true },
542
+ });
543
+ await notifyWorkspaceMembershipRemoved(ctx.db, {
544
+ memberUserId: input.memberId,
545
+ workspaceId: input.workspaceId,
546
+ workspaceTitle: workspace.title,
547
+ actorUserId: ctx.session.user.id,
548
+ actorName: actor?.name || actor?.email || 'A workspace admin',
549
+ }).catch(() => { });
454
550
  // Remove member from workspace
455
551
  await ctx.db.workspaceMember.delete({
456
552
  where: { id: member.id }
@@ -551,4 +647,89 @@ export const members = router({
551
647
  message: 'Invitation cancelled successfully'
552
648
  };
553
649
  }),
650
+ /**
651
+ * Resend a pending invitation (owner only)
652
+ */
653
+ resendInvitation: authedProcedure
654
+ .input(z.object({
655
+ invitationId: z.string(),
656
+ }))
657
+ .mutation(async ({ ctx, input }) => {
658
+ // Check if user is owner of the workspace and invitation is pending
659
+ const invitation = await ctx.db.workspaceInvitation.findFirst({
660
+ where: {
661
+ id: input.invitationId,
662
+ acceptedAt: null,
663
+ workspace: {
664
+ ownerId: ctx.session.user.id
665
+ }
666
+ },
667
+ include: {
668
+ workspace: {
669
+ select: {
670
+ title: true,
671
+ owner: {
672
+ select: {
673
+ name: true,
674
+ email: true,
675
+ }
676
+ }
677
+ }
678
+ }
679
+ }
680
+ });
681
+ if (!invitation) {
682
+ throw new TRPCError({
683
+ code: 'NOT_FOUND',
684
+ message: 'Invitation not found or insufficient permissions'
685
+ });
686
+ }
687
+ // Check if expired and update expiry if needed
688
+ if (invitation.expiresAt < new Date()) {
689
+ const newExpiry = new Date();
690
+ newExpiry.setDate(newExpiry.getDate() + 7);
691
+ await ctx.db.workspaceInvitation.update({
692
+ where: { id: invitation.id },
693
+ data: { expiresAt: newExpiry }
694
+ });
695
+ invitation.expiresAt = newExpiry;
696
+ }
697
+ // Send email notification
698
+ await sendInvitationEmail({
699
+ email: invitation.email,
700
+ token: invitation.token,
701
+ role: invitation.role,
702
+ workspaceTitle: invitation.workspace.title,
703
+ invitedByName: invitation.workspace.owner.name || invitation.workspace.owner.email || 'Someone',
704
+ });
705
+ logger.info(`📧 Invitation resent to ${invitation.email} for workspace ${invitation.workspaceId}`, 'WORKSPACE', {
706
+ invitationId: invitation.id,
707
+ workspaceId: invitation.workspaceId,
708
+ email: invitation.email,
709
+ resentBy: ctx.session.user.id
710
+ });
711
+ return {
712
+ invitationId: invitation.id,
713
+ message: 'Invitation email resent successfully'
714
+ };
715
+ }),
716
+ /**
717
+ * DEBUG ONLY: Get all invitations for a workspace
718
+ */
719
+ getAllInvitationsDebug: authedProcedure
720
+ .input(z.object({
721
+ workspaceId: z.string(),
722
+ }))
723
+ .query(async ({ ctx, input }) => {
724
+ // Check if user is owner
725
+ const workspace = await ctx.db.workspace.findFirst({
726
+ where: { id: input.workspaceId, ownerId: ctx.session.user.id }
727
+ });
728
+ if (!workspace)
729
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
730
+ return ctx.db.workspaceInvitation.findMany({
731
+ where: { workspaceId: input.workspaceId },
732
+ orderBy: { createdAt: 'desc' }
733
+ });
734
+ }),
554
735
  });
@@ -0,0 +1,99 @@
1
+ export declare const notifications: import("@trpc/server").TRPCBuiltRouter<{
2
+ ctx: import("../context.js").Context;
3
+ meta: object;
4
+ errorShape: {
5
+ data: {
6
+ zodError: string | null;
7
+ code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
8
+ httpStatus: number;
9
+ path?: string;
10
+ stack?: string;
11
+ };
12
+ message: string;
13
+ code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
14
+ };
15
+ transformer: true;
16
+ }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
17
+ list: import("@trpc/server").TRPCQueryProcedure<{
18
+ input: {
19
+ cursor?: string | undefined;
20
+ limit?: number | undefined;
21
+ unreadOnly?: boolean | undefined;
22
+ types?: string[] | undefined;
23
+ };
24
+ output: {
25
+ items: ({
26
+ workspace: {
27
+ id: string;
28
+ title: string;
29
+ } | null;
30
+ actor: {
31
+ name: string | null;
32
+ id: string;
33
+ email: string | null;
34
+ } | null;
35
+ } & {
36
+ id: string;
37
+ createdAt: Date;
38
+ updatedAt: Date;
39
+ workspaceId: string | null;
40
+ type: string;
41
+ title: string;
42
+ content: string | null;
43
+ userId: string;
44
+ actorUserId: string | null;
45
+ body: string;
46
+ actionUrl: string | null;
47
+ metadata: import("@prisma/client/runtime/library").JsonValue | null;
48
+ priority: import("@prisma/client").$Enums.NotificationPriority;
49
+ sourceId: string | null;
50
+ read: boolean;
51
+ readAt: Date | null;
52
+ deliveredAt: Date | null;
53
+ })[];
54
+ nextCursor: string | undefined;
55
+ };
56
+ meta: object;
57
+ }>;
58
+ unreadCount: import("@trpc/server").TRPCQueryProcedure<{
59
+ input: void;
60
+ output: {
61
+ count: number;
62
+ };
63
+ meta: object;
64
+ }>;
65
+ markRead: import("@trpc/server").TRPCMutationProcedure<{
66
+ input: {
67
+ id: string;
68
+ };
69
+ output: {
70
+ success: boolean;
71
+ };
72
+ meta: object;
73
+ }>;
74
+ markManyRead: import("@trpc/server").TRPCMutationProcedure<{
75
+ input: {
76
+ ids: string[];
77
+ };
78
+ output: {
79
+ success: boolean;
80
+ };
81
+ meta: object;
82
+ }>;
83
+ markAllRead: import("@trpc/server").TRPCMutationProcedure<{
84
+ input: void;
85
+ output: {
86
+ success: boolean;
87
+ };
88
+ meta: object;
89
+ }>;
90
+ delete: import("@trpc/server").TRPCMutationProcedure<{
91
+ input: {
92
+ id: string;
93
+ };
94
+ output: {
95
+ success: boolean;
96
+ };
97
+ meta: object;
98
+ }>;
99
+ }>>;
@@ -0,0 +1,127 @@
1
+ import { z } from 'zod';
2
+ import { authedProcedure, router } from '../trpc.js';
3
+ import PusherService from '../lib/pusher.js';
4
+ const listInputSchema = z.object({
5
+ cursor: z.string().optional(),
6
+ limit: z.number().min(1).max(50).default(20),
7
+ unreadOnly: z.boolean().optional(),
8
+ types: z.array(z.string()).optional(),
9
+ });
10
+ export const notifications = router({
11
+ list: authedProcedure
12
+ .input(listInputSchema)
13
+ .query(async ({ ctx, input }) => {
14
+ const { cursor, limit, unreadOnly, types } = input;
15
+ const userId = ctx.userId;
16
+ const items = await ctx.db.notification.findMany({
17
+ where: {
18
+ userId,
19
+ ...(unreadOnly ? { read: false } : {}),
20
+ ...(types?.length ? { type: { in: types } } : {}),
21
+ },
22
+ include: {
23
+ actor: {
24
+ select: {
25
+ id: true,
26
+ name: true,
27
+ email: true,
28
+ },
29
+ },
30
+ workspace: {
31
+ select: {
32
+ id: true,
33
+ title: true,
34
+ },
35
+ },
36
+ },
37
+ orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
38
+ take: limit + 1,
39
+ ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
40
+ });
41
+ let nextCursor;
42
+ if (items.length > limit) {
43
+ const next = items.pop();
44
+ nextCursor = next?.id;
45
+ }
46
+ return {
47
+ items,
48
+ nextCursor,
49
+ };
50
+ }),
51
+ unreadCount: authedProcedure.query(async ({ ctx }) => {
52
+ const count = await ctx.db.notification.count({
53
+ where: {
54
+ userId: ctx.userId,
55
+ read: false,
56
+ },
57
+ });
58
+ return { count };
59
+ }),
60
+ markRead: authedProcedure
61
+ .input(z.object({ id: z.string() }))
62
+ .mutation(async ({ ctx, input }) => {
63
+ const now = new Date();
64
+ await ctx.db.notification.updateMany({
65
+ where: {
66
+ id: input.id,
67
+ userId: ctx.userId,
68
+ },
69
+ data: {
70
+ read: true,
71
+ readAt: now,
72
+ },
73
+ });
74
+ const unreadCount = await ctx.db.notification.count({
75
+ where: { userId: ctx.userId, read: false },
76
+ });
77
+ await PusherService.emitNotificationReadState(ctx.userId, { unreadCount });
78
+ return { success: true };
79
+ }),
80
+ markManyRead: authedProcedure
81
+ .input(z.object({ ids: z.array(z.string()).min(1) }))
82
+ .mutation(async ({ ctx, input }) => {
83
+ const now = new Date();
84
+ await ctx.db.notification.updateMany({
85
+ where: {
86
+ id: { in: input.ids },
87
+ userId: ctx.userId,
88
+ },
89
+ data: {
90
+ read: true,
91
+ readAt: now,
92
+ },
93
+ });
94
+ const unreadCount = await ctx.db.notification.count({
95
+ where: { userId: ctx.userId, read: false },
96
+ });
97
+ await PusherService.emitNotificationReadState(ctx.userId, { unreadCount });
98
+ return { success: true };
99
+ }),
100
+ markAllRead: authedProcedure
101
+ .mutation(async ({ ctx }) => {
102
+ const now = new Date();
103
+ await ctx.db.notification.updateMany({
104
+ where: {
105
+ userId: ctx.userId,
106
+ read: false,
107
+ },
108
+ data: {
109
+ read: true,
110
+ readAt: now,
111
+ },
112
+ });
113
+ await PusherService.emitNotificationReadState(ctx.userId, { unreadCount: 0 });
114
+ return { success: true };
115
+ }),
116
+ delete: authedProcedure
117
+ .input(z.object({ id: z.string() }))
118
+ .mutation(async ({ ctx, input }) => {
119
+ await ctx.db.notification.deleteMany({
120
+ where: {
121
+ id: input.id,
122
+ userId: ctx.userId,
123
+ },
124
+ });
125
+ return { success: true };
126
+ }),
127
+ });
@@ -0,0 +1,89 @@
1
+ export declare const paymentRouter: import("@trpc/server").TRPCBuiltRouter<{
2
+ ctx: import("../context.js").Context;
3
+ meta: object;
4
+ errorShape: {
5
+ data: {
6
+ zodError: string | null;
7
+ code: import("@trpc/server").TRPC_ERROR_CODE_KEY;
8
+ httpStatus: number;
9
+ path?: string;
10
+ stack?: string;
11
+ };
12
+ message: string;
13
+ code: import("@trpc/server").TRPC_ERROR_CODE_NUMBER;
14
+ };
15
+ transformer: true;
16
+ }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
17
+ getPlans: import("@trpc/server").TRPCQueryProcedure<{
18
+ input: void;
19
+ output: any[];
20
+ meta: object;
21
+ }>;
22
+ createCheckoutSession: import("@trpc/server").TRPCMutationProcedure<{
23
+ input: {
24
+ planId: string;
25
+ };
26
+ output: {
27
+ url: any;
28
+ };
29
+ meta: object;
30
+ }>;
31
+ confirmCheckoutSuccess: import("@trpc/server").TRPCMutationProcedure<{
32
+ input: {
33
+ sessionId: string;
34
+ };
35
+ output: {
36
+ confirmed: boolean;
37
+ reason: string;
38
+ kind?: undefined;
39
+ } | {
40
+ confirmed: boolean;
41
+ kind: "payment";
42
+ reason?: undefined;
43
+ } | {
44
+ confirmed: boolean;
45
+ kind: "subscription";
46
+ reason?: undefined;
47
+ };
48
+ meta: object;
49
+ }>;
50
+ createResourcePurchaseSession: import("@trpc/server").TRPCMutationProcedure<{
51
+ input: {
52
+ resourceType: "STUDY_GUIDE" | "FLASHCARD_SET" | "WORKSHEET" | "MEETING_SUMMARY" | "PODCAST_EPISODE" | "STORAGE";
53
+ quantity?: number | undefined;
54
+ };
55
+ output: {
56
+ url: any;
57
+ };
58
+ meta: object;
59
+ }>;
60
+ getUsageOverview: import("@trpc/server").TRPCQueryProcedure<{
61
+ input: void;
62
+ output: {
63
+ usage: import("../lib/usage_service.js").UserUsage;
64
+ limits: {
65
+ id: string;
66
+ planId: string;
67
+ maxStorageBytes: bigint;
68
+ maxWorksheets: number;
69
+ maxFlashcards: number;
70
+ maxPodcasts: number;
71
+ maxStudyGuides: number;
72
+ createdAt: Date;
73
+ updatedAt: Date;
74
+ } | null;
75
+ hasActivePlan: boolean;
76
+ };
77
+ meta: object;
78
+ }>;
79
+ getResourcePrices: import("@trpc/server").TRPCQueryProcedure<{
80
+ input: void;
81
+ output: {
82
+ id: string;
83
+ updatedAt: Date;
84
+ resourceType: import("@prisma/client").$Enums.ArtifactType;
85
+ priceCents: number;
86
+ }[];
87
+ meta: object;
88
+ }>;
89
+ }>>;