@coursebuilder/server 0.1.0-canary.0.0c9beb2ae

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/dist/actions/checkout.d.ts +6 -0
  3. package/dist/actions/checkout.js +9 -0
  4. package/dist/actions/checkout.js.map +1 -0
  5. package/dist/actions/claimed.d.ts +6 -0
  6. package/dist/actions/claimed.js +8 -0
  7. package/dist/actions/claimed.js.map +1 -0
  8. package/dist/actions/create-magic-link.d.ts +6 -0
  9. package/dist/actions/create-magic-link.js +9 -0
  10. package/dist/actions/create-magic-link.js.map +1 -0
  11. package/dist/actions/index.d.ts +11 -0
  12. package/dist/actions/index.js +18 -0
  13. package/dist/actions/index.js.map +1 -0
  14. package/dist/actions/prices-coupon-for-code.d.ts +6 -0
  15. package/dist/actions/prices-coupon-for-code.js +63 -0
  16. package/dist/actions/prices-coupon-for-code.js.map +1 -0
  17. package/dist/actions/prices-formatted.d.ts +6 -0
  18. package/dist/actions/prices-formatted.js +8 -0
  19. package/dist/actions/prices-formatted.js.map +1 -0
  20. package/dist/actions/process-refund.d.ts +1 -0
  21. package/dist/actions/process-refund.js +8 -0
  22. package/dist/actions/process-refund.js.map +1 -0
  23. package/dist/actions/redeem.d.ts +1 -0
  24. package/dist/actions/redeem.js +8 -0
  25. package/dist/actions/redeem.js.map +1 -0
  26. package/dist/actions/transfer-purchase.d.ts +1 -0
  27. package/dist/actions/transfer-purchase.js +8 -0
  28. package/dist/actions/transfer-purchase.js.map +1 -0
  29. package/dist/actions/user-lookup.d.ts +6 -0
  30. package/dist/actions/user-lookup.js +9 -0
  31. package/dist/actions/user-lookup.js.map +1 -0
  32. package/dist/adapter-BtDS6LDw.d.ts +9 -0
  33. package/dist/chunk-22AMOZYZ.js +67 -0
  34. package/dist/chunk-22AMOZYZ.js.map +1 -0
  35. package/dist/chunk-4EQQSW57.js +32 -0
  36. package/dist/chunk-4EQQSW57.js.map +1 -0
  37. package/dist/chunk-4LZZLN6T.js +52 -0
  38. package/dist/chunk-4LZZLN6T.js.map +1 -0
  39. package/dist/chunk-4QQA3TC2.js +29 -0
  40. package/dist/chunk-4QQA3TC2.js.map +1 -0
  41. package/dist/chunk-4TSEPUSC.js +160 -0
  42. package/dist/chunk-4TSEPUSC.js.map +1 -0
  43. package/dist/chunk-5O4UO4IR.js +93 -0
  44. package/dist/chunk-5O4UO4IR.js.map +1 -0
  45. package/dist/chunk-7FCQOLKL.js +26 -0
  46. package/dist/chunk-7FCQOLKL.js.map +1 -0
  47. package/dist/chunk-AZLUMIIF.js +8 -0
  48. package/dist/chunk-AZLUMIIF.js.map +1 -0
  49. package/dist/chunk-E6HV6TNI.js +38 -0
  50. package/dist/chunk-E6HV6TNI.js.map +1 -0
  51. package/dist/chunk-ECNJPNPU.js +54 -0
  52. package/dist/chunk-ECNJPNPU.js.map +1 -0
  53. package/dist/chunk-F3KUYN5I.js +111 -0
  54. package/dist/chunk-F3KUYN5I.js.map +1 -0
  55. package/dist/chunk-G63YAJF2.js +107 -0
  56. package/dist/chunk-G63YAJF2.js.map +1 -0
  57. package/dist/chunk-JFR7XO72.js +7 -0
  58. package/dist/chunk-JFR7XO72.js.map +1 -0
  59. package/dist/chunk-KNC7QVGZ.js +26 -0
  60. package/dist/chunk-KNC7QVGZ.js.map +1 -0
  61. package/dist/chunk-ODSSRMD2.js +270 -0
  62. package/dist/chunk-ODSSRMD2.js.map +1 -0
  63. package/dist/chunk-VEKYIR7I.js +26 -0
  64. package/dist/chunk-VEKYIR7I.js.map +1 -0
  65. package/dist/chunk-W7YAT4NR.js +12 -0
  66. package/dist/chunk-W7YAT4NR.js.map +1 -0
  67. package/dist/chunk-WGAPYIUP.js +56 -0
  68. package/dist/chunk-WGAPYIUP.js.map +1 -0
  69. package/dist/chunk-WYNH2AWD.js +7 -0
  70. package/dist/chunk-WYNH2AWD.js.map +1 -0
  71. package/dist/chunk-Y5X5J55Z.js +384 -0
  72. package/dist/chunk-Y5X5J55Z.js.map +1 -0
  73. package/dist/chunk-YFLGJIMA.js +216 -0
  74. package/dist/chunk-YFLGJIMA.js.map +1 -0
  75. package/dist/chunk-ZFZNN4E5.js +7 -0
  76. package/dist/chunk-ZFZNN4E5.js.map +1 -0
  77. package/dist/create-inngest-middleware.d.ts +635 -0
  78. package/dist/create-inngest-middleware.js +12 -0
  79. package/dist/create-inngest-middleware.js.map +1 -0
  80. package/dist/http-router.d.ts +20 -0
  81. package/dist/http-router.js +24 -0
  82. package/dist/http-router.js.map +1 -0
  83. package/dist/http.d.ts +38 -0
  84. package/dist/http.js +32 -0
  85. package/dist/http.js.map +1 -0
  86. package/dist/index.d.ts +34 -0
  87. package/dist/index.js +365 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/magic-string.es-ENO43NON.js +1389 -0
  90. package/dist/magic-string.es-ENO43NON.js.map +1 -0
  91. package/dist/runtime/actions.d.ts +5 -0
  92. package/dist/runtime/actions.js +8 -0
  93. package/dist/runtime/actions.js.map +1 -0
  94. package/dist/runtime/assert.d.ts +15 -0
  95. package/dist/runtime/assert.js +8 -0
  96. package/dist/runtime/assert.js.map +1 -0
  97. package/dist/runtime/cookie.d.ts +8 -0
  98. package/dist/runtime/cookie.js +8 -0
  99. package/dist/runtime/cookie.js.map +1 -0
  100. package/dist/runtime/env.d.ts +9 -0
  101. package/dist/runtime/env.js +11 -0
  102. package/dist/runtime/env.js.map +1 -0
  103. package/dist/runtime/env.test.d.ts +2 -0
  104. package/dist/runtime/env.test.js +14956 -0
  105. package/dist/runtime/env.test.js.map +1 -0
  106. package/dist/runtime/init.d.ts +26 -0
  107. package/dist/runtime/init.js +14 -0
  108. package/dist/runtime/init.js.map +1 -0
  109. package/dist/runtime/logger.d.ts +4 -0
  110. package/dist/runtime/logger.js +10 -0
  111. package/dist/runtime/logger.js.map +1 -0
  112. package/dist/runtime/merge.d.ts +4 -0
  113. package/dist/runtime/merge.js +8 -0
  114. package/dist/runtime/merge.js.map +1 -0
  115. package/dist/runtime/providers.d.ts +21 -0
  116. package/dist/runtime/providers.js +9 -0
  117. package/dist/runtime/providers.js.map +1 -0
  118. package/dist/runtime/web.d.ts +29 -0
  119. package/dist/runtime/web.js +16 -0
  120. package/dist/runtime/web.js.map +1 -0
  121. package/package.json +65 -0
  122. package/src/actions/actions.test.ts +232 -0
  123. package/src/actions/checkout.ts +160 -0
  124. package/src/actions/claimed.ts +40 -0
  125. package/src/actions/create-magic-link.ts +78 -0
  126. package/src/actions/index.ts +401 -0
  127. package/src/actions/prices-coupon-for-code.ts +91 -0
  128. package/src/actions/prices-formatted.ts +313 -0
  129. package/src/actions/process-refund.ts +2 -0
  130. package/src/actions/redeem.ts +2 -0
  131. package/src/actions/transfer-purchase.ts +2 -0
  132. package/src/actions/user-lookup.ts +37 -0
  133. package/src/adapter.ts +28 -0
  134. package/src/create-inngest-middleware.ts +157 -0
  135. package/src/http-actions.ts +29 -0
  136. package/src/http-router.ts +79 -0
  137. package/src/http.test.ts +46 -0
  138. package/src/http.ts +83 -0
  139. package/src/index.test.ts +21 -0
  140. package/src/index.ts +44 -0
  141. package/src/mock-adapter.ts +398 -0
  142. package/src/mock-payment-provider.ts +56 -0
  143. package/src/resource-chat.ts +371 -0
  144. package/src/runtime/actions.ts +25 -0
  145. package/src/runtime/assert.ts +55 -0
  146. package/src/runtime/cookie.ts +12 -0
  147. package/src/runtime/env.test.ts +49 -0
  148. package/src/runtime/env.ts +63 -0
  149. package/src/runtime/init.ts +112 -0
  150. package/src/runtime/logger.ts +6 -0
  151. package/src/runtime/merge.ts +25 -0
  152. package/src/runtime/providers.ts +38 -0
  153. package/src/runtime/web.ts +199 -0
  154. package/src/streaming-chat-prompt-executor.ts +43 -0
  155. package/src/type-contracts.test.ts +167 -0
  156. package/src/web-body-parse.test.ts +55 -0
@@ -0,0 +1,78 @@
1
+ import type { CourseBuilderEmailProvider } from '@coursebuilder/core/auth'
2
+ import type {
3
+ InternalOptions,
4
+ RequestInternal,
5
+ ResponseInternal,
6
+ } from '@coursebuilder/core/types'
7
+ import { createVerificationUrl } from '@coursebuilder/email/send-server-email'
8
+
9
+ import type { Cookie } from '../runtime/cookie'
10
+ import { logger } from '../runtime/logger'
11
+
12
+ export async function createMagicLink(
13
+ request: RequestInternal,
14
+ cookies: Cookie[],
15
+ options: InternalOptions,
16
+ ): Promise<ResponseInternal> {
17
+ if (!options.adapter) throw new Error('Adapter not found')
18
+ if (request.headers?.['x-skill-secret'] !== process.env.SKILL_SECRET) {
19
+ return { status: 401, body: 'unauthorized' }
20
+ }
21
+
22
+ const { email } = request.body || {}
23
+
24
+ if (!email) {
25
+ return { status: 400, body: 'email is required' }
26
+ }
27
+
28
+ const emailProvider = options.providers.find((p) => p.type === 'email')
29
+
30
+ if (!emailProvider) {
31
+ return { status: 400, body: 'email provider not found' }
32
+ }
33
+
34
+ const expiresIn = request.body?.expiresIn || request.query?.expiresIn
35
+
36
+ let expiresAt: Date | undefined = undefined
37
+
38
+ if (expiresIn) {
39
+ const durationInMilliseconds = expiresIn * 1000
40
+ expiresAt = new Date(Date.now() + durationInMilliseconds)
41
+ }
42
+
43
+ try {
44
+ const verificationDetails = await createVerificationUrl({
45
+ email,
46
+ emailProvider: emailProvider as CourseBuilderEmailProvider,
47
+ authOptions: options.authConfig,
48
+ adapter: options.adapter,
49
+ baseUrl: options.baseUrl,
50
+ expiresAt,
51
+ })
52
+ if (!verificationDetails?.url) {
53
+ return {
54
+ status: 500,
55
+ body: JSON.stringify({
56
+ error: 'Could not create verification url',
57
+ }),
58
+ }
59
+ }
60
+ return {
61
+ status: 200,
62
+ body: JSON.stringify({
63
+ url: verificationDetails.url,
64
+ }),
65
+ }
66
+ } catch (e) {
67
+ const error = e instanceof Error ? e : new Error(String(e))
68
+ logger.error(error, {
69
+ context: 'magic-link.create',
70
+ hasEmail: Boolean(email),
71
+ expiresIn: expiresIn ?? null,
72
+ })
73
+ return {
74
+ status: 500,
75
+ body: JSON.stringify({ error: error.message }),
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,401 @@
1
+ import { z } from 'zod'
2
+
3
+ import { processStripeWebhook } from '@coursebuilder/commerce/process-stripe-webhook'
4
+ import type { CourseBuilderEmailProvider } from '@coursebuilder/core/auth'
5
+ import {
6
+ VIDEO_SRT_READY_EVENT,
7
+ VIDEO_TRANSCRIPT_READY_EVENT,
8
+ } from '@coursebuilder/core/events/video'
9
+ import type {
10
+ InternalOptions,
11
+ RequestInternal,
12
+ ResponseInternal,
13
+ } from '@coursebuilder/core/types'
14
+ import { sendServerEmail } from '@coursebuilder/email/send-server-email'
15
+ import {
16
+ srtFromTranscriptResult,
17
+ transcriptAsParagraphsWithTimestamps,
18
+ wordLevelSrtFromTranscriptResult,
19
+ } from '@coursebuilder/video-processing/transcript-transforms'
20
+
21
+ import type { Cookie } from '../runtime/cookie'
22
+
23
+ const oneYear = 31556952
24
+
25
+ function getConvertkitSubscriberCookie(subscriber: any): Cookie[] {
26
+ return subscriber
27
+ ? [
28
+ {
29
+ name: 'ck_subscriber',
30
+ value: JSON.stringify(subscriber),
31
+ options: {
32
+ secure: true,
33
+ httpOnly: true,
34
+ path: '/',
35
+ maxAge: oneYear,
36
+ sameSite: 'lax',
37
+ },
38
+ },
39
+ {
40
+ name:
41
+ process.env.NEXT_PUBLIC_CONVERTKIT_SUBSCRIBER_KEY ||
42
+ 'ck_subscriber_id',
43
+ value: subscriber.id,
44
+ options: {
45
+ secure: true,
46
+ httpOnly: true,
47
+ path: '/',
48
+ maxAge: oneYear,
49
+ sameSite: 'lax',
50
+ },
51
+ },
52
+ ]
53
+ : []
54
+ }
55
+
56
+ type Subscriber = Record<string, any>
57
+
58
+ function filterNullFields(obj: Subscriber): Subscriber {
59
+ const filteredObj: Subscriber = {}
60
+
61
+ for (const key in obj) {
62
+ if (obj[key] !== null) {
63
+ if (typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
64
+ filteredObj[key] = filterNullFields(obj[key] as Subscriber)
65
+ } else {
66
+ filteredObj[key] = obj[key]
67
+ }
68
+ }
69
+ }
70
+
71
+ return filteredObj
72
+ }
73
+
74
+ export async function getUserPurchases(
75
+ request: RequestInternal,
76
+ cookies: Cookie[],
77
+ options: InternalOptions,
78
+ ): Promise<ResponseInternal> {
79
+ const client = options.adapter
80
+
81
+ const currentUser = await options.getCurrentUser?.()
82
+
83
+ if (!currentUser?.id || currentUser.id !== request.query?.userId) {
84
+ return {
85
+ status: 401,
86
+ body: [],
87
+ headers: { 'Content-Type': 'application/json' },
88
+ cookies,
89
+ }
90
+ }
91
+
92
+ if (!request.query?.userId)
93
+ return {
94
+ status: 200,
95
+ body: [],
96
+ headers: { 'Content-Type': 'application/json' },
97
+ cookies,
98
+ }
99
+
100
+ const purchases = await client?.getPurchasesForUser(request.query.userId)
101
+
102
+ if (!purchases) {
103
+ return {
104
+ status: 200,
105
+ body: [],
106
+ headers: { 'Content-Type': 'application/json' },
107
+ cookies,
108
+ }
109
+ } else {
110
+ return {
111
+ status: 200,
112
+ body: purchases,
113
+ headers: { 'Content-Type': 'application/json' },
114
+ cookies,
115
+ }
116
+ }
117
+ }
118
+
119
+ export async function getSubscriber(
120
+ options: InternalOptions<'email-list'>,
121
+ cookies: Cookie[],
122
+ ): Promise<ResponseInternal<any | null>> {
123
+ switch (options.provider.type) {
124
+ case 'email-list':
125
+ const subscriber = await options.provider.getSubscriber(
126
+ options.url.searchParams.get('subscriberId') ||
127
+ options.cookies.ck_subscriber_id ||
128
+ null,
129
+ )
130
+ return {
131
+ status: 200,
132
+ body: subscriber,
133
+ headers: { 'Content-Type': 'application/json' },
134
+ cookies: getConvertkitSubscriberCookie(subscriber),
135
+ }
136
+ default:
137
+ throw new Error('Unsupported provider')
138
+ }
139
+ }
140
+
141
+ export async function subscribeToList(
142
+ request: RequestInternal,
143
+ cookies: Cookie[],
144
+ options: InternalOptions<'email-list'>,
145
+ ): Promise<ResponseInternal<any | null>> {
146
+ const response: ResponseInternal<any | null> = {
147
+ body: null,
148
+ headers: { 'Content-Type': 'application/json' },
149
+ cookies,
150
+ }
151
+
152
+ switch (options.provider.type) {
153
+ case 'email-list':
154
+ const subscribeOptions = z
155
+ .object({
156
+ listId: z.union([z.number(), z.string()]).optional(),
157
+ listType: z.string().optional(),
158
+ fields: z.record(z.string(), z.any()).optional().nullable(),
159
+ email: z.string(),
160
+ name: z.string().optional().nullable(),
161
+ })
162
+ .parse(request.body)
163
+
164
+ let user = await options.adapter?.getUserByEmail(subscribeOptions.email)
165
+
166
+ if (!user) {
167
+ user = await options.adapter?.createUser({
168
+ id: crypto.randomUUID(),
169
+ email: subscribeOptions.email,
170
+ name: subscribeOptions.name,
171
+ emailVerified: null,
172
+ })
173
+ if (!user) throw new Error('Could not create user')
174
+ options.logger.debug(`created user ${user.id}`)
175
+ } else {
176
+ options.logger.debug(`found user ${user.id}`)
177
+ }
178
+
179
+ response.body = await options.provider.subscribeToList({
180
+ user,
181
+ listId: subscribeOptions.listId || options.provider.defaultListId,
182
+ fields: subscribeOptions.fields || {},
183
+ listType: subscribeOptions.listType || options.provider.defaultListType,
184
+ })
185
+
186
+ response.cookies = getConvertkitSubscriberCookie(
187
+ filterNullFields(response.body),
188
+ )
189
+
190
+ if (!user.emailVerified) {
191
+ const emailProvider = options.providers.find(
192
+ (p) => p.type === 'email',
193
+ ) as CourseBuilderEmailProvider
194
+
195
+ if (
196
+ emailProvider &&
197
+ options.adapter &&
198
+ options.provider.id === 'coursebuilder'
199
+ ) {
200
+ await sendServerEmail({
201
+ email: user.email,
202
+ type: 'signup',
203
+ callbackUrl: `${options.baseUrl}/confirmed`,
204
+ emailProvider,
205
+ authOptions: options.authConfig,
206
+ adapter: options.adapter,
207
+ baseUrl: options.baseUrl,
208
+ })
209
+ }
210
+ }
211
+ }
212
+
213
+ return response
214
+ }
215
+
216
+ export async function session(
217
+ options: InternalOptions,
218
+ cookies: Cookie[],
219
+ ): Promise<ResponseInternal<any | null>> {
220
+ const { callbacks, logger } = options
221
+
222
+ const response: ResponseInternal<any | null> = {
223
+ body: null,
224
+ headers: { 'Content-Type': 'application/json' },
225
+ cookies,
226
+ }
227
+
228
+ try {
229
+ response.body = await callbacks.session({})
230
+ } catch (e) {
231
+ logger.error(e as Error)
232
+ }
233
+
234
+ return response
235
+ }
236
+
237
+ export async function srt(
238
+ request: RequestInternal,
239
+ cookies: Cookie[],
240
+ options: InternalOptions,
241
+ ): Promise<ResponseInternal> {
242
+ const client = options.adapter
243
+
244
+ const resource = await client?.getContentResource(
245
+ request.query?.videoResourceId,
246
+ )
247
+
248
+ if (!resource) throw new Error('Resource not found')
249
+
250
+ return {
251
+ status: 200,
252
+ body: resource.fields?.srt,
253
+ headers: { 'Content-Type': 'application/text' },
254
+ cookies,
255
+ }
256
+ }
257
+
258
+ export async function webhook(
259
+ request: RequestInternal,
260
+ cookies: Cookie[],
261
+ options: InternalOptions<'transcription' | 'payment'>,
262
+ ): Promise<ResponseInternal> {
263
+ if (!options.provider) throw new Error('Provider not found')
264
+
265
+ switch (options.provider.type) {
266
+ case 'payment':
267
+ if (options.provider.id === 'stripe') {
268
+ await processStripeWebhook(request.body, options)
269
+ return Response.json('ok', { status: 200 })
270
+ } else {
271
+ throw new Error('Unsupported provider')
272
+ }
273
+
274
+ case 'transcription':
275
+ if (!request.body) throw new Error('No body')
276
+
277
+ const { results } = request.body
278
+
279
+ const videoResourceId = options.url.searchParams.get('videoResourceId')
280
+
281
+ if (!videoResourceId) throw new Error('No videoResourceId')
282
+
283
+ const videoResource =
284
+ await options.adapter?.getContentResource(videoResourceId)
285
+
286
+ if (!videoResource) throw new Error('No videoResource')
287
+
288
+ const rawTranscriptId = `raw-transcript-${videoResourceId}`
289
+
290
+ const existingRawTranscript =
291
+ await options.adapter?.getContentResource(rawTranscriptId)
292
+
293
+ if (!existingRawTranscript) {
294
+ await options.adapter?.createContentResource({
295
+ id: rawTranscriptId,
296
+ type: 'raw-transcript',
297
+ fields: {
298
+ deepgramResults: results,
299
+ },
300
+ createdById: videoResource.createdById,
301
+ })
302
+ await options.adapter?.addResourceToResource({
303
+ childResourceId: rawTranscriptId,
304
+ parentResourceId: videoResourceId,
305
+ })
306
+ }
307
+
308
+ const srt = srtFromTranscriptResult(results)
309
+ const wordLevelSrt = wordLevelSrtFromTranscriptResult(results)
310
+ const transcript = transcriptAsParagraphsWithTimestamps(results)
311
+
312
+ // await options.adapter?.createContentResource({
313
+ // id: `raw-transcript-${videoResourceId}`,
314
+ // type: 'raw-transcript',
315
+ // fields: {
316
+ // rawTranscript: results,
317
+ // },
318
+ // createdById: videoResource.createdById,
319
+ // })
320
+ //
321
+ // await options.adapter?.addResourceToResource({
322
+ // childResourceId: `transcript-${videoResourceId}`,
323
+ // parentResourceId: videoResourceId,
324
+ // })
325
+ //
326
+ // await options.adapter?.createContentResource({
327
+ // id: `transcript-${videoResourceId}`,
328
+ // type: 'transcript',
329
+ // fields: {
330
+ // transcript,
331
+ // },
332
+ // createdById: videoResource.createdById,
333
+ // })
334
+ //
335
+ // await options.adapter?.addResourceToResource({
336
+ // childResourceId: `transcript-${videoResourceId}`,
337
+ // parentResourceId: videoResourceId,
338
+ // })
339
+ //
340
+ // await options.adapter?.createContentResource({
341
+ // id: `srt-${videoResourceId}`,
342
+ // type: 'srt',
343
+ // fields: {
344
+ // srt,
345
+ // },
346
+ // createdById: videoResource.createdById,
347
+ // })
348
+ //
349
+ // await options.adapter?.addResourceToResource({
350
+ // childResourceId: `srt-${videoResourceId}`,
351
+ // parentResourceId: videoResourceId,
352
+ // })
353
+ //
354
+ // await options.adapter?.createContentResource({
355
+ // id: `word-level-srt-${videoResourceId}`,
356
+ // type: 'word-level-srt',
357
+ // fields: {
358
+ // wordLevelSrt,
359
+ // },
360
+ // createdById: videoResource.createdById,
361
+ // })
362
+ //
363
+ // await options.adapter?.addResourceToResource({
364
+ // childResourceId: `word-level-srt-${videoResourceId}`,
365
+ // parentResourceId: videoResourceId,
366
+ // })
367
+
368
+ await options.adapter?.updateContentResourceFields({
369
+ id: videoResourceId as string,
370
+ fields: {
371
+ transcript,
372
+ srt,
373
+ wordLevelSrt,
374
+ },
375
+ })
376
+
377
+ await options.inngest.send({
378
+ name: VIDEO_TRANSCRIPT_READY_EVENT,
379
+ data: {
380
+ videoResourceId,
381
+ },
382
+ })
383
+
384
+ if (srt && wordLevelSrt && videoResourceId) {
385
+ await options.inngest.send({
386
+ name: VIDEO_SRT_READY_EVENT,
387
+ data: {
388
+ videoResourceId: videoResourceId,
389
+ },
390
+ })
391
+ }
392
+ return {
393
+ status: 200,
394
+ body: null,
395
+ headers: { 'Content-Type': 'application/json' },
396
+ cookies,
397
+ }
398
+ }
399
+
400
+ throw new Error('Invalid provider type')
401
+ }
@@ -0,0 +1,91 @@
1
+ import { isBefore } from 'date-fns'
2
+
3
+ import type { Coupon } from '@coursebuilder/core/schemas'
4
+ import type {
5
+ InternalOptions,
6
+ RequestInternal,
7
+ ResponseInternal,
8
+ } from '@coursebuilder/core/types'
9
+
10
+ import type { Cookie } from '../runtime/cookie'
11
+
12
+ const validateCoupon = (coupon: Coupon | null, productIds: string[] = []) => {
13
+ if (!coupon) {
14
+ return {
15
+ isValid: false,
16
+ isRedeemable: false,
17
+ error: 'coupon-not-found',
18
+ }
19
+ }
20
+
21
+ const isUsedUp =
22
+ coupon.maxUses > 0 ? coupon.usedCount >= coupon.maxUses : false
23
+
24
+ const isExpired = coupon.expires
25
+ ? isBefore(new Date(coupon.expires), new Date())
26
+ : false
27
+
28
+ if (
29
+ coupon.restrictedToProductId &&
30
+ !productIds.includes(coupon.restrictedToProductId)
31
+ ) {
32
+ return {
33
+ isValid: false,
34
+ isRedeemable: false,
35
+ error: 'coupon-not-valid-for-product',
36
+ }
37
+ }
38
+
39
+ const isValid = !isUsedUp && !isExpired
40
+
41
+ return {
42
+ isExpired,
43
+ isUsedUp,
44
+ isRedeemable:
45
+ isValid && Number(coupon.percentageDiscount) >= 1 && !coupon.default,
46
+ isValid,
47
+ }
48
+ }
49
+
50
+ export async function getCouponForCode(
51
+ request: RequestInternal,
52
+ cookies: Cookie[],
53
+ options: InternalOptions,
54
+ ): Promise<ResponseInternal> {
55
+ const { code, productIds } = request.body || {}
56
+
57
+ if (!code)
58
+ return {
59
+ body: null,
60
+ headers: { 'Content-Type': 'application/json' },
61
+ cookies,
62
+ }
63
+
64
+ let couponFromCode = code && (await options.adapter?.getCoupon(code))
65
+
66
+ if (couponFromCode) {
67
+ const is100Percent =
68
+ productIds.length === 0 &&
69
+ couponFromCode.restrictedToProductId &&
70
+ couponFromCode.percentageDiscount === 1
71
+
72
+ const validatedCoupon = validateCoupon(
73
+ couponFromCode,
74
+ is100Percent ? [couponFromCode?.restrictedToProductId] : productIds,
75
+ )
76
+ return {
77
+ body: {
78
+ ...couponFromCode,
79
+ ...validatedCoupon,
80
+ },
81
+ headers: { 'Content-Type': 'application/json' },
82
+ cookies,
83
+ }
84
+ }
85
+
86
+ return {
87
+ body: null,
88
+ headers: { 'Content-Type': 'application/json' },
89
+ cookies,
90
+ }
91
+ }