@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.
- package/dist/context.d.ts +5 -1
- package/dist/lib/activity_human_description.d.ts +13 -0
- package/dist/lib/activity_human_description.js +221 -0
- package/dist/lib/activity_human_description.test.d.ts +1 -0
- package/dist/lib/activity_human_description.test.js +16 -0
- package/dist/lib/activity_log_service.d.ts +87 -0
- package/dist/lib/activity_log_service.js +276 -0
- package/dist/lib/activity_log_service.test.d.ts +1 -0
- package/dist/lib/activity_log_service.test.js +27 -0
- package/dist/lib/ai-session.d.ts +15 -2
- package/dist/lib/ai-session.js +147 -85
- package/dist/lib/constants.d.ts +13 -0
- package/dist/lib/constants.js +12 -0
- package/dist/lib/email.d.ts +11 -0
- package/dist/lib/email.js +193 -0
- package/dist/lib/env.d.ts +13 -0
- package/dist/lib/env.js +16 -0
- package/dist/lib/inference.d.ts +4 -1
- package/dist/lib/inference.js +3 -3
- package/dist/lib/logger.d.ts +4 -4
- package/dist/lib/logger.js +30 -8
- package/dist/lib/notification-service.d.ts +152 -0
- package/dist/lib/notification-service.js +473 -0
- package/dist/lib/notification-service.test.d.ts +1 -0
- package/dist/lib/notification-service.test.js +87 -0
- package/dist/lib/prisma.d.ts +2 -1
- package/dist/lib/prisma.js +5 -1
- package/dist/lib/pusher.d.ts +23 -0
- package/dist/lib/pusher.js +69 -5
- package/dist/lib/retry.d.ts +15 -0
- package/dist/lib/retry.js +37 -0
- package/dist/lib/storage.js +2 -2
- package/dist/lib/stripe.d.ts +9 -0
- package/dist/lib/stripe.js +36 -0
- package/dist/lib/subscription_service.d.ts +37 -0
- package/dist/lib/subscription_service.js +654 -0
- package/dist/lib/usage_service.d.ts +26 -0
- package/dist/lib/usage_service.js +59 -0
- package/dist/lib/worksheet-generation.d.ts +91 -0
- package/dist/lib/worksheet-generation.js +95 -0
- package/dist/lib/worksheet-generation.test.d.ts +1 -0
- package/dist/lib/worksheet-generation.test.js +20 -0
- package/dist/lib/workspace-access.d.ts +18 -0
- package/dist/lib/workspace-access.js +13 -0
- package/dist/routers/_app.d.ts +1349 -253
- package/dist/routers/_app.js +10 -0
- package/dist/routers/admin.d.ts +361 -0
- package/dist/routers/admin.js +633 -0
- package/dist/routers/annotations.d.ts +219 -0
- package/dist/routers/annotations.js +187 -0
- package/dist/routers/auth.d.ts +88 -7
- package/dist/routers/auth.js +339 -19
- package/dist/routers/chat.d.ts +6 -12
- package/dist/routers/copilot.d.ts +199 -0
- package/dist/routers/copilot.js +571 -0
- package/dist/routers/flashcards.d.ts +47 -81
- package/dist/routers/flashcards.js +143 -27
- package/dist/routers/members.d.ts +36 -7
- package/dist/routers/members.js +200 -19
- package/dist/routers/notifications.d.ts +99 -0
- package/dist/routers/notifications.js +127 -0
- package/dist/routers/payment.d.ts +89 -0
- package/dist/routers/payment.js +403 -0
- package/dist/routers/podcast.d.ts +8 -13
- package/dist/routers/podcast.js +54 -31
- package/dist/routers/studyguide.d.ts +1 -29
- package/dist/routers/studyguide.js +80 -71
- package/dist/routers/worksheets.d.ts +105 -38
- package/dist/routers/worksheets.js +258 -68
- package/dist/routers/workspace.d.ts +139 -60
- package/dist/routers/workspace.js +455 -315
- package/dist/scripts/purge-deleted-users.d.ts +1 -0
- package/dist/scripts/purge-deleted-users.js +149 -0
- package/dist/server.js +130 -10
- package/dist/services/flashcard-progress.service.d.ts +18 -66
- package/dist/services/flashcard-progress.service.js +51 -42
- package/dist/trpc.d.ts +20 -21
- package/dist/trpc.js +150 -1
- package/package.json +1 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { router, verifiedProcedure } from '../trpc.js';
|
|
3
|
+
import { TRPCError } from '@trpc/server';
|
|
4
|
+
import { stripe } from '../lib/stripe.js';
|
|
5
|
+
import { env } from '../lib/env.js';
|
|
6
|
+
import { getUserUsage, getUserPlanLimits } from '../lib/usage_service.js';
|
|
7
|
+
import { notifyPaymentSucceeded, notifySubscriptionActivated, notifySubscriptionPaymentSucceeded, } from '../lib/notification-service.js';
|
|
8
|
+
import { upsertSubscriptionFromStripe } from '../lib/subscription_service.js';
|
|
9
|
+
import { ArtifactType } from '../lib/prisma.js';
|
|
10
|
+
const ArtifactTypeUnion = z.enum(['STUDY_GUIDE', 'FLASHCARD_SET', 'WORKSHEET', 'MEETING_SUMMARY', 'PODCAST_EPISODE', 'STORAGE']);
|
|
11
|
+
/** Stripe Checkout URLs contain the session id (cs_…); we only store the URL in DB. */
|
|
12
|
+
const CHECKOUT_SESSION_ID_IN_URL = /cs_[a-zA-Z0-9]+/;
|
|
13
|
+
/**
|
|
14
|
+
* On idempotency conflict, we may have a stored checkout URL. Only reuse it if the
|
|
15
|
+
* Stripe session is still `open`. Otherwise release the row so a new session can be created
|
|
16
|
+
* (avoids redirecting users to a completed/expired checkout = "already completed").
|
|
17
|
+
*/
|
|
18
|
+
async function reuseCheckoutUrlIfSessionStillOpen(stripeClient, db, existing, lockKey) {
|
|
19
|
+
const url = existing.stripeSessionId;
|
|
20
|
+
if (!url)
|
|
21
|
+
return null;
|
|
22
|
+
const idMatch = url.match(CHECKOUT_SESSION_ID_IN_URL);
|
|
23
|
+
if (!idMatch) {
|
|
24
|
+
await db.idempotencyRecord.updateMany({
|
|
25
|
+
where: { id: existing.id, activeLockKey: lockKey },
|
|
26
|
+
data: { activeLockKey: null, status: 'expired' },
|
|
27
|
+
});
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const session = await stripeClient.checkout.sessions.retrieve(idMatch[0]);
|
|
32
|
+
if (session.status === 'open') {
|
|
33
|
+
return url;
|
|
34
|
+
}
|
|
35
|
+
await db.idempotencyRecord.updateMany({
|
|
36
|
+
where: { id: existing.id, activeLockKey: lockKey },
|
|
37
|
+
data: {
|
|
38
|
+
activeLockKey: null,
|
|
39
|
+
status: session.status === 'complete' ? 'completed' : 'expired',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
await db.idempotencyRecord.updateMany({
|
|
46
|
+
where: { id: existing.id, activeLockKey: lockKey },
|
|
47
|
+
data: { activeLockKey: null, status: 'expired' },
|
|
48
|
+
});
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export const paymentRouter = router({
|
|
53
|
+
getPlans: verifiedProcedure
|
|
54
|
+
.query(async (opts) => {
|
|
55
|
+
const ctx = opts.ctx;
|
|
56
|
+
const userId = ctx.userId;
|
|
57
|
+
const plans = await ctx.db.plan.findMany({
|
|
58
|
+
where: { active: true },
|
|
59
|
+
include: { limit: true },
|
|
60
|
+
orderBy: { price: 'asc' }
|
|
61
|
+
});
|
|
62
|
+
const activeSubscriptions = await ctx.db.subscription.findMany({
|
|
63
|
+
where: {
|
|
64
|
+
userId: userId,
|
|
65
|
+
status: 'active'
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return plans.map((plan) => {
|
|
69
|
+
return {
|
|
70
|
+
...plan,
|
|
71
|
+
isActive: activeSubscriptions.some((sub) => sub.planId === plan.id)
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}),
|
|
75
|
+
createCheckoutSession: verifiedProcedure
|
|
76
|
+
.input(z.object({
|
|
77
|
+
planId: z.string()
|
|
78
|
+
}))
|
|
79
|
+
.mutation(async (opts) => {
|
|
80
|
+
const ctx = opts.ctx;
|
|
81
|
+
const input = opts.input;
|
|
82
|
+
const userId = ctx.userId;
|
|
83
|
+
if (!stripe) {
|
|
84
|
+
throw new TRPCError({
|
|
85
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
86
|
+
message: 'Stripe not configured'
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const user = await ctx.db.user.findUnique({ where: { id: userId } });
|
|
90
|
+
if (!user)
|
|
91
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
|
|
92
|
+
const lockKey = `pending_${userId}_${input.planId}`;
|
|
93
|
+
let attempt = null;
|
|
94
|
+
let retryCount = 0;
|
|
95
|
+
while (retryCount < 3) {
|
|
96
|
+
try {
|
|
97
|
+
attempt = await ctx.db.idempotencyRecord.create({
|
|
98
|
+
data: {
|
|
99
|
+
userId: userId,
|
|
100
|
+
planId: input.planId,
|
|
101
|
+
activeLockKey: lockKey,
|
|
102
|
+
status: 'pending'
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err.code === 'P2002') {
|
|
109
|
+
const existing = await ctx.db.idempotencyRecord.findUnique({
|
|
110
|
+
where: { activeLockKey: lockKey }
|
|
111
|
+
});
|
|
112
|
+
if (!existing) {
|
|
113
|
+
retryCount++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const isStale = (Date.now() - existing.updatedAt.getTime()) > (24 * 60 * 60 * 1000);
|
|
117
|
+
if (isStale) {
|
|
118
|
+
const result = await ctx.db.idempotencyRecord.updateMany({
|
|
119
|
+
where: { id: existing.id, activeLockKey: lockKey },
|
|
120
|
+
data: { activeLockKey: null, status: 'expired' }
|
|
121
|
+
});
|
|
122
|
+
if (result.count > 0) {
|
|
123
|
+
retryCount++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (existing.stripeSessionId && stripe) {
|
|
128
|
+
const reusable = await reuseCheckoutUrlIfSessionStillOpen(stripe, ctx.db, existing, lockKey);
|
|
129
|
+
if (reusable)
|
|
130
|
+
return { url: reusable };
|
|
131
|
+
retryCount++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
135
|
+
retryCount++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!attempt)
|
|
142
|
+
throw new TRPCError({ code: 'CONFLICT', message: "Concurrent request" });
|
|
143
|
+
const plan = await ctx.db.plan.findUnique({ where: { id: input.planId } });
|
|
144
|
+
if (!plan)
|
|
145
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: "Plan not found" });
|
|
146
|
+
try {
|
|
147
|
+
const successUrl = env.STRIPE_SUCCESS_URL.includes('session_id=')
|
|
148
|
+
? env.STRIPE_SUCCESS_URL
|
|
149
|
+
: `${env.STRIPE_SUCCESS_URL}${env.STRIPE_SUCCESS_URL.includes('?') ? '&' : '?'}session_id={CHECKOUT_SESSION_ID}`;
|
|
150
|
+
const session = await stripe.checkout.sessions.create({
|
|
151
|
+
customer: user.stripe_customer_id || undefined,
|
|
152
|
+
customer_email: user.stripe_customer_id ? undefined : (user.email || undefined),
|
|
153
|
+
line_items: [{ price: plan.stripePriceId, quantity: 1 }],
|
|
154
|
+
mode: plan.interval ? 'subscription' : 'payment',
|
|
155
|
+
subscription_data: plan.interval ? {
|
|
156
|
+
metadata: {
|
|
157
|
+
userId: userId,
|
|
158
|
+
planId: plan.id,
|
|
159
|
+
attemptId: attempt.id
|
|
160
|
+
}
|
|
161
|
+
} : undefined,
|
|
162
|
+
success_url: successUrl,
|
|
163
|
+
cancel_url: env.STRIPE_CANCEL_URL,
|
|
164
|
+
metadata: {
|
|
165
|
+
userId: userId,
|
|
166
|
+
planId: plan.id,
|
|
167
|
+
attemptId: attempt.id
|
|
168
|
+
}
|
|
169
|
+
}, {
|
|
170
|
+
idempotencyKey: attempt.id
|
|
171
|
+
});
|
|
172
|
+
await ctx.db.idempotencyRecord.update({
|
|
173
|
+
where: { id: attempt.id },
|
|
174
|
+
data: { stripeSessionId: session.url }
|
|
175
|
+
});
|
|
176
|
+
return { url: session.url };
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
await ctx.db.idempotencyRecord.update({
|
|
180
|
+
where: { id: attempt.id },
|
|
181
|
+
data: { status: 'failed', activeLockKey: null }
|
|
182
|
+
});
|
|
183
|
+
throw new TRPCError({
|
|
184
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
185
|
+
message: error.message
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}),
|
|
189
|
+
confirmCheckoutSuccess: verifiedProcedure
|
|
190
|
+
.input(z.object({
|
|
191
|
+
sessionId: z.string().min(1),
|
|
192
|
+
}))
|
|
193
|
+
.mutation(async (opts) => {
|
|
194
|
+
const ctx = opts.ctx;
|
|
195
|
+
const input = opts.input;
|
|
196
|
+
if (!stripe) {
|
|
197
|
+
throw new TRPCError({
|
|
198
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
199
|
+
message: 'Stripe not configured'
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const session = await stripe.checkout.sessions.retrieve(input.sessionId, {
|
|
203
|
+
expand: ['line_items', 'subscription'],
|
|
204
|
+
});
|
|
205
|
+
const metadata = session.metadata || {};
|
|
206
|
+
if (metadata.userId !== ctx.userId) {
|
|
207
|
+
throw new TRPCError({
|
|
208
|
+
code: 'FORBIDDEN',
|
|
209
|
+
message: 'This checkout session does not belong to the current user',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (session.status !== 'complete') {
|
|
213
|
+
return { confirmed: false, reason: 'checkout_not_complete' };
|
|
214
|
+
}
|
|
215
|
+
const plan = metadata.planId
|
|
216
|
+
? await ctx.db.plan.findUnique({ where: { id: metadata.planId } })
|
|
217
|
+
: null;
|
|
218
|
+
if (session.mode === 'payment' && session.payment_status === 'paid') {
|
|
219
|
+
await notifyPaymentSucceeded(ctx.db, {
|
|
220
|
+
userId: ctx.userId,
|
|
221
|
+
planId: metadata.planId,
|
|
222
|
+
planName: plan?.name || metadata.planType,
|
|
223
|
+
stripeSessionId: session.id,
|
|
224
|
+
amountPaid: session.amount_total ?? undefined,
|
|
225
|
+
});
|
|
226
|
+
return { confirmed: true, kind: 'payment' };
|
|
227
|
+
}
|
|
228
|
+
if (session.mode === 'subscription') {
|
|
229
|
+
const stripeSubscriptionId = typeof session.subscription === 'string'
|
|
230
|
+
? session.subscription
|
|
231
|
+
: session.subscription?.id;
|
|
232
|
+
if (stripeSubscriptionId) {
|
|
233
|
+
await upsertSubscriptionFromStripe(stripeSubscriptionId);
|
|
234
|
+
await notifySubscriptionActivated(ctx.db, {
|
|
235
|
+
userId: ctx.userId,
|
|
236
|
+
planId: metadata.planId,
|
|
237
|
+
planName: plan?.name || metadata.planType,
|
|
238
|
+
stripeSubscriptionId,
|
|
239
|
+
});
|
|
240
|
+
if (session.payment_status === 'paid') {
|
|
241
|
+
await notifySubscriptionPaymentSucceeded(ctx.db, {
|
|
242
|
+
userId: ctx.userId,
|
|
243
|
+
planId: metadata.planId,
|
|
244
|
+
planName: plan?.name || metadata.planType,
|
|
245
|
+
stripeInvoiceId: `checkout_${session.id}`,
|
|
246
|
+
amountPaid: session.amount_total ?? undefined,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { confirmed: true, kind: 'subscription' };
|
|
251
|
+
}
|
|
252
|
+
return { confirmed: false, reason: 'unsupported_mode' };
|
|
253
|
+
}),
|
|
254
|
+
createResourcePurchaseSession: verifiedProcedure
|
|
255
|
+
.input(z.object({
|
|
256
|
+
resourceType: z.nativeEnum(ArtifactType),
|
|
257
|
+
quantity: z.number().min(1).default(1),
|
|
258
|
+
}))
|
|
259
|
+
.mutation(async ({ ctx, input }) => {
|
|
260
|
+
if (!stripe) {
|
|
261
|
+
throw new TRPCError({
|
|
262
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
263
|
+
message: 'Stripe is not configured on the server',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const userId = ctx.userId;
|
|
267
|
+
const user = await ctx.db.user.findUnique({
|
|
268
|
+
where: { id: userId },
|
|
269
|
+
});
|
|
270
|
+
if (!user) {
|
|
271
|
+
throw new TRPCError({
|
|
272
|
+
code: 'NOT_FOUND',
|
|
273
|
+
message: 'User not found',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
const resourcePrice = await ctx.db.resourcePrice.findUnique({
|
|
277
|
+
where: { resourceType: input.resourceType },
|
|
278
|
+
});
|
|
279
|
+
if (!resourcePrice) {
|
|
280
|
+
throw new TRPCError({
|
|
281
|
+
code: 'PRECONDITION_FAILED',
|
|
282
|
+
message: 'Price not set',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const lockKey = `topup_${userId}_${input.resourceType}`;
|
|
286
|
+
let attempt = null;
|
|
287
|
+
let retryCount = 0;
|
|
288
|
+
while (retryCount < 3) {
|
|
289
|
+
try {
|
|
290
|
+
attempt = await ctx.db.idempotencyRecord.create({
|
|
291
|
+
data: {
|
|
292
|
+
userId: userId,
|
|
293
|
+
resourceType: input.resourceType,
|
|
294
|
+
activeLockKey: lockKey,
|
|
295
|
+
status: 'pending'
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
if (err.code === 'P2002') {
|
|
302
|
+
const existing = await ctx.db.idempotencyRecord.findUnique({
|
|
303
|
+
where: { activeLockKey: lockKey }
|
|
304
|
+
});
|
|
305
|
+
if (!existing) {
|
|
306
|
+
retryCount++;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const isStale = (Date.now() - existing.updatedAt.getTime()) > (24 * 60 * 60 * 1000);
|
|
310
|
+
if (isStale) {
|
|
311
|
+
const result = await ctx.db.idempotencyRecord.updateMany({
|
|
312
|
+
where: { id: existing.id, activeLockKey: lockKey },
|
|
313
|
+
data: { activeLockKey: null, status: 'expired' }
|
|
314
|
+
});
|
|
315
|
+
if (result.count > 0) {
|
|
316
|
+
retryCount++;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (existing.stripeSessionId && stripe) {
|
|
321
|
+
const reusable = await reuseCheckoutUrlIfSessionStillOpen(stripe, ctx.db, existing, lockKey);
|
|
322
|
+
if (reusable)
|
|
323
|
+
return { url: reusable };
|
|
324
|
+
retryCount++;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
328
|
+
retryCount++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!attempt)
|
|
335
|
+
throw new TRPCError({ code: 'CONFLICT', message: "Concurrent request" });
|
|
336
|
+
try {
|
|
337
|
+
const resourceName = input.resourceType
|
|
338
|
+
.split('_')
|
|
339
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
340
|
+
.join(' ');
|
|
341
|
+
const session = await stripe.checkout.sessions.create({
|
|
342
|
+
customer: user.stripe_customer_id || undefined,
|
|
343
|
+
line_items: [
|
|
344
|
+
{
|
|
345
|
+
price_data: {
|
|
346
|
+
currency: 'usd',
|
|
347
|
+
product_data: {
|
|
348
|
+
name: `Add-on: extra ${resourceName}s`,
|
|
349
|
+
description: `Purchase of ${input.quantity} additional ${resourceName}(s)`
|
|
350
|
+
},
|
|
351
|
+
unit_amount: resourcePrice.priceCents
|
|
352
|
+
},
|
|
353
|
+
quantity: input.quantity
|
|
354
|
+
}
|
|
355
|
+
],
|
|
356
|
+
mode: 'payment',
|
|
357
|
+
success_url: `${env.STRIPE_SUCCESS_URL}?success=true`,
|
|
358
|
+
cancel_url: env.STRIPE_CANCEL_URL,
|
|
359
|
+
metadata: {
|
|
360
|
+
userId: userId,
|
|
361
|
+
resourceType: input.resourceType,
|
|
362
|
+
quantity: input.quantity.toString(),
|
|
363
|
+
isPurchase: 'true',
|
|
364
|
+
attemptId: attempt.id,
|
|
365
|
+
},
|
|
366
|
+
invoice_creation: {
|
|
367
|
+
enabled: true,
|
|
368
|
+
},
|
|
369
|
+
}, {
|
|
370
|
+
idempotencyKey: attempt.id
|
|
371
|
+
});
|
|
372
|
+
await ctx.db.idempotencyRecord.update({
|
|
373
|
+
where: { id: attempt.id },
|
|
374
|
+
data: { stripeSessionId: session.url }
|
|
375
|
+
});
|
|
376
|
+
return { url: session.url };
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
await ctx.db.idempotencyRecord.update({
|
|
380
|
+
where: { id: attempt.id },
|
|
381
|
+
data: { status: 'failed', activeLockKey: null }
|
|
382
|
+
});
|
|
383
|
+
throw new TRPCError({
|
|
384
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
385
|
+
message: error.message
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}),
|
|
389
|
+
getUsageOverview: verifiedProcedure
|
|
390
|
+
.query(async (opts) => {
|
|
391
|
+
const ctx = opts.ctx;
|
|
392
|
+
const userId = ctx.userId;
|
|
393
|
+
const [usage, limits] = await Promise.all([
|
|
394
|
+
getUserUsage(userId),
|
|
395
|
+
getUserPlanLimits(userId)
|
|
396
|
+
]);
|
|
397
|
+
return { usage: usage, limits: limits, hasActivePlan: (!!limits) };
|
|
398
|
+
}),
|
|
399
|
+
getResourcePrices: verifiedProcedure
|
|
400
|
+
.query(async (opts) => {
|
|
401
|
+
return opts.ctx.db.resourcePrice.findMany();
|
|
402
|
+
})
|
|
403
|
+
});
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
export declare const podcast: import("@trpc/server").TRPCBuiltRouter<{
|
|
2
|
-
ctx:
|
|
3
|
-
db: import("@prisma/client").PrismaClient<import("@prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
|
|
4
|
-
session: any;
|
|
5
|
-
req: import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>;
|
|
6
|
-
res: import("express").Response<any, Record<string, any>>;
|
|
7
|
-
cookies: Record<string, string | undefined>;
|
|
8
|
-
};
|
|
2
|
+
ctx: import("../context.js").Context;
|
|
9
3
|
meta: object;
|
|
10
4
|
errorShape: {
|
|
11
5
|
data: {
|
|
@@ -181,11 +175,11 @@ export declare const podcast: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
181
175
|
createdAt: Date;
|
|
182
176
|
updatedAt: Date;
|
|
183
177
|
title: string;
|
|
184
|
-
objectKey: string | null;
|
|
185
178
|
generating: boolean;
|
|
186
179
|
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
187
180
|
artifactId: string;
|
|
188
181
|
content: string;
|
|
182
|
+
objectKey: string | null;
|
|
189
183
|
duration: number;
|
|
190
184
|
order: number;
|
|
191
185
|
startTime: number;
|
|
@@ -239,17 +233,18 @@ export declare const podcast: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
239
233
|
id: string;
|
|
240
234
|
createdAt: Date;
|
|
241
235
|
updatedAt: Date;
|
|
242
|
-
title: string;
|
|
243
|
-
description: string | null;
|
|
244
236
|
workspaceId: string;
|
|
245
237
|
type: import("@prisma/client").$Enums.ArtifactType;
|
|
238
|
+
title: string;
|
|
246
239
|
isArchived: boolean;
|
|
247
|
-
generating: boolean;
|
|
248
|
-
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
249
240
|
difficulty: import("@prisma/client").$Enums.Difficulty | null;
|
|
250
241
|
estimatedTime: string | null;
|
|
251
|
-
imageObjectKey: string | null;
|
|
252
242
|
createdById: string | null;
|
|
243
|
+
description: string | null;
|
|
244
|
+
generating: boolean;
|
|
245
|
+
generatingMetadata: import("@prisma/client/runtime/library").JsonValue | null;
|
|
246
|
+
worksheetConfig: import("@prisma/client/runtime/library").JsonValue | null;
|
|
247
|
+
imageObjectKey: string | null;
|
|
253
248
|
};
|
|
254
249
|
meta: object;
|
|
255
250
|
}>;
|