@coursebuilder/adapter-drizzle 1.0.4 → 1.0.6
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/{chunk-KPBVZ44W.js → chunk-2I7NIOKI.js} +2 -2
- package/dist/chunk-2X5XVNDN.js +12 -0
- package/dist/{chunk-BLDW3QUS.js → chunk-5NNBQJUP.js} +2 -2
- package/dist/{chunk-HWIQUI7O.js → chunk-5WMUBNWJ.js} +6 -5
- package/dist/{chunk-WJWJ6E5Y.js → chunk-65H5Y4PD.js} +2 -2
- package/dist/{chunk-MP4CUWYC.js → chunk-6SJORFGN.js} +2 -2
- package/dist/{chunk-ET3QAIY7.js → chunk-CCXVDMTA.js} +2 -2
- package/dist/{chunk-DKJO4Y3K.js → chunk-DNBW5D64.js} +2 -2
- package/dist/{chunk-OBUGOZKM.js → chunk-EJ7UDN4T.js} +6 -5
- package/dist/{chunk-YQNIXCD3.js → chunk-ENIKVP6O.js} +6 -5
- package/dist/{chunk-ARHWIBFG.js → chunk-HEQ5MII4.js} +2789 -1237
- package/dist/chunk-MQSLKVLJ.js +101 -0
- package/dist/{chunk-LPJ2P2KJ.js → chunk-MUFYDXA7.js} +65 -44
- package/dist/{chunk-WPJZAYX7.js → chunk-NON5CXDH.js} +4 -3
- package/dist/{chunk-ORRM7KVW.js → chunk-OFZECDCQ.js} +2 -2
- package/dist/{chunk-LERR6HQ3.js → chunk-OUJPUONP.js} +2 -2
- package/dist/{chunk-MGZMGDQ5.js → chunk-PMCOC5PM.js} +2 -2
- package/dist/{chunk-EHRARBRS.js → chunk-QKFITIXC.js} +2 -2
- package/dist/{chunk-6MRFUIH5.js → chunk-SIZFEXIS.js} +2 -2
- package/dist/chunk-SOOAUY4E.js +63 -0
- package/dist/{chunk-3GONHHMY.js → chunk-SUOPB24V.js} +4 -4
- package/dist/{chunk-253RMSB3.js → chunk-U6XVAMCK.js} +6 -5
- package/dist/{chunk-LZHZDFNZ.js → chunk-UWUDJCJM.js} +2 -2
- package/dist/{chunk-6YPHJWS2.js → chunk-WMCZWSAG.js} +2 -2
- package/dist/{chunk-FPAQINSA.js → chunk-XKCPXM63.js} +3 -3
- package/dist/{chunk-PBTEHA7F.js → chunk-YMVONZZW.js} +6 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +26 -24
- package/dist/lib/mysql/index.cjs +3205 -1495
- package/dist/lib/mysql/index.cjs.map +1 -1
- package/dist/lib/mysql/index.d.cts +759 -238
- package/dist/lib/mysql/index.d.ts +759 -238
- package/dist/lib/mysql/index.js +30 -26
- package/dist/lib/mysql/schemas/auth/accounts.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/accounts.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/accounts.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/accounts.js +12 -12
- package/dist/lib/mysql/schemas/auth/device-access-token.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/device-access-token.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/device-access-token.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/device-access-token.js +13 -13
- package/dist/lib/mysql/schemas/auth/device-verification.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/device-verification.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/device-verification.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/device-verification.js +13 -13
- package/dist/lib/mysql/schemas/auth/permissions.js +2 -2
- package/dist/lib/mysql/schemas/auth/profiles.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/profiles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/profiles.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/profiles.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/profiles.js +12 -12
- package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/role-permissions.js +13 -13
- package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/roles.js +12 -12
- package/dist/lib/mysql/schemas/auth/sessions.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/sessions.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/sessions.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/sessions.js +13 -13
- package/dist/lib/mysql/schemas/auth/user-permissions.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-permissions.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/user-permissions.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/user-permissions.js +12 -12
- package/dist/lib/mysql/schemas/auth/user-prefs.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-prefs.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/user-prefs.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/user-prefs.js +12 -12
- package/dist/lib/mysql/schemas/auth/user-roles.cjs +3 -2
- package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-roles.d.cts +2 -1
- package/dist/lib/mysql/schemas/auth/user-roles.d.ts +2 -1
- package/dist/lib/mysql/schemas/auth/user-roles.js +12 -12
- package/dist/lib/mysql/schemas/auth/users.cjs +11 -3
- package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/users.d.cts +5 -2
- package/dist/lib/mysql/schemas/auth/users.d.ts +5 -2
- package/dist/lib/mysql/schemas/auth/users.js +12 -12
- package/dist/lib/mysql/schemas/auth/verification-tokens.js +2 -2
- package/dist/lib/mysql/schemas/commerce/coupon.cjs +4 -2
- package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/coupon.d.cts +17 -1
- package/dist/lib/mysql/schemas/commerce/coupon.d.ts +17 -1
- package/dist/lib/mysql/schemas/commerce/coupon.js +12 -12
- package/dist/lib/mysql/schemas/commerce/merchant-account.js +2 -2
- package/dist/lib/mysql/schemas/commerce/merchant-charge.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/merchant-charge.js +12 -12
- package/dist/lib/mysql/schemas/commerce/merchant-coupon.cjs +2 -1
- package/dist/lib/mysql/schemas/commerce/merchant-coupon.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/merchant-coupon.d.cts +17 -1
- package/dist/lib/mysql/schemas/commerce/merchant-coupon.d.ts +17 -1
- package/dist/lib/mysql/schemas/commerce/merchant-coupon.js +2 -2
- package/dist/lib/mysql/schemas/commerce/merchant-customer.js +2 -2
- package/dist/lib/mysql/schemas/commerce/merchant-events.cjs +120 -0
- package/dist/lib/mysql/schemas/commerce/merchant-events.cjs.map +1 -0
- package/dist/lib/mysql/schemas/commerce/merchant-events.d.cts +101 -0
- package/dist/lib/mysql/schemas/commerce/merchant-events.d.ts +101 -0
- package/dist/lib/mysql/schemas/commerce/merchant-events.js +11 -0
- package/dist/lib/mysql/schemas/commerce/merchant-events.js.map +1 -0
- package/dist/lib/mysql/schemas/commerce/merchant-price.js +2 -2
- package/dist/lib/mysql/schemas/commerce/merchant-product.js +2 -2
- package/dist/lib/mysql/schemas/commerce/merchant-session.js +2 -2
- package/dist/lib/mysql/schemas/commerce/merchant-subscription.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/merchant-subscription.js +12 -12
- package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/price.js +12 -12
- package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/product.js +12 -12
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs +3 -2
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.d.cts +2 -1
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.d.ts +2 -1
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +13 -13
- package/dist/lib/mysql/schemas/commerce/purchase.cjs +5 -3
- package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/purchase.d.cts +2 -1
- package/dist/lib/mysql/schemas/commerce/purchase.d.ts +2 -1
- package/dist/lib/mysql/schemas/commerce/purchase.js +12 -12
- package/dist/lib/mysql/schemas/commerce/subscription.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/subscription.js +12 -12
- package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/upgradable-products.js +13 -13
- package/dist/lib/mysql/schemas/communication/comment.cjs +3 -2
- package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/comment.d.cts +2 -1
- package/dist/lib/mysql/schemas/communication/comment.d.ts +2 -1
- package/dist/lib/mysql/schemas/communication/comment.js +12 -12
- package/dist/lib/mysql/schemas/communication/communication-channel.js +2 -2
- package/dist/lib/mysql/schemas/communication/communication-preference-types.js +2 -2
- package/dist/lib/mysql/schemas/communication/communication-preferences.cjs +3 -2
- package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/communication-preferences.d.cts +2 -1
- package/dist/lib/mysql/schemas/communication/communication-preferences.d.ts +2 -1
- package/dist/lib/mysql/schemas/communication/communication-preferences.js +12 -12
- package/dist/lib/mysql/schemas/communication/question-response.cjs +351 -0
- package/dist/lib/mysql/schemas/communication/question-response.cjs.map +1 -0
- package/dist/lib/mysql/schemas/communication/question-response.d.cts +200 -0
- package/dist/lib/mysql/schemas/communication/question-response.d.ts +200 -0
- package/dist/lib/mysql/schemas/communication/question-response.js +21 -0
- package/dist/lib/mysql/schemas/communication/question-response.js.map +1 -0
- package/dist/lib/mysql/schemas/content/content-contributions.cjs +11 -3
- package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-contributions.d.cts +2 -1
- package/dist/lib/mysql/schemas/content/content-contributions.d.ts +2 -1
- package/dist/lib/mysql/schemas/content/content-contributions.js +12 -12
- package/dist/lib/mysql/schemas/content/content-resource-product.cjs +8 -1
- package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-product.js +12 -12
- package/dist/lib/mysql/schemas/content/content-resource-resource.cjs +8 -1
- package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-resource.js +12 -12
- package/dist/lib/mysql/schemas/content/content-resource-tag.cjs +8 -1
- package/dist/lib/mysql/schemas/content/content-resource-tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-tag.js +12 -12
- package/dist/lib/mysql/schemas/content/content-resource-version.cjs +11 -3
- package/dist/lib/mysql/schemas/content/content-resource-version.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-version.d.cts +2 -1
- package/dist/lib/mysql/schemas/content/content-resource-version.d.ts +2 -1
- package/dist/lib/mysql/schemas/content/content-resource-version.js +12 -12
- package/dist/lib/mysql/schemas/content/content-resource.cjs +11 -3
- package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource.d.cts +18 -1
- package/dist/lib/mysql/schemas/content/content-resource.d.ts +18 -1
- package/dist/lib/mysql/schemas/content/content-resource.js +12 -12
- package/dist/lib/mysql/schemas/content/contribution-types.js +2 -2
- package/dist/lib/mysql/schemas/content/lesson-progress.js +2 -2
- package/dist/lib/mysql/schemas/content/resource-progress.js +2 -2
- package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/tag-tag.js +12 -12
- package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/tag.js +12 -12
- package/dist/lib/mysql/schemas/entitlements/entitlement-type.js +2 -2
- package/dist/lib/mysql/schemas/entitlements/entitlement.cjs +3 -2
- package/dist/lib/mysql/schemas/entitlements/entitlement.cjs.map +1 -1
- package/dist/lib/mysql/schemas/entitlements/entitlement.d.cts +2 -1
- package/dist/lib/mysql/schemas/entitlements/entitlement.d.ts +2 -1
- package/dist/lib/mysql/schemas/entitlements/entitlement.js +13 -13
- package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organization-membership-roles.js +12 -12
- package/dist/lib/mysql/schemas/org/organization-memberships.cjs +3 -2
- package/dist/lib/mysql/schemas/org/organization-memberships.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organization-memberships.d.cts +2 -1
- package/dist/lib/mysql/schemas/org/organization-memberships.d.ts +2 -1
- package/dist/lib/mysql/schemas/org/organization-memberships.js +12 -12
- package/dist/lib/mysql/schemas/org/organizations.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organizations.js +12 -12
- package/dist/lib/mysql/utils.js +1 -1
- package/dist/lib/utils.d.cts +1 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +1 -1
- package/package.json +7 -7
- package/src/lib/mysql/index.ts +1887 -153
- package/src/lib/mysql/schemas/auth/accounts.ts +7 -3
- package/src/lib/mysql/schemas/auth/device-access-token.ts +7 -3
- package/src/lib/mysql/schemas/auth/device-verification.ts +7 -3
- package/src/lib/mysql/schemas/auth/profiles.ts +7 -3
- package/src/lib/mysql/schemas/auth/sessions.ts +7 -3
- package/src/lib/mysql/schemas/auth/user-permissions.ts +7 -3
- package/src/lib/mysql/schemas/auth/user-prefs.ts +7 -3
- package/src/lib/mysql/schemas/auth/user-roles.ts +7 -3
- package/src/lib/mysql/schemas/auth/users.ts +17 -3
- package/src/lib/mysql/schemas/commerce/coupon.ts +2 -1
- package/src/lib/mysql/schemas/commerce/merchant-coupon.ts +2 -1
- package/src/lib/mysql/schemas/commerce/merchant-events.ts +62 -0
- package/src/lib/mysql/schemas/commerce/purchase-user-transfer.ts +4 -2
- package/src/lib/mysql/schemas/commerce/purchase.ts +7 -3
- package/src/lib/mysql/schemas/communication/comment.ts +7 -3
- package/src/lib/mysql/schemas/communication/communication-preferences.ts +4 -2
- package/src/lib/mysql/schemas/communication/question-response.ts +90 -0
- package/src/lib/mysql/schemas/content/content-contributions.ts +4 -2
- package/src/lib/mysql/schemas/content/content-resource-version.ts +4 -2
- package/src/lib/mysql/schemas/content/content-resource.ts +13 -3
- package/src/lib/mysql/schemas/entitlements/entitlement.ts +7 -3
- package/src/lib/mysql/schemas/org/organization-memberships.ts +4 -2
- package/dist/chunk-H736K5TN.js +0 -7
package/src/lib/mysql/index.ts
CHANGED
|
@@ -28,11 +28,15 @@ import { type CourseBuilderAdapter } from '@coursebuilder/core/adapters'
|
|
|
28
28
|
import {
|
|
29
29
|
Coupon,
|
|
30
30
|
couponSchema,
|
|
31
|
+
Entitlement,
|
|
32
|
+
entitlementSchema,
|
|
31
33
|
MerchantCharge,
|
|
32
34
|
merchantChargeSchema,
|
|
33
35
|
MerchantCoupon,
|
|
34
36
|
merchantCouponSchema,
|
|
35
37
|
MerchantCustomer,
|
|
38
|
+
MerchantEvents,
|
|
39
|
+
merchantEventsSchema,
|
|
36
40
|
merchantPriceSchema,
|
|
37
41
|
MerchantProduct,
|
|
38
42
|
merchantProductSchema,
|
|
@@ -72,7 +76,7 @@ import { type ModuleProgress } from '@coursebuilder/core/schemas/resource-progre
|
|
|
72
76
|
import { SubscriptionSchema } from '@coursebuilder/core/schemas/subscription'
|
|
73
77
|
import { VideoResourceSchema } from '@coursebuilder/core/schemas/video-resource'
|
|
74
78
|
import { PaymentsProviderConfig } from '@coursebuilder/core/types'
|
|
75
|
-
import { logger } from '@coursebuilder/core/utils/logger'
|
|
79
|
+
import { logger as baseLogger } from '@coursebuilder/core/utils/logger'
|
|
76
80
|
import { validateCoupon } from '@coursebuilder/core/utils/validate-coupon'
|
|
77
81
|
|
|
78
82
|
import {
|
|
@@ -122,6 +126,7 @@ import {
|
|
|
122
126
|
import {
|
|
123
127
|
getUsersRelationsSchema,
|
|
124
128
|
getUsersSchema,
|
|
129
|
+
type UsersSchemaOverrides,
|
|
125
130
|
} from './schemas/auth/users.js'
|
|
126
131
|
import { getVerificationTokensSchema } from './schemas/auth/verification-tokens.js'
|
|
127
132
|
import {
|
|
@@ -135,6 +140,10 @@ import {
|
|
|
135
140
|
} from './schemas/commerce/merchant-charge.js'
|
|
136
141
|
import { getMerchantCouponSchema } from './schemas/commerce/merchant-coupon.js'
|
|
137
142
|
import { getMerchantCustomerSchema } from './schemas/commerce/merchant-customer.js'
|
|
143
|
+
import {
|
|
144
|
+
getMerchantEventsRelationsSchema,
|
|
145
|
+
getMerchantEventsSchema,
|
|
146
|
+
} from './schemas/commerce/merchant-events.js'
|
|
138
147
|
import { getMerchantPriceSchema } from './schemas/commerce/merchant-price.js'
|
|
139
148
|
import { getMerchantProductSchema } from './schemas/commerce/merchant-product.js'
|
|
140
149
|
import { getMerchantSessionSchema } from './schemas/commerce/merchant-session.js'
|
|
@@ -173,6 +182,10 @@ import {
|
|
|
173
182
|
getCommunicationPreferencesRelationsSchema,
|
|
174
183
|
getCommunicationPreferencesSchema,
|
|
175
184
|
} from './schemas/communication/communication-preferences.js'
|
|
185
|
+
import {
|
|
186
|
+
getQuestionResponseRelationsSchema,
|
|
187
|
+
getQuestionResponseSchema,
|
|
188
|
+
} from './schemas/communication/question-response.js'
|
|
176
189
|
import {
|
|
177
190
|
getContentContributionRelationsSchema,
|
|
178
191
|
getContentContributionsSchema,
|
|
@@ -228,10 +241,60 @@ import {
|
|
|
228
241
|
|
|
229
242
|
export const guid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 5)
|
|
230
243
|
|
|
231
|
-
export
|
|
244
|
+
export const normalizeExpirationDate = (date: Date | undefined) => {
|
|
245
|
+
if (!date) return undefined
|
|
246
|
+
|
|
247
|
+
// Create a new Date object for 23:59:59 UTC on the date part of the input date
|
|
248
|
+
// The input date should be a JS Date representing 00:00:00 LA time for the chosen day.
|
|
249
|
+
// Its UTC date parts (getUTCFullYear, etc.) will give us the correct calendar day.
|
|
250
|
+
return new Date(
|
|
251
|
+
Date.UTC(
|
|
252
|
+
date.getUTCFullYear(),
|
|
253
|
+
date.getUTCMonth(), // 0-indexed
|
|
254
|
+
date.getUTCDate(),
|
|
255
|
+
23, // hours
|
|
256
|
+
59, // minutes
|
|
257
|
+
59, // seconds
|
|
258
|
+
0, // milliseconds
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function toError(error: unknown) {
|
|
264
|
+
return error instanceof Error ? error : new Error(String(error))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const logger = baseLogger as unknown as {
|
|
268
|
+
debug: (message: string, attrs?: Record<string, unknown>) => void
|
|
269
|
+
error: (error: Error, attrs?: Record<string, unknown>) => void
|
|
270
|
+
info: (message: string, attrs?: Record<string, unknown>) => void
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export type CourseBuilderSchemaOverrides<
|
|
274
|
+
TUsers extends ReturnType<typeof getUsersSchema> = ReturnType<
|
|
275
|
+
typeof getUsersSchema
|
|
276
|
+
>,
|
|
277
|
+
> = UsersSchemaOverrides<TUsers>
|
|
278
|
+
|
|
279
|
+
export function getCourseBuilderSchema<
|
|
280
|
+
TUsers extends ReturnType<typeof getUsersSchema> = ReturnType<
|
|
281
|
+
typeof getUsersSchema
|
|
282
|
+
>,
|
|
283
|
+
>(
|
|
284
|
+
mysqlTable: MySqlTableFn,
|
|
285
|
+
overrides: CourseBuilderSchemaOverrides<TUsers> = {},
|
|
286
|
+
) {
|
|
287
|
+
const resolvedGetUsersSchema = (overrides.getUsersSchema ??
|
|
288
|
+
getUsersSchema) as (mysqlTable: MySqlTableFn) => TUsers
|
|
289
|
+
const userRelationsOverrides: UsersSchemaOverrides<TUsers> = {
|
|
290
|
+
getUsersSchema: resolvedGetUsersSchema,
|
|
291
|
+
}
|
|
232
292
|
return {
|
|
233
293
|
accounts: getAccountsSchema(mysqlTable),
|
|
234
|
-
accountsRelations: getAccountsRelationsSchema(
|
|
294
|
+
accountsRelations: getAccountsRelationsSchema(
|
|
295
|
+
mysqlTable,
|
|
296
|
+
userRelationsOverrides,
|
|
297
|
+
),
|
|
235
298
|
permissions: getPermissionsSchema(mysqlTable),
|
|
236
299
|
permissionsRelations: getPermissionsRelationsSchema(mysqlTable),
|
|
237
300
|
rolePermissions: getRolePermissionsSchema(mysqlTable),
|
|
@@ -239,13 +302,22 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
|
|
|
239
302
|
roles: getRolesSchema(mysqlTable),
|
|
240
303
|
rolesRelations: getRolesRelationsSchema(mysqlTable),
|
|
241
304
|
sessions: getSessionsSchema(mysqlTable),
|
|
242
|
-
sessionsRelations: getSessionRelationsSchema(
|
|
305
|
+
sessionsRelations: getSessionRelationsSchema(
|
|
306
|
+
mysqlTable,
|
|
307
|
+
userRelationsOverrides,
|
|
308
|
+
),
|
|
243
309
|
userPermissions: getUserPermissionsSchema(mysqlTable),
|
|
244
|
-
userPermissionsRelations: getUserPermissionsRelationsSchema(
|
|
310
|
+
userPermissionsRelations: getUserPermissionsRelationsSchema(
|
|
311
|
+
mysqlTable,
|
|
312
|
+
userRelationsOverrides,
|
|
313
|
+
),
|
|
245
314
|
userRoles: getUserRolesSchema(mysqlTable),
|
|
246
|
-
userRolesRelations: getUserRolesRelationsSchema(
|
|
247
|
-
|
|
248
|
-
|
|
315
|
+
userRolesRelations: getUserRolesRelationsSchema(
|
|
316
|
+
mysqlTable,
|
|
317
|
+
userRelationsOverrides,
|
|
318
|
+
),
|
|
319
|
+
users: resolvedGetUsersSchema(mysqlTable),
|
|
320
|
+
usersRelations: getUsersRelationsSchema(mysqlTable, userRelationsOverrides),
|
|
249
321
|
verificationTokens: getVerificationTokensSchema(mysqlTable),
|
|
250
322
|
coupon: getCouponSchema(mysqlTable),
|
|
251
323
|
couponRelations: getCouponRelationsSchema(mysqlTable),
|
|
@@ -253,6 +325,8 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
|
|
|
253
325
|
merchantAccount: getMerchantAccountSchema(mysqlTable),
|
|
254
326
|
merchantCharge: getMerchantChargeSchema(mysqlTable),
|
|
255
327
|
merchantChargeRelations: getMerchantChargeRelationsSchema(mysqlTable),
|
|
328
|
+
merchantEvents: getMerchantEventsSchema(mysqlTable),
|
|
329
|
+
merchantEventsRelations: getMerchantEventsRelationsSchema(mysqlTable),
|
|
256
330
|
merchantCoupon: getMerchantCouponSchema(mysqlTable),
|
|
257
331
|
merchantCustomer: getMerchantCustomerSchema(mysqlTable),
|
|
258
332
|
merchantPrice: getMerchantPriceSchema(mysqlTable),
|
|
@@ -261,24 +335,39 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
|
|
|
261
335
|
prices: getPriceSchema(mysqlTable),
|
|
262
336
|
products: getProductSchema(mysqlTable),
|
|
263
337
|
purchases: getPurchaseSchema(mysqlTable),
|
|
264
|
-
purchaseRelations: getPurchaseRelationsSchema(
|
|
338
|
+
purchaseRelations: getPurchaseRelationsSchema(
|
|
339
|
+
mysqlTable,
|
|
340
|
+
userRelationsOverrides,
|
|
341
|
+
),
|
|
265
342
|
purchaseUserTransfer: getPurchaseUserTransferSchema(mysqlTable),
|
|
266
|
-
purchaseUserTransferRelations:
|
|
267
|
-
|
|
343
|
+
purchaseUserTransferRelations: getPurchaseUserTransferRelationsSchema(
|
|
344
|
+
mysqlTable,
|
|
345
|
+
userRelationsOverrides,
|
|
346
|
+
),
|
|
268
347
|
communicationChannel: getCommunicationChannelSchema(mysqlTable),
|
|
269
348
|
communicationPreferenceTypes:
|
|
270
349
|
getCommunicationPreferenceTypesSchema(mysqlTable),
|
|
271
350
|
communicationPreferences: getCommunicationPreferencesSchema(mysqlTable),
|
|
272
351
|
communicationPreferencesRelations:
|
|
273
|
-
getCommunicationPreferencesRelationsSchema(
|
|
352
|
+
getCommunicationPreferencesRelationsSchema(
|
|
353
|
+
mysqlTable,
|
|
354
|
+
userRelationsOverrides,
|
|
355
|
+
),
|
|
274
356
|
contentContributions: getContentContributionsSchema(mysqlTable),
|
|
275
|
-
contentContributionRelations:
|
|
276
|
-
|
|
357
|
+
contentContributionRelations: getContentContributionRelationsSchema(
|
|
358
|
+
mysqlTable,
|
|
359
|
+
userRelationsOverrides,
|
|
360
|
+
),
|
|
277
361
|
contentResource: getContentResourceSchema(mysqlTable),
|
|
278
362
|
contentResourceVersion: getContentResourceVersionSchema(mysqlTable),
|
|
279
|
-
contentResourceVersionRelations:
|
|
280
|
-
|
|
281
|
-
|
|
363
|
+
contentResourceVersionRelations: getContentResourceVersionRelationsSchema(
|
|
364
|
+
mysqlTable,
|
|
365
|
+
userRelationsOverrides,
|
|
366
|
+
),
|
|
367
|
+
contentResourceRelations: getContentResourceRelationsSchema(
|
|
368
|
+
mysqlTable,
|
|
369
|
+
userRelationsOverrides,
|
|
370
|
+
),
|
|
282
371
|
contentResourceResource: getContentResourceResourceSchema(mysqlTable),
|
|
283
372
|
contentResourceResourceRelations:
|
|
284
373
|
getContentResourceResourceRelationsSchema(mysqlTable),
|
|
@@ -288,6 +377,11 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
|
|
|
288
377
|
contributionTypes: getContributionTypesSchema(mysqlTable),
|
|
289
378
|
contributionTypesRelations: getContributionTypesRelationsSchema(mysqlTable),
|
|
290
379
|
resourceProgress: getResourceProgressSchema(mysqlTable),
|
|
380
|
+
questionResponse: getQuestionResponseSchema(mysqlTable),
|
|
381
|
+
questionResponseRelations: getQuestionResponseRelationsSchema(
|
|
382
|
+
mysqlTable,
|
|
383
|
+
userRelationsOverrides,
|
|
384
|
+
),
|
|
291
385
|
upgradableProducts: getUpgradableProductsSchema(mysqlTable),
|
|
292
386
|
upgradableProductsRelations:
|
|
293
387
|
getUpgradableProductsRelationsSchema(mysqlTable),
|
|
@@ -296,23 +390,36 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
|
|
|
296
390
|
getContentResourceProductRelationsSchema(mysqlTable),
|
|
297
391
|
productRelations: getProductRelationsSchema(mysqlTable),
|
|
298
392
|
comments: getCommentsSchema(mysqlTable),
|
|
299
|
-
commentsRelations: getCommentRelationsSchema(
|
|
393
|
+
commentsRelations: getCommentRelationsSchema(
|
|
394
|
+
mysqlTable,
|
|
395
|
+
userRelationsOverrides,
|
|
396
|
+
),
|
|
300
397
|
deviceVerifications: getDeviceVerificationSchema(mysqlTable),
|
|
301
|
-
deviceVerificationRelations:
|
|
302
|
-
|
|
398
|
+
deviceVerificationRelations: getDeviceVerificationRelationsSchema(
|
|
399
|
+
mysqlTable,
|
|
400
|
+
userRelationsOverrides,
|
|
401
|
+
),
|
|
303
402
|
deviceAccessToken: getDeviceAccessTokenSchema(mysqlTable),
|
|
304
|
-
deviceAccessTokenRelations: getDeviceAccessTokenRelationsSchema(
|
|
403
|
+
deviceAccessTokenRelations: getDeviceAccessTokenRelationsSchema(
|
|
404
|
+
mysqlTable,
|
|
405
|
+
userRelationsOverrides,
|
|
406
|
+
),
|
|
305
407
|
tag: getTagSchema(mysqlTable),
|
|
306
408
|
tagRelations: getTagRelationsSchema(mysqlTable),
|
|
307
409
|
tagTag: getTagTagSchema(mysqlTable),
|
|
308
410
|
tagTagRelations: getTagTagRelationsSchema(mysqlTable),
|
|
309
411
|
userPrefs: getUserPrefsSchema(mysqlTable),
|
|
310
|
-
userPrefsRelations: getUserPrefsRelationsSchema(
|
|
412
|
+
userPrefsRelations: getUserPrefsRelationsSchema(
|
|
413
|
+
mysqlTable,
|
|
414
|
+
userRelationsOverrides,
|
|
415
|
+
),
|
|
311
416
|
organization: getOrganizationsSchema(mysqlTable),
|
|
312
417
|
organizationRelations: getOrganizationsRelationsSchema(mysqlTable),
|
|
313
418
|
organizationMemberships: getOrganizationMembershipsSchema(mysqlTable),
|
|
314
|
-
organizationMembershipRelations:
|
|
315
|
-
|
|
419
|
+
organizationMembershipRelations: getOrganizationMembershipsRelationsSchema(
|
|
420
|
+
mysqlTable,
|
|
421
|
+
userRelationsOverrides,
|
|
422
|
+
),
|
|
316
423
|
organizationMembershipRoles:
|
|
317
424
|
getOrganizationMembershipRolesSchema(mysqlTable),
|
|
318
425
|
organizationMembershipRolesRelations:
|
|
@@ -323,10 +430,16 @@ export function getCourseBuilderSchema(mysqlTable: MySqlTableFn) {
|
|
|
323
430
|
subscription: getSubscriptionSchema(mysqlTable),
|
|
324
431
|
subscriptionRelations: getSubscriptionRelationsSchema(mysqlTable),
|
|
325
432
|
profiles: getProfilesSchema(mysqlTable),
|
|
326
|
-
profilesRelations: getProfilesRelationsSchema(
|
|
433
|
+
profilesRelations: getProfilesRelationsSchema(
|
|
434
|
+
mysqlTable,
|
|
435
|
+
userRelationsOverrides,
|
|
436
|
+
),
|
|
327
437
|
entitlementTypes: getEntitlementTypesSchema(mysqlTable),
|
|
328
438
|
entitlements: getEntitlementsSchema(mysqlTable),
|
|
329
|
-
entitlementsRelations: getEntitlementRelationsSchema(
|
|
439
|
+
entitlementsRelations: getEntitlementRelationsSchema(
|
|
440
|
+
mysqlTable,
|
|
441
|
+
userRelationsOverrides,
|
|
442
|
+
),
|
|
330
443
|
} as const
|
|
331
444
|
}
|
|
332
445
|
|
|
@@ -349,12 +462,14 @@ export function mySqlDrizzleAdapter(
|
|
|
349
462
|
contentResource,
|
|
350
463
|
contentResourceResource,
|
|
351
464
|
contentResourceProduct,
|
|
465
|
+
contentResourceTag,
|
|
352
466
|
purchases: purchaseTable,
|
|
353
467
|
purchaseUserTransfer,
|
|
354
468
|
coupon,
|
|
355
469
|
merchantCoupon,
|
|
356
470
|
merchantCharge,
|
|
357
471
|
merchantAccount,
|
|
472
|
+
merchantEvents,
|
|
358
473
|
merchantPrice,
|
|
359
474
|
merchantCustomer,
|
|
360
475
|
merchantSession,
|
|
@@ -370,6 +485,8 @@ export function mySqlDrizzleAdapter(
|
|
|
370
485
|
roles: roleTable,
|
|
371
486
|
merchantSubscription: merchantSubscriptionTable,
|
|
372
487
|
subscription: subscriptionTable,
|
|
488
|
+
entitlements: entitlementTable,
|
|
489
|
+
entitlementTypes,
|
|
373
490
|
} = createTables(tableFn)
|
|
374
491
|
|
|
375
492
|
const adapter: CourseBuilderAdapter = {
|
|
@@ -420,7 +537,13 @@ export function mySqlDrizzleAdapter(
|
|
|
420
537
|
|
|
421
538
|
if (existingPurchases.length > 0) {
|
|
422
539
|
const errorMessage = `already-purchased-${email}`
|
|
423
|
-
|
|
540
|
+
logger.info('adapter.coupon.redeem.already_purchased', {
|
|
541
|
+
couponId: coupon.id,
|
|
542
|
+
currentUserId,
|
|
543
|
+
email,
|
|
544
|
+
productId,
|
|
545
|
+
userId: user.id,
|
|
546
|
+
})
|
|
424
547
|
return {
|
|
425
548
|
error: {
|
|
426
549
|
message: errorMessage,
|
|
@@ -510,6 +633,21 @@ export function mySqlDrizzleAdapter(
|
|
|
510
633
|
}),
|
|
511
634
|
)
|
|
512
635
|
},
|
|
636
|
+
createMerchantCoupon: async (options) => {
|
|
637
|
+
await client.insert(merchantCoupon).values({
|
|
638
|
+
id: `mcoupon_${v4()}`,
|
|
639
|
+
identifier: options.identifier,
|
|
640
|
+
merchantAccountId: options.merchantAccountId,
|
|
641
|
+
type: options.type,
|
|
642
|
+
amountDiscount: Math.floor(options.amountDiscount),
|
|
643
|
+
status: 1,
|
|
644
|
+
})
|
|
645
|
+
return merchantCouponSchema.parse(
|
|
646
|
+
await client.query.merchantCoupon.findFirst({
|
|
647
|
+
where: eq(merchantCoupon.identifier, options.identifier),
|
|
648
|
+
}),
|
|
649
|
+
)
|
|
650
|
+
},
|
|
513
651
|
getMerchantAccount: async (options) => {
|
|
514
652
|
return merchantAccountSchema.parse(
|
|
515
653
|
await client.query.merchantAccount.findFirst({
|
|
@@ -529,10 +667,10 @@ export function mySqlDrizzleAdapter(
|
|
|
529
667
|
merchantPriceSchema.safeParse(merchantPriceData)
|
|
530
668
|
|
|
531
669
|
if (!parsedMerchantPrice.success) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
)
|
|
670
|
+
logger.error(parsedMerchantPrice.error, {
|
|
671
|
+
event: 'adapter.merchantPrice.parse_failed',
|
|
672
|
+
productId,
|
|
673
|
+
})
|
|
536
674
|
return null
|
|
537
675
|
}
|
|
538
676
|
|
|
@@ -638,7 +776,11 @@ export function mySqlDrizzleAdapter(
|
|
|
638
776
|
resourceProgressSchema.safeParse(lessonProgress)
|
|
639
777
|
|
|
640
778
|
if (!parsedLessonProgress.success) {
|
|
641
|
-
|
|
779
|
+
logger.error(parsedLessonProgress.error, {
|
|
780
|
+
event: 'adapter.lessonProgress.parse_failed',
|
|
781
|
+
lessonId: options.lessonId,
|
|
782
|
+
userId: options.userId,
|
|
783
|
+
})
|
|
642
784
|
return null
|
|
643
785
|
}
|
|
644
786
|
|
|
@@ -680,10 +822,11 @@ export function mySqlDrizzleAdapter(
|
|
|
680
822
|
.safeParse(couponForIdOrCode)
|
|
681
823
|
|
|
682
824
|
if (!parsedCoupon.success) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
825
|
+
logger.error(parsedCoupon.error, {
|
|
826
|
+
code: options.code,
|
|
827
|
+
couponId: options.couponId,
|
|
828
|
+
event: 'adapter.coupon.parse_failed',
|
|
829
|
+
})
|
|
687
830
|
return null
|
|
688
831
|
}
|
|
689
832
|
|
|
@@ -817,15 +960,35 @@ export function mySqlDrizzleAdapter(
|
|
|
817
960
|
})
|
|
818
961
|
.where(eq(coupon.id, bulkCouponId))
|
|
819
962
|
} else {
|
|
820
|
-
|
|
963
|
+
// Try to find merchant coupon by stripeCouponId first
|
|
964
|
+
let merchantCouponToUse = stripeCouponId
|
|
821
965
|
? merchantCouponSchema.nullable().parse(
|
|
822
|
-
await client.query.merchantCoupon.findFirst({
|
|
966
|
+
(await client.query.merchantCoupon.findFirst({
|
|
823
967
|
where: eq(merchantCoupon.identifier, stripeCouponId),
|
|
824
|
-
}),
|
|
968
|
+
})) || null,
|
|
825
969
|
)
|
|
826
970
|
: null
|
|
827
971
|
|
|
828
|
-
|
|
972
|
+
// Fallback: If not found, try to find via usedCouponId from checkout session metadata
|
|
973
|
+
if (!merchantCouponToUse && usedCouponId) {
|
|
974
|
+
// First get the coupon record to find its merchantCouponId
|
|
975
|
+
const usedCoupon = couponSchema.nullable().parse(
|
|
976
|
+
(await client.query.coupon.findFirst({
|
|
977
|
+
where: eq(coupon.id, usedCouponId),
|
|
978
|
+
})) || null,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
// Then get the merchant coupon
|
|
982
|
+
if (usedCoupon?.merchantCouponId) {
|
|
983
|
+
merchantCouponToUse = merchantCouponSchema.nullable().parse(
|
|
984
|
+
(await client.query.merchantCoupon.findFirst({
|
|
985
|
+
where: eq(merchantCoupon.id, usedCoupon.merchantCouponId),
|
|
986
|
+
})) || null,
|
|
987
|
+
)
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const bulkCouponValues = {
|
|
829
992
|
id: bulkCouponId as string,
|
|
830
993
|
percentageDiscount: '1.0',
|
|
831
994
|
restrictedToProductId: productId,
|
|
@@ -837,7 +1000,11 @@ export function mySqlDrizzleAdapter(
|
|
|
837
1000
|
merchantCouponId: merchantCouponToUse.id,
|
|
838
1001
|
}
|
|
839
1002
|
: {}),
|
|
840
|
-
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
couponToUpdate = await client
|
|
1006
|
+
.insert(coupon)
|
|
1007
|
+
.values(bulkCouponValues)
|
|
841
1008
|
}
|
|
842
1009
|
}
|
|
843
1010
|
|
|
@@ -909,7 +1076,14 @@ export function mySqlDrizzleAdapter(
|
|
|
909
1076
|
|
|
910
1077
|
return purchaseId
|
|
911
1078
|
} catch (error) {
|
|
912
|
-
|
|
1079
|
+
logger.error(toError(error), {
|
|
1080
|
+
checkoutSessionId: options.checkoutSessionId,
|
|
1081
|
+
event: 'adapter.purchase.create.failed',
|
|
1082
|
+
organizationId: options.organizationId,
|
|
1083
|
+
productId: options.productId,
|
|
1084
|
+
stripeChargeId: options.stripeChargeId,
|
|
1085
|
+
userId: options.userId,
|
|
1086
|
+
})
|
|
913
1087
|
trx.rollback()
|
|
914
1088
|
throw error
|
|
915
1089
|
}
|
|
@@ -922,11 +1096,12 @@ export function mySqlDrizzleAdapter(
|
|
|
922
1096
|
)
|
|
923
1097
|
|
|
924
1098
|
if (!parsedPurchase.success) {
|
|
925
|
-
|
|
926
|
-
'
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1099
|
+
logger.error(parsedPurchase.error, {
|
|
1100
|
+
event: 'adapter.purchase.parse_failed',
|
|
1101
|
+
productId: options.productId,
|
|
1102
|
+
purchaseId: purchaseId as string,
|
|
1103
|
+
userId: options.userId,
|
|
1104
|
+
})
|
|
930
1105
|
throw new Error('Error parsing purchase')
|
|
931
1106
|
}
|
|
932
1107
|
|
|
@@ -957,6 +1132,45 @@ export function mySqlDrizzleAdapter(
|
|
|
957
1132
|
userId: options.user.id,
|
|
958
1133
|
})
|
|
959
1134
|
},
|
|
1135
|
+
async createMerchantEvent(options: {
|
|
1136
|
+
merchantAccountId: string
|
|
1137
|
+
identifier: string
|
|
1138
|
+
payload: Record<string, any>
|
|
1139
|
+
}): Promise<MerchantEvents> {
|
|
1140
|
+
const eventId = `me_${v4()}`
|
|
1141
|
+
|
|
1142
|
+
await client.insert(merchantEvents).values({
|
|
1143
|
+
id: eventId,
|
|
1144
|
+
merchantAccountId: options.merchantAccountId,
|
|
1145
|
+
identifier: options.identifier,
|
|
1146
|
+
payload: options.payload,
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
const createdEvent = await client.query.merchantEvents.findFirst({
|
|
1150
|
+
where: eq(merchantEvents.id, eventId),
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
return merchantEventsSchema.parse(createdEvent)
|
|
1154
|
+
},
|
|
1155
|
+
async getMerchantEventByIdentifier(
|
|
1156
|
+
identifier: string,
|
|
1157
|
+
): Promise<MerchantEvents | null> {
|
|
1158
|
+
const event = await client.query.merchantEvents.findFirst({
|
|
1159
|
+
where: eq(merchantEvents.identifier, identifier),
|
|
1160
|
+
})
|
|
1161
|
+
|
|
1162
|
+
return event ? merchantEventsSchema.parse(event) : null
|
|
1163
|
+
},
|
|
1164
|
+
async getMerchantEventsByAccount(
|
|
1165
|
+
merchantAccountId: string,
|
|
1166
|
+
): Promise<MerchantEvents[]> {
|
|
1167
|
+
const events = await client.query.merchantEvents.findMany({
|
|
1168
|
+
where: eq(merchantEvents.merchantAccountId, merchantAccountId),
|
|
1169
|
+
orderBy: [desc(merchantEvents.createdAt)],
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
return events.map((event) => merchantEventsSchema.parse(event))
|
|
1173
|
+
},
|
|
960
1174
|
async findOrCreateUser(
|
|
961
1175
|
email: string,
|
|
962
1176
|
name?: string | null,
|
|
@@ -973,10 +1187,13 @@ export function mySqlDrizzleAdapter(
|
|
|
973
1187
|
name,
|
|
974
1188
|
emailVerified: null,
|
|
975
1189
|
})
|
|
976
|
-
console.log('newUser', { newUser })
|
|
977
1190
|
if (!newUser) {
|
|
978
1191
|
throw new Error('Could not create user')
|
|
979
1192
|
}
|
|
1193
|
+
logger.info('adapter.user.created', {
|
|
1194
|
+
email,
|
|
1195
|
+
userId: newUser.id,
|
|
1196
|
+
})
|
|
980
1197
|
return {
|
|
981
1198
|
user: newUser as User,
|
|
982
1199
|
isNewUser: true,
|
|
@@ -997,7 +1214,10 @@ export function mySqlDrizzleAdapter(
|
|
|
997
1214
|
),
|
|
998
1215
|
})) || null
|
|
999
1216
|
|
|
1000
|
-
logger.debug('
|
|
1217
|
+
logger.debug('getCoupon', {
|
|
1218
|
+
couponIdOrCode,
|
|
1219
|
+
found: !!loadedCoupon,
|
|
1220
|
+
})
|
|
1001
1221
|
|
|
1002
1222
|
return couponSchema.nullable().parse(loadedCoupon)
|
|
1003
1223
|
},
|
|
@@ -1022,7 +1242,7 @@ export function mySqlDrizzleAdapter(
|
|
|
1022
1242
|
> {
|
|
1023
1243
|
logger.debug('getCouponWithBulkPurchases', { couponId })
|
|
1024
1244
|
let couponData
|
|
1025
|
-
let bulkCouponPurchases
|
|
1245
|
+
let bulkCouponPurchases: unknown[] = []
|
|
1026
1246
|
try {
|
|
1027
1247
|
couponData =
|
|
1028
1248
|
(await client.query.coupon.findFirst({
|
|
@@ -1033,8 +1253,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1033
1253
|
},
|
|
1034
1254
|
})) || null
|
|
1035
1255
|
} catch (e) {
|
|
1036
|
-
|
|
1037
|
-
|
|
1256
|
+
logger.error(e as Error, {
|
|
1257
|
+
method: 'getCouponWithBulkPurchases',
|
|
1258
|
+
couponId,
|
|
1259
|
+
step: 'load-coupon',
|
|
1260
|
+
})
|
|
1038
1261
|
}
|
|
1039
1262
|
|
|
1040
1263
|
try {
|
|
@@ -1044,10 +1267,16 @@ export function mySqlDrizzleAdapter(
|
|
|
1044
1267
|
user: true,
|
|
1045
1268
|
},
|
|
1046
1269
|
})
|
|
1047
|
-
|
|
1270
|
+
logger.debug('getCouponWithBulkPurchases.redeemed', {
|
|
1271
|
+
couponId,
|
|
1272
|
+
count: bulkCouponPurchases?.length ?? 0,
|
|
1273
|
+
})
|
|
1048
1274
|
} catch (e) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1275
|
+
logger.error(e as Error, {
|
|
1276
|
+
method: 'getCouponWithBulkPurchases',
|
|
1277
|
+
couponId,
|
|
1278
|
+
step: 'load-redeemed-purchases',
|
|
1279
|
+
})
|
|
1051
1280
|
}
|
|
1052
1281
|
|
|
1053
1282
|
if (!couponData) {
|
|
@@ -1073,11 +1302,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1073
1302
|
.safeParse(couponWithBulkPurchases)
|
|
1074
1303
|
|
|
1075
1304
|
if (!parsedCoupon.success) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
)
|
|
1305
|
+
logger.error(parsedCoupon.error, {
|
|
1306
|
+
couponId: couponData?.id ?? couponId,
|
|
1307
|
+
event: 'adapter.coupon.parse_failed',
|
|
1308
|
+
redeemedBulkCouponPurchasesCount: bulkCouponPurchases?.length ?? 0,
|
|
1309
|
+
})
|
|
1081
1310
|
return null
|
|
1082
1311
|
}
|
|
1083
1312
|
|
|
@@ -1141,7 +1370,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1141
1370
|
})
|
|
1142
1371
|
const parsed = z.array(resourceProgressSchema).safeParse(userProgress)
|
|
1143
1372
|
if (!parsed.success) {
|
|
1144
|
-
|
|
1373
|
+
logger.error(parsed.error, {
|
|
1374
|
+
event: 'adapter.userProgress.parse_failed',
|
|
1375
|
+
userId,
|
|
1376
|
+
})
|
|
1145
1377
|
return []
|
|
1146
1378
|
}
|
|
1147
1379
|
return parsed.data
|
|
@@ -1172,41 +1404,63 @@ export function mySqlDrizzleAdapter(
|
|
|
1172
1404
|
.transform((val) => (val ? new Date(val) : null)),
|
|
1173
1405
|
})
|
|
1174
1406
|
|
|
1175
|
-
// Execute the optimized query
|
|
1407
|
+
// Execute the optimized query - create a flattened, properly ordered list
|
|
1176
1408
|
const results: any = await client.execute(sql`
|
|
1177
|
-
|
|
1178
|
-
|
|
1409
|
+
WITH RECURSIVE workshop AS (
|
|
1410
|
+
SELECT id, fields->>'$.slug' AS slug
|
|
1411
|
+
FROM ${contentResource}
|
|
1412
|
+
WHERE id = ${moduleIdOrSlug} OR fields->>'$.slug' = ${moduleIdOrSlug}
|
|
1413
|
+
),
|
|
1414
|
+
-- Get all workshop resources with their positions
|
|
1415
|
+
workshop_structure AS (
|
|
1416
|
+
SELECT
|
|
1417
|
+
w.id AS workshop_id,
|
|
1418
|
+
crr.resourceId,
|
|
1419
|
+
crr.position AS position,
|
|
1179
1420
|
cr.type AS resource_type,
|
|
1180
1421
|
cr.fields->>'$.slug' AS resource_slug,
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1422
|
+
cr.id AS resource_content_id
|
|
1423
|
+
FROM workshop w
|
|
1424
|
+
JOIN ${contentResourceResource} crr ON w.id = crr.resourceOfId
|
|
1425
|
+
JOIN ${contentResource} cr ON crr.resourceId = cr.id
|
|
1426
|
+
ORDER BY crr.position
|
|
1427
|
+
),
|
|
1428
|
+
-- Recursively expand sections and maintain global ordering
|
|
1429
|
+
expanded_resources AS (
|
|
1430
|
+
-- Base case: direct lessons/posts from workshop
|
|
1431
|
+
SELECT
|
|
1432
|
+
workshop_id,
|
|
1433
|
+
position * 1000 AS global_order, -- Multiply by 1000 to leave room for section items
|
|
1434
|
+
resource_content_id AS resource_id,
|
|
1435
|
+
resource_type,
|
|
1436
|
+
resource_slug
|
|
1437
|
+
FROM workshop_structure
|
|
1438
|
+
WHERE resource_type IN ('lesson', 'post')
|
|
1439
|
+
UNION ALL
|
|
1440
|
+
-- Recursive case: lessons within sections
|
|
1441
|
+
SELECT
|
|
1442
|
+
ws.workshop_id,
|
|
1443
|
+
ws.position * 1000 + crr.position AS global_order, -- Section position * 1000 + lesson position
|
|
1444
|
+
cr.id AS resource_id,
|
|
1445
|
+
cr.type AS resource_type,
|
|
1446
|
+
cr.fields->>'$.slug' AS resource_slug
|
|
1447
|
+
FROM workshop_structure ws
|
|
1448
|
+
JOIN ${contentResourceResource} crr ON ws.resource_content_id = crr.resourceOfId
|
|
1449
|
+
JOIN ${contentResource} cr ON crr.resourceId = cr.id
|
|
1450
|
+
WHERE ws.resource_type = 'section'
|
|
1451
|
+
AND cr.type IN ('lesson', 'post')
|
|
1452
|
+
)
|
|
1453
|
+
SELECT
|
|
1454
|
+
er.resource_id,
|
|
1455
|
+
er.resource_type,
|
|
1456
|
+
er.resource_slug,
|
|
1457
|
+
rp.completedAt AS completed_at,
|
|
1458
|
+
er.global_order
|
|
1459
|
+
FROM expanded_resources er
|
|
1460
|
+
LEFT JOIN ${resourceProgress} rp ON rp.resourceId = er.resource_id
|
|
1461
|
+
AND rp.userId = ${user.id}
|
|
1462
|
+
ORDER BY er.global_order ASC
|
|
1463
|
+
`)
|
|
1210
1464
|
// Process the results
|
|
1211
1465
|
const completedLessons: ResourceProgress[] = []
|
|
1212
1466
|
let nextResource: Partial<ContentResource> | null = null
|
|
@@ -1216,7 +1470,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1216
1470
|
const parsedRows = z.array(ResultRowSchema).safeParse(results.rows)
|
|
1217
1471
|
|
|
1218
1472
|
if (!parsedRows.success) {
|
|
1219
|
-
|
|
1473
|
+
logger.error(parsedRows.error, {
|
|
1474
|
+
event: 'adapter.moduleProgress.rows.parse_failed',
|
|
1475
|
+
moduleIdOrSlug,
|
|
1476
|
+
userId: user.id,
|
|
1477
|
+
})
|
|
1220
1478
|
return {
|
|
1221
1479
|
completedLessons: [],
|
|
1222
1480
|
nextResource: null,
|
|
@@ -1270,7 +1528,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1270
1528
|
})
|
|
1271
1529
|
const parsed = merchantChargeSchema.safeParse(mCharge)
|
|
1272
1530
|
if (!parsed.success) {
|
|
1273
|
-
|
|
1531
|
+
logger.error(parsed.error, {
|
|
1532
|
+
event: 'adapter.merchantCharge.parse_failed',
|
|
1533
|
+
merchantChargeId,
|
|
1534
|
+
})
|
|
1274
1535
|
return null
|
|
1275
1536
|
}
|
|
1276
1537
|
return parsed.data
|
|
@@ -1314,6 +1575,26 @@ export function mySqlDrizzleAdapter(
|
|
|
1314
1575
|
|
|
1315
1576
|
return null
|
|
1316
1577
|
},
|
|
1578
|
+
async getMerchantCouponForTypeAndAmount(params: {
|
|
1579
|
+
type: string
|
|
1580
|
+
amountDiscount: number
|
|
1581
|
+
}): Promise<MerchantCoupon | null> {
|
|
1582
|
+
const foundMerchantCoupon = await client.query.merchantCoupon.findFirst({
|
|
1583
|
+
where: and(
|
|
1584
|
+
eq(merchantCoupon.type, params.type),
|
|
1585
|
+
eq(merchantCoupon.amountDiscount, params.amountDiscount),
|
|
1586
|
+
),
|
|
1587
|
+
})
|
|
1588
|
+
|
|
1589
|
+
const parsed = merchantCouponSchema
|
|
1590
|
+
.nullable()
|
|
1591
|
+
.safeParse(foundMerchantCoupon)
|
|
1592
|
+
if (parsed.success) {
|
|
1593
|
+
return parsed.data
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
return null
|
|
1597
|
+
},
|
|
1317
1598
|
async getMerchantCoupon(
|
|
1318
1599
|
merchantCouponId: string,
|
|
1319
1600
|
): Promise<MerchantCoupon | null> {
|
|
@@ -1445,26 +1726,56 @@ export function mySqlDrizzleAdapter(
|
|
|
1445
1726
|
currentProduct.price.unitAmount.toString() !==
|
|
1446
1727
|
input.price?.unitAmount.toString()
|
|
1447
1728
|
|
|
1448
|
-
if
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1729
|
+
// Get current merchant price and Stripe price to check if it needs updating
|
|
1730
|
+
const currentMerchantPrice = merchantPriceSchema.nullish().parse(
|
|
1731
|
+
await client.query.merchantPrice.findFirst({
|
|
1732
|
+
where: (merchantPrice, { eq, and }) =>
|
|
1733
|
+
and(
|
|
1734
|
+
eq(merchantPrice.merchantProductId, merchantProduct.id),
|
|
1735
|
+
eq(merchantPrice.status, 1),
|
|
1736
|
+
),
|
|
1737
|
+
}),
|
|
1738
|
+
)
|
|
1458
1739
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1740
|
+
if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
|
|
1741
|
+
throw new Error(`Merchant price not found`)
|
|
1742
|
+
}
|
|
1462
1743
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1744
|
+
const currentStripePrice = await paymentProvider.getPrice(
|
|
1745
|
+
currentMerchantPrice.identifier,
|
|
1746
|
+
)
|
|
1466
1747
|
|
|
1467
|
-
|
|
1748
|
+
// Check if type changed to/from membership (requires Stripe price recreation)
|
|
1749
|
+
const membershipTypeChanged =
|
|
1750
|
+
currentProduct.type !== input.type &&
|
|
1751
|
+
(input.type === 'membership' || currentProduct.type === 'membership')
|
|
1752
|
+
|
|
1753
|
+
// Check if billing interval changed for membership products
|
|
1754
|
+
const billingIntervalChanged =
|
|
1755
|
+
input.type === 'membership' &&
|
|
1756
|
+
currentProduct.type === 'membership' &&
|
|
1757
|
+
currentProduct.fields?.billingInterval !== input.fields?.billingInterval
|
|
1758
|
+
|
|
1759
|
+
// Check if Stripe price recurring status doesn't match expected state for input type:
|
|
1760
|
+
// - membership products require recurring=true
|
|
1761
|
+
// - non-membership products require recurring=false (null/undefined)
|
|
1762
|
+
const isMembership = input.type === 'membership'
|
|
1763
|
+
const stripePriceMismatch =
|
|
1764
|
+
isMembership !== !!currentStripePrice.recurring
|
|
1765
|
+
|
|
1766
|
+
// Need to recreate price if: price changed, type changed to/from membership,
|
|
1767
|
+
// billing interval changed, or Stripe price doesn't match expected recurring status
|
|
1768
|
+
const needsPriceRecreation =
|
|
1769
|
+
priceChanged ||
|
|
1770
|
+
membershipTypeChanged ||
|
|
1771
|
+
billingIntervalChanged ||
|
|
1772
|
+
stripePriceMismatch
|
|
1773
|
+
|
|
1774
|
+
if (needsPriceRecreation) {
|
|
1775
|
+
// Get billing interval from input or default to 'year'
|
|
1776
|
+
const billingInterval = input.fields?.billingInterval || 'year'
|
|
1777
|
+
|
|
1778
|
+
const priceParams = {
|
|
1468
1779
|
product: stripeProduct.id,
|
|
1469
1780
|
unit_amount: Math.floor(Number(input.price?.unitAmount || 0) * 100),
|
|
1470
1781
|
currency: 'usd',
|
|
@@ -1472,8 +1783,26 @@ export function mySqlDrizzleAdapter(
|
|
|
1472
1783
|
slug: input.fields.slug,
|
|
1473
1784
|
},
|
|
1474
1785
|
active: true,
|
|
1786
|
+
// Membership products get recurring billing
|
|
1787
|
+
...(input.type === 'membership' && {
|
|
1788
|
+
recurring: { interval: billingInterval },
|
|
1789
|
+
}),
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
logger.debug('adapter.product.price_recreation.started', {
|
|
1793
|
+
billingInterval,
|
|
1794
|
+
currency: priceParams.currency,
|
|
1795
|
+
currentMerchantPriceId: currentMerchantPrice.id,
|
|
1796
|
+
isMembership: input.type === 'membership',
|
|
1797
|
+
merchantProductId: merchantProduct.id,
|
|
1798
|
+
productId: input.id,
|
|
1799
|
+
productSlug: input.fields.slug,
|
|
1800
|
+
stripeProductId: stripeProduct.id,
|
|
1801
|
+
unitAmount: priceParams.unit_amount,
|
|
1475
1802
|
})
|
|
1476
1803
|
|
|
1804
|
+
const newStripePrice = await paymentProvider.createPrice(priceParams)
|
|
1805
|
+
|
|
1477
1806
|
await paymentProvider.updateProduct(stripeProduct.id, {
|
|
1478
1807
|
default_price: newStripePrice.id,
|
|
1479
1808
|
})
|
|
@@ -1568,6 +1897,16 @@ export function mySqlDrizzleAdapter(
|
|
|
1568
1897
|
state: input.state || 'draft',
|
|
1569
1898
|
visibility: input.visibility || 'unlisted',
|
|
1570
1899
|
slug: slugify(`${input.name}-${hash}`),
|
|
1900
|
+
...(input.openEnrollment && { openEnrollment: input.openEnrollment }),
|
|
1901
|
+
...(input.closeEnrollment && {
|
|
1902
|
+
closeEnrollment: input.closeEnrollment,
|
|
1903
|
+
}),
|
|
1904
|
+
...(typeof input.availableAfterDays === 'number' && {
|
|
1905
|
+
availableAfterDays: input.availableAfterDays,
|
|
1906
|
+
}),
|
|
1907
|
+
...(typeof input.accessDurationDays === 'number' && {
|
|
1908
|
+
accessDurationDays: input.accessDurationDays,
|
|
1909
|
+
}),
|
|
1571
1910
|
},
|
|
1572
1911
|
}
|
|
1573
1912
|
|
|
@@ -1600,6 +1939,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1600
1939
|
metadata: {
|
|
1601
1940
|
slug: product?.fields?.slug || null,
|
|
1602
1941
|
},
|
|
1942
|
+
// Membership products get recurring billing
|
|
1943
|
+
...(input.type === 'membership' && {
|
|
1944
|
+
recurring: { interval: input.billingInterval || 'year' },
|
|
1945
|
+
}),
|
|
1603
1946
|
})
|
|
1604
1947
|
|
|
1605
1948
|
const newMerchantProductId = `mproduct_${v4()}`
|
|
@@ -1662,16 +2005,20 @@ export function mySqlDrizzleAdapter(
|
|
|
1662
2005
|
})
|
|
1663
2006
|
const parsedProduct = productSchema.safeParse(productData)
|
|
1664
2007
|
if (!parsedProduct.success) {
|
|
1665
|
-
|
|
1666
|
-
'
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
)
|
|
2008
|
+
logger.error(parsedProduct.error, {
|
|
2009
|
+
event: 'adapter.product.parse_failed',
|
|
2010
|
+
productId: productData?.id,
|
|
2011
|
+
productSlugOrId,
|
|
2012
|
+
})
|
|
1670
2013
|
return null
|
|
1671
2014
|
}
|
|
1672
2015
|
return parsedProduct.data
|
|
1673
2016
|
} catch (e) {
|
|
1674
|
-
|
|
2017
|
+
logger.error(toError(e), {
|
|
2018
|
+
event: 'adapter.product.lookup.failed',
|
|
2019
|
+
productSlugOrId,
|
|
2020
|
+
withResources,
|
|
2021
|
+
})
|
|
1675
2022
|
return null
|
|
1676
2023
|
}
|
|
1677
2024
|
},
|
|
@@ -1742,11 +2089,14 @@ export function mySqlDrizzleAdapter(
|
|
|
1742
2089
|
)
|
|
1743
2090
|
|
|
1744
2091
|
if (!chargeForPurchase) {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2092
|
+
// Expected during initial checkout processing — the merchant
|
|
2093
|
+
// charge hasn't been created yet. Also expected when the
|
|
2094
|
+
// payment-status-checker passes a PaymentIntent ID (pi_xxx)
|
|
2095
|
+
// instead of a Charge ID (ch_xxx).
|
|
2096
|
+
logger.debug('getPurchaseForStripeCharge: no matching charge', {
|
|
2097
|
+
stripeChargeId,
|
|
2098
|
+
idPrefix: stripeChargeId?.substring(0, 3),
|
|
2099
|
+
})
|
|
1750
2100
|
return null
|
|
1751
2101
|
}
|
|
1752
2102
|
|
|
@@ -1767,6 +2117,40 @@ export function mySqlDrizzleAdapter(
|
|
|
1767
2117
|
|
|
1768
2118
|
return purchase.data
|
|
1769
2119
|
},
|
|
2120
|
+
async getPurchaseByCheckoutSessionId(
|
|
2121
|
+
checkoutSessionId: string,
|
|
2122
|
+
): Promise<Purchase | null> {
|
|
2123
|
+
logger.debug('getPurchaseByCheckoutSessionId', { checkoutSessionId })
|
|
2124
|
+
|
|
2125
|
+
// Find the MerchantSession by its identifier (the Stripe checkout session ID)
|
|
2126
|
+
const session = await client.query.merchantSession.findFirst({
|
|
2127
|
+
where: eq(merchantSession.identifier, checkoutSessionId),
|
|
2128
|
+
})
|
|
2129
|
+
|
|
2130
|
+
if (!session) {
|
|
2131
|
+
logger.debug('getPurchaseByCheckoutSessionId: no matching session', {
|
|
2132
|
+
checkoutSessionId,
|
|
2133
|
+
})
|
|
2134
|
+
return null
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
const purchase = purchaseSchema.safeParse(
|
|
2138
|
+
await client.query.purchases.findFirst({
|
|
2139
|
+
where: eq(purchaseTable.merchantSessionId, String(session.id)),
|
|
2140
|
+
with: {
|
|
2141
|
+
user: true,
|
|
2142
|
+
product: true,
|
|
2143
|
+
bulkCoupon: true,
|
|
2144
|
+
},
|
|
2145
|
+
}),
|
|
2146
|
+
)
|
|
2147
|
+
|
|
2148
|
+
if (!purchase.success) {
|
|
2149
|
+
return null
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
return purchase.data
|
|
2153
|
+
},
|
|
1770
2154
|
async getPurchaseUserTransferById(options: { id: string }): Promise<
|
|
1771
2155
|
| (PurchaseUserTransfer & {
|
|
1772
2156
|
sourceUser: User
|
|
@@ -1817,7 +2201,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1817
2201
|
.safeParse(purchaseData)
|
|
1818
2202
|
|
|
1819
2203
|
if (!parsedPurchase.success) {
|
|
1820
|
-
|
|
2204
|
+
logger.error(parsedPurchase.error, {
|
|
2205
|
+
event: 'adapter.purchase.parse_failed',
|
|
2206
|
+
purchaseId,
|
|
2207
|
+
})
|
|
1821
2208
|
return null
|
|
1822
2209
|
}
|
|
1823
2210
|
return parsedPurchase.data
|
|
@@ -1845,15 +2232,85 @@ export function mySqlDrizzleAdapter(
|
|
|
1845
2232
|
const parsedPurchases = z.array(purchaseSchema).safeParse(userPurchases)
|
|
1846
2233
|
|
|
1847
2234
|
if (!parsedPurchases.success) {
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2235
|
+
logger.error(parsedPurchases.error, {
|
|
2236
|
+
event: 'adapter.purchase.list.parse_failed',
|
|
2237
|
+
purchaseCount: userPurchases.length,
|
|
2238
|
+
userId,
|
|
2239
|
+
})
|
|
1852
2240
|
return []
|
|
1853
2241
|
}
|
|
1854
2242
|
|
|
1855
2243
|
return parsedPurchases.data
|
|
1856
2244
|
},
|
|
2245
|
+
async getEntitlementsForUser(params: {
|
|
2246
|
+
userId: string
|
|
2247
|
+
sourceType?: string
|
|
2248
|
+
entitlementType?: string
|
|
2249
|
+
}): Promise<Entitlement[]> {
|
|
2250
|
+
const { userId, sourceType, entitlementType } = params
|
|
2251
|
+
|
|
2252
|
+
if (!userId) {
|
|
2253
|
+
return []
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
const conditions = [eq(entitlementTable.userId, userId)]
|
|
2257
|
+
|
|
2258
|
+
if (sourceType) {
|
|
2259
|
+
conditions.push(eq(entitlementTable.sourceType, sourceType))
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
if (entitlementType) {
|
|
2263
|
+
conditions.push(eq(entitlementTable.entitlementType, entitlementType))
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// Only return active entitlements (not deleted, not expired)
|
|
2267
|
+
conditions.push(isNull(entitlementTable.deletedAt))
|
|
2268
|
+
const expiresCondition = or(
|
|
2269
|
+
isNull(entitlementTable.expiresAt),
|
|
2270
|
+
gte(entitlementTable.expiresAt, sql`CURRENT_TIMESTAMP`),
|
|
2271
|
+
)
|
|
2272
|
+
if (expiresCondition) {
|
|
2273
|
+
conditions.push(expiresCondition)
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
const userEntitlements = await client.query.entitlements.findMany({
|
|
2277
|
+
where: conditions.length > 0 ? and(...conditions) : undefined,
|
|
2278
|
+
orderBy: asc(entitlementTable.createdAt),
|
|
2279
|
+
})
|
|
2280
|
+
|
|
2281
|
+
const parsedEntitlements = z
|
|
2282
|
+
.array(entitlementSchema)
|
|
2283
|
+
.safeParse(userEntitlements)
|
|
2284
|
+
|
|
2285
|
+
if (!parsedEntitlements.success) {
|
|
2286
|
+
logger.error(parsedEntitlements.error, {
|
|
2287
|
+
entitlementCount: userEntitlements.length,
|
|
2288
|
+
entitlementType,
|
|
2289
|
+
event: 'adapter.entitlement.list.parse_failed',
|
|
2290
|
+
sourceType,
|
|
2291
|
+
userId,
|
|
2292
|
+
})
|
|
2293
|
+
return []
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
return parsedEntitlements.data
|
|
2297
|
+
},
|
|
2298
|
+
async getEntitlementTypeByName(
|
|
2299
|
+
name: string,
|
|
2300
|
+
): Promise<{ id: string; name: string } | null> {
|
|
2301
|
+
const entitlementType = await client.query.entitlementTypes.findFirst({
|
|
2302
|
+
where: eq(entitlementTypes.name, name),
|
|
2303
|
+
})
|
|
2304
|
+
|
|
2305
|
+
if (!entitlementType) {
|
|
2306
|
+
return null
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
return {
|
|
2310
|
+
id: String(entitlementType.id),
|
|
2311
|
+
name: String(entitlementType.name),
|
|
2312
|
+
}
|
|
2313
|
+
},
|
|
1857
2314
|
async getPurchaseDetails(
|
|
1858
2315
|
purchaseId: string,
|
|
1859
2316
|
userId: string,
|
|
@@ -1896,7 +2353,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1896
2353
|
const parsedPurchase = purchaseSchema.safeParse(thePurchase)
|
|
1897
2354
|
|
|
1898
2355
|
if (!parsedPurchase.success) {
|
|
1899
|
-
|
|
2356
|
+
logger.error(parsedPurchase.error, {
|
|
2357
|
+
event: 'adapter.purchase.details.parse_failed',
|
|
2358
|
+
purchaseId,
|
|
2359
|
+
userId,
|
|
2360
|
+
})
|
|
1900
2361
|
return {
|
|
1901
2362
|
availableUpgrades: [],
|
|
1902
2363
|
}
|
|
@@ -2095,7 +2556,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2095
2556
|
resourceProgressSchema.safeParse(lessonProgress)
|
|
2096
2557
|
|
|
2097
2558
|
if (!parsedLessonProgress.success) {
|
|
2098
|
-
|
|
2559
|
+
logger.error(parsedLessonProgress.error, {
|
|
2560
|
+
event: 'adapter.lessonProgress.parse_failed',
|
|
2561
|
+
lessonId: options.lessonId,
|
|
2562
|
+
userId: options.userId,
|
|
2563
|
+
})
|
|
2099
2564
|
return null
|
|
2100
2565
|
}
|
|
2101
2566
|
|
|
@@ -2206,7 +2671,10 @@ export function mySqlDrizzleAdapter(
|
|
|
2206
2671
|
status: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',
|
|
2207
2672
|
): Promise<Purchase | undefined> {
|
|
2208
2673
|
try {
|
|
2209
|
-
|
|
2674
|
+
logger.debug('adapter.purchase.status_update.started', {
|
|
2675
|
+
chargeId,
|
|
2676
|
+
status,
|
|
2677
|
+
})
|
|
2210
2678
|
const merchantChargeForPurchase =
|
|
2211
2679
|
await client.query.merchantCharge.findFirst({
|
|
2212
2680
|
where: or(
|
|
@@ -2215,7 +2683,12 @@ export function mySqlDrizzleAdapter(
|
|
|
2215
2683
|
),
|
|
2216
2684
|
})
|
|
2217
2685
|
|
|
2218
|
-
|
|
2686
|
+
logger.debug('adapter.purchase.status_update.charge_resolved', {
|
|
2687
|
+
chargeId,
|
|
2688
|
+
merchantChargeId: merchantChargeForPurchase?.id ?? null,
|
|
2689
|
+
merchantChargeIdentifier:
|
|
2690
|
+
merchantChargeForPurchase?.identifier ?? null,
|
|
2691
|
+
})
|
|
2219
2692
|
|
|
2220
2693
|
const parsedMerchantChargeForPurchase = merchantChargeSchema.parse(
|
|
2221
2694
|
merchantChargeForPurchase,
|
|
@@ -2248,7 +2721,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2248
2721
|
throw new Error(`no-purchase-found-for-charge ${chargeId}`)
|
|
2249
2722
|
}
|
|
2250
2723
|
} catch (e) {
|
|
2251
|
-
|
|
2724
|
+
logger.error(toError(e), {
|
|
2725
|
+
chargeId,
|
|
2726
|
+
event: 'adapter.purchase.status_update.failed',
|
|
2727
|
+
status,
|
|
2728
|
+
})
|
|
2252
2729
|
throw e
|
|
2253
2730
|
}
|
|
2254
2731
|
},
|
|
@@ -2383,7 +2860,10 @@ export function mySqlDrizzleAdapter(
|
|
|
2383
2860
|
const parsedResource = ContentResourceSchema.safeParse(resource)
|
|
2384
2861
|
|
|
2385
2862
|
if (!parsedResource.success) {
|
|
2386
|
-
|
|
2863
|
+
logger.error(parsedResource.error, {
|
|
2864
|
+
event: 'adapter.contentResource.parse_failed',
|
|
2865
|
+
resourceId: options.id,
|
|
2866
|
+
})
|
|
2387
2867
|
return null
|
|
2388
2868
|
}
|
|
2389
2869
|
|
|
@@ -2418,10 +2898,63 @@ export function mySqlDrizzleAdapter(
|
|
|
2418
2898
|
return parsedResource.success ? parsedResource.data : null
|
|
2419
2899
|
})
|
|
2420
2900
|
.catch((error) => {
|
|
2421
|
-
|
|
2901
|
+
logger.error(toError(error), {
|
|
2902
|
+
event: 'adapter.videoResource.lookup.failed',
|
|
2903
|
+
resourceId: id,
|
|
2904
|
+
})
|
|
2422
2905
|
throw error
|
|
2423
2906
|
})
|
|
2424
2907
|
},
|
|
2908
|
+
async getParentResourceOfVideoResource(
|
|
2909
|
+
videoResourceId: string,
|
|
2910
|
+
): Promise<ContentResource | null> {
|
|
2911
|
+
if (!videoResourceId) {
|
|
2912
|
+
return null
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
// Use a SQL query to find the parent post or lesson of this video resource
|
|
2916
|
+
const results = await client
|
|
2917
|
+
.select()
|
|
2918
|
+
.from(contentResource)
|
|
2919
|
+
.innerJoin(
|
|
2920
|
+
contentResourceResource,
|
|
2921
|
+
eq(contentResourceResource.resourceOfId, contentResource.id),
|
|
2922
|
+
)
|
|
2923
|
+
.where(
|
|
2924
|
+
and(
|
|
2925
|
+
eq(contentResourceResource.resourceId, videoResourceId),
|
|
2926
|
+
or(
|
|
2927
|
+
eq(contentResource.type, 'post'),
|
|
2928
|
+
eq(contentResource.type, 'lesson'),
|
|
2929
|
+
),
|
|
2930
|
+
),
|
|
2931
|
+
)
|
|
2932
|
+
.limit(1)
|
|
2933
|
+
|
|
2934
|
+
if (!results || results.length === 0) {
|
|
2935
|
+
return null
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
const resourceData = results[0]?.ContentResource
|
|
2939
|
+
|
|
2940
|
+
if (!resourceData) {
|
|
2941
|
+
return null
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// Fetch the full resource with relationships
|
|
2945
|
+
const fullResource = await client.query.contentResource.findFirst({
|
|
2946
|
+
where: eq(contentResource.id, resourceData.id),
|
|
2947
|
+
with: {
|
|
2948
|
+
resources: true,
|
|
2949
|
+
},
|
|
2950
|
+
})
|
|
2951
|
+
|
|
2952
|
+
if (!fullResource) {
|
|
2953
|
+
return null
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
return ContentResourceSchema.parse(fullResource)
|
|
2957
|
+
},
|
|
2425
2958
|
async createContentResource(data) {
|
|
2426
2959
|
const id = data.id || crypto.randomUUID()
|
|
2427
2960
|
|
|
@@ -2451,7 +2984,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2451
2984
|
const parsedResource = ContentResourceSchema.safeParse(resource)
|
|
2452
2985
|
|
|
2453
2986
|
if (!parsedResource.success) {
|
|
2454
|
-
|
|
2987
|
+
logger.error(parsedResource.error, {
|
|
2988
|
+
event: 'adapter.contentResource.parse_failed',
|
|
2989
|
+
resourceId: id,
|
|
2990
|
+
resourceType: data.type,
|
|
2991
|
+
})
|
|
2455
2992
|
throw new Error('Error parsing resource')
|
|
2456
2993
|
}
|
|
2457
2994
|
|
|
@@ -2496,32 +3033,1216 @@ export function mySqlDrizzleAdapter(
|
|
|
2496
3033
|
const parsedResource = ContentResourceSchema.safeParse(resource)
|
|
2497
3034
|
|
|
2498
3035
|
if (!parsedResource.success) {
|
|
2499
|
-
|
|
3036
|
+
logger.error(parsedResource.error, {
|
|
3037
|
+
event: 'adapter.contentResource.parse_failed',
|
|
3038
|
+
resourceId: resource?.id,
|
|
3039
|
+
resourceIdOrSlug: data,
|
|
3040
|
+
})
|
|
2500
3041
|
return null
|
|
2501
3042
|
}
|
|
2502
3043
|
|
|
2503
3044
|
return parsedResource.data
|
|
2504
3045
|
},
|
|
2505
|
-
async
|
|
2506
|
-
|
|
2507
|
-
|
|
3046
|
+
async getEvent(
|
|
3047
|
+
eventIdOrSlug: string,
|
|
3048
|
+
options?: {
|
|
3049
|
+
withResources?: boolean
|
|
3050
|
+
withTags?: boolean
|
|
3051
|
+
withProducts?: boolean
|
|
3052
|
+
withPricing?: boolean
|
|
3053
|
+
},
|
|
3054
|
+
): Promise<ContentResource | null> {
|
|
3055
|
+
const {
|
|
3056
|
+
withResources = false,
|
|
3057
|
+
withTags = false,
|
|
3058
|
+
withProducts = false,
|
|
3059
|
+
withPricing = false,
|
|
3060
|
+
} = options || {}
|
|
2508
3061
|
|
|
2509
|
-
|
|
3062
|
+
const eventQuery = await client.query.contentResource.findFirst({
|
|
3063
|
+
where: and(
|
|
3064
|
+
or(
|
|
3065
|
+
eq(contentResource.type, 'event'),
|
|
3066
|
+
eq(contentResource.type, 'event-series'),
|
|
3067
|
+
),
|
|
3068
|
+
or(
|
|
3069
|
+
eq(contentResource.id, eventIdOrSlug),
|
|
3070
|
+
eq(
|
|
3071
|
+
sql`JSON_EXTRACT (${contentResource.fields}, "$.slug")`,
|
|
3072
|
+
eventIdOrSlug,
|
|
3073
|
+
),
|
|
3074
|
+
),
|
|
3075
|
+
),
|
|
3076
|
+
with: {
|
|
3077
|
+
...(withResources && {
|
|
3078
|
+
resources: {
|
|
3079
|
+
with: { resource: true },
|
|
3080
|
+
orderBy: asc(contentResourceResource.position),
|
|
3081
|
+
},
|
|
3082
|
+
}),
|
|
3083
|
+
...(withTags && {
|
|
3084
|
+
tags: {
|
|
3085
|
+
with: { tag: true },
|
|
3086
|
+
orderBy: asc(contentResourceTag.position),
|
|
3087
|
+
},
|
|
3088
|
+
}),
|
|
3089
|
+
...(withProducts && {
|
|
3090
|
+
resourceProducts: {
|
|
3091
|
+
with: {
|
|
3092
|
+
product: {
|
|
3093
|
+
...(withPricing && {
|
|
3094
|
+
with: { price: true },
|
|
3095
|
+
}),
|
|
3096
|
+
},
|
|
3097
|
+
},
|
|
3098
|
+
},
|
|
3099
|
+
}),
|
|
3100
|
+
},
|
|
3101
|
+
})
|
|
2510
3102
|
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
3103
|
+
const parsedEvent = ContentResourceSchema.safeParse(eventQuery)
|
|
3104
|
+
if (!parsedEvent.success) {
|
|
3105
|
+
logger.debug('Error parsing event', {
|
|
3106
|
+
eventQuery,
|
|
3107
|
+
error: parsedEvent.error,
|
|
2515
3108
|
})
|
|
3109
|
+
return null
|
|
3110
|
+
}
|
|
2516
3111
|
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
3112
|
+
return parsedEvent.data
|
|
3113
|
+
},
|
|
3114
|
+
async addResourceToProduct(options: {
|
|
3115
|
+
resource: ContentResource
|
|
3116
|
+
productId: string
|
|
3117
|
+
userId: string
|
|
3118
|
+
}) {
|
|
3119
|
+
const { resource, productId, userId } = options
|
|
3120
|
+
const product = await adapter.getProduct(productId)
|
|
2520
3121
|
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
3122
|
+
if (!product) {
|
|
3123
|
+
throw new Error(`Product not found for id (${productId})`)
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
await client.insert(contentResourceProduct).values({
|
|
3127
|
+
resourceId: resource.id,
|
|
3128
|
+
productId,
|
|
3129
|
+
position: product.resources?.length || 0,
|
|
3130
|
+
metadata: {
|
|
3131
|
+
addedBy: userId,
|
|
3132
|
+
},
|
|
3133
|
+
})
|
|
3134
|
+
|
|
3135
|
+
const resourceProduct =
|
|
3136
|
+
await client.query.contentResourceProduct.findFirst({
|
|
3137
|
+
where: eq(contentResourceProduct.resourceId, resource.id),
|
|
3138
|
+
with: {
|
|
3139
|
+
resource: true,
|
|
3140
|
+
product: true,
|
|
3141
|
+
},
|
|
3142
|
+
})
|
|
3143
|
+
|
|
3144
|
+
return (
|
|
3145
|
+
ContentResourceProductSchema.safeParse(resourceProduct).data || null
|
|
3146
|
+
)
|
|
3147
|
+
},
|
|
3148
|
+
async createEvent(
|
|
3149
|
+
input: {
|
|
3150
|
+
type: 'event'
|
|
3151
|
+
fields: {
|
|
3152
|
+
title: string
|
|
3153
|
+
startsAt?: Date | null | undefined
|
|
3154
|
+
endsAt?: Date | null | undefined
|
|
3155
|
+
description?: string | null | undefined
|
|
3156
|
+
price?: number | null | undefined
|
|
3157
|
+
quantity?: number | null | undefined
|
|
3158
|
+
state?: string | null | undefined
|
|
3159
|
+
visibility?: string | null | undefined
|
|
3160
|
+
slug?: string | null | undefined
|
|
3161
|
+
tagIds?:
|
|
3162
|
+
| { id: string; fields: Record<string, any> }[]
|
|
3163
|
+
| null
|
|
3164
|
+
| undefined
|
|
3165
|
+
}
|
|
3166
|
+
coupon?: {
|
|
3167
|
+
enabled: boolean
|
|
3168
|
+
percentageDiscount: string
|
|
3169
|
+
expires: Date
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
userId: string,
|
|
3173
|
+
) {
|
|
3174
|
+
const hash = guid()
|
|
3175
|
+
const newEventId = slugify(`${input.type}~${hash}`)
|
|
3176
|
+
|
|
3177
|
+
const newEvent = {
|
|
3178
|
+
id: newEventId,
|
|
3179
|
+
type: 'event',
|
|
3180
|
+
fields: {
|
|
3181
|
+
title: input.fields.title,
|
|
3182
|
+
state: 'draft',
|
|
3183
|
+
visibility: 'public',
|
|
3184
|
+
slug: slugify(`${input.fields.title}~${hash}`),
|
|
3185
|
+
description: input.fields.description,
|
|
3186
|
+
startsAt: input.fields.startsAt,
|
|
3187
|
+
endsAt: input.fields.endsAt,
|
|
3188
|
+
},
|
|
3189
|
+
createdById: userId,
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
await client.insert(contentResource).values(newEvent)
|
|
3193
|
+
const event = await adapter.getEvent(newEventId, {
|
|
3194
|
+
withResources: true,
|
|
3195
|
+
withTags: true,
|
|
3196
|
+
withProducts: true,
|
|
3197
|
+
withPricing: true,
|
|
3198
|
+
})
|
|
3199
|
+
|
|
3200
|
+
const parsedEvent = ContentResourceSchema.safeParse(event)
|
|
3201
|
+
|
|
3202
|
+
if (!parsedEvent.success) {
|
|
3203
|
+
logger.error(new Error('Error parsing event resource'))
|
|
3204
|
+
throw new Error('Error parsing event resource')
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// if price is provided, create a product and associate it with the event
|
|
3208
|
+
if (input.fields.price && input.fields.price > 0) {
|
|
3209
|
+
try {
|
|
3210
|
+
const product = await adapter.createProduct({
|
|
3211
|
+
name: input.fields.title,
|
|
3212
|
+
price: input.fields.price,
|
|
3213
|
+
quantityAvailable: input.fields.quantity ?? -1,
|
|
3214
|
+
type: 'live',
|
|
3215
|
+
state: 'published',
|
|
3216
|
+
visibility: 'public',
|
|
3217
|
+
})
|
|
3218
|
+
if (product) {
|
|
3219
|
+
const resourceProduct = await adapter.addResourceToProduct({
|
|
3220
|
+
resource: parsedEvent.data,
|
|
3221
|
+
productId: product.id,
|
|
3222
|
+
userId,
|
|
3223
|
+
})
|
|
3224
|
+
if (!resourceProduct) {
|
|
3225
|
+
logger.error(new Error('Failed to add resource to product'))
|
|
3226
|
+
logger.debug('event.addResourceToProduct.failed', {
|
|
3227
|
+
eventId: newEventId,
|
|
3228
|
+
productId: product.id,
|
|
3229
|
+
userId,
|
|
3230
|
+
})
|
|
3231
|
+
} else {
|
|
3232
|
+
logger.debug('event.addResourceToProduct.success', {
|
|
3233
|
+
eventId: newEventId,
|
|
3234
|
+
productId: product.id,
|
|
3235
|
+
userId,
|
|
3236
|
+
})
|
|
3237
|
+
}
|
|
3238
|
+
// Create coupon if enabled
|
|
3239
|
+
if (
|
|
3240
|
+
input.coupon?.enabled &&
|
|
3241
|
+
input.coupon.percentageDiscount &&
|
|
3242
|
+
input.coupon.expires
|
|
3243
|
+
) {
|
|
3244
|
+
try {
|
|
3245
|
+
const finalExpires = normalizeExpirationDate(
|
|
3246
|
+
input.coupon.expires,
|
|
3247
|
+
)
|
|
3248
|
+
const couponInput = {
|
|
3249
|
+
quantity: '1',
|
|
3250
|
+
maxUses: -1,
|
|
3251
|
+
expires: finalExpires || null,
|
|
3252
|
+
restrictedToProductId: product.id,
|
|
3253
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
3254
|
+
status: 1,
|
|
3255
|
+
default: true,
|
|
3256
|
+
fields: {
|
|
3257
|
+
bypassSoldOut: false,
|
|
3258
|
+
},
|
|
3259
|
+
}
|
|
3260
|
+
await adapter.createCoupon(couponInput)
|
|
3261
|
+
logger.debug('event.create.coupon.success', {
|
|
3262
|
+
eventId: newEventId,
|
|
3263
|
+
productId: product.id,
|
|
3264
|
+
userId,
|
|
3265
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
3266
|
+
})
|
|
3267
|
+
} catch (couponError) {
|
|
3268
|
+
logger.error(
|
|
3269
|
+
new Error(
|
|
3270
|
+
`Failed to create coupon for event: ${couponError}`,
|
|
3271
|
+
),
|
|
3272
|
+
)
|
|
3273
|
+
logger.debug('event.create.coupon.failed', {
|
|
3274
|
+
eventId: newEventId,
|
|
3275
|
+
productId: product.id,
|
|
3276
|
+
userId,
|
|
3277
|
+
})
|
|
3278
|
+
// Don't throw here - event creation should succeed even if coupon fails
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
} else {
|
|
3282
|
+
logger.error(new Error('Failed to create product'))
|
|
3283
|
+
logger.debug('event.create.product.failed', {
|
|
3284
|
+
eventId: newEventId,
|
|
3285
|
+
userId,
|
|
3286
|
+
price: input.fields.price,
|
|
3287
|
+
})
|
|
3288
|
+
}
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
logger.error(
|
|
3291
|
+
new Error(`Error creating and associating product: ${error}`),
|
|
3292
|
+
)
|
|
3293
|
+
logger.debug('event.create.product.failed', {
|
|
3294
|
+
eventId: newEventId,
|
|
3295
|
+
userId,
|
|
3296
|
+
price: input.fields.price,
|
|
3297
|
+
})
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// if we provide tagIds, we need to associate them with the event
|
|
3302
|
+
if (input.fields.tagIds) {
|
|
3303
|
+
try {
|
|
3304
|
+
await client.insert(contentResourceTag).values(
|
|
3305
|
+
input.fields.tagIds.map((tag) => ({
|
|
3306
|
+
contentResourceId: newEventId,
|
|
3307
|
+
tagId: tag.id,
|
|
3308
|
+
createdAt: new Date(),
|
|
3309
|
+
updatedAt: new Date(),
|
|
3310
|
+
position: 0,
|
|
3311
|
+
})),
|
|
3312
|
+
)
|
|
3313
|
+
logger.debug('event.create.tags.success', {
|
|
3314
|
+
eventId: newEventId,
|
|
3315
|
+
userId,
|
|
3316
|
+
tagIds: input.fields.tagIds,
|
|
3317
|
+
})
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
logger.error(new Error(`Error associating tags with event: ${error}`))
|
|
3320
|
+
logger.debug('event.create.tags.failed', {
|
|
3321
|
+
eventId: newEventId,
|
|
3322
|
+
userId,
|
|
3323
|
+
tagIds: input.fields.tagIds,
|
|
3324
|
+
})
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
return parsedEvent.data
|
|
3329
|
+
},
|
|
3330
|
+
async createEventSeries(
|
|
3331
|
+
input: {
|
|
3332
|
+
eventSeries: {
|
|
3333
|
+
type: 'event-series'
|
|
3334
|
+
fields: {
|
|
3335
|
+
title: string
|
|
3336
|
+
description?: string | undefined
|
|
3337
|
+
tagIds?:
|
|
3338
|
+
| { id: string; fields: { label: string; name: string } }[]
|
|
3339
|
+
| null
|
|
3340
|
+
| undefined
|
|
3341
|
+
}
|
|
3342
|
+
sharedFields: {
|
|
3343
|
+
price: number | null | undefined
|
|
3344
|
+
quantity: number | null | undefined
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
childEvents: Array<{
|
|
3348
|
+
type: 'event'
|
|
3349
|
+
fields: {
|
|
3350
|
+
title: string
|
|
3351
|
+
startsAt: Date | null | undefined
|
|
3352
|
+
endsAt: Date | null | undefined
|
|
3353
|
+
description?: string | undefined
|
|
3354
|
+
tagIds?:
|
|
3355
|
+
| { id: string; fields: { label: string; name: string } }[]
|
|
3356
|
+
| null
|
|
3357
|
+
| undefined
|
|
3358
|
+
}
|
|
3359
|
+
}>
|
|
3360
|
+
coupon?:
|
|
3361
|
+
| {
|
|
3362
|
+
enabled: boolean
|
|
3363
|
+
expires?: Date | undefined
|
|
3364
|
+
percentageDiscount?: string | undefined
|
|
3365
|
+
}
|
|
3366
|
+
| undefined
|
|
3367
|
+
},
|
|
3368
|
+
userId: string,
|
|
3369
|
+
) {
|
|
3370
|
+
const { eventSeries: eventSeriesInput, childEvents: childEventsInput } =
|
|
3371
|
+
input
|
|
3372
|
+
|
|
3373
|
+
if (childEventsInput.length === 0) {
|
|
3374
|
+
throw new Error('At least one event is required')
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
// Execute all database operations within a transaction
|
|
3378
|
+
const result = await client.transaction(async (tx) => {
|
|
3379
|
+
// Step 1: Create the event series resource
|
|
3380
|
+
const eventSeriesHash = guid()
|
|
3381
|
+
const eventSeriesResourceId = slugify(`event-series~${eventSeriesHash}`)
|
|
3382
|
+
|
|
3383
|
+
const newEventSeries = {
|
|
3384
|
+
id: eventSeriesResourceId,
|
|
3385
|
+
type: 'event-series',
|
|
3386
|
+
fields: {
|
|
3387
|
+
...eventSeriesInput.fields,
|
|
3388
|
+
title: eventSeriesInput.fields.title,
|
|
3389
|
+
state: 'draft',
|
|
3390
|
+
visibility: 'public',
|
|
3391
|
+
slug: slugify(
|
|
3392
|
+
`${eventSeriesInput.fields.title}~${eventSeriesHash}`,
|
|
3393
|
+
),
|
|
3394
|
+
},
|
|
3395
|
+
createdById: userId,
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
await tx.insert(contentResource).values(newEventSeries)
|
|
3399
|
+
|
|
3400
|
+
// Fetch the created event series with relations
|
|
3401
|
+
const eventSeriesResource = await tx.query.contentResource.findFirst({
|
|
3402
|
+
where: eq(contentResource.id, eventSeriesResourceId),
|
|
3403
|
+
with: {
|
|
3404
|
+
resources: {
|
|
3405
|
+
with: {
|
|
3406
|
+
resource: {
|
|
3407
|
+
with: {
|
|
3408
|
+
resources: {
|
|
3409
|
+
with: {
|
|
3410
|
+
resource: true,
|
|
3411
|
+
},
|
|
3412
|
+
orderBy: asc(contentResourceResource.position),
|
|
3413
|
+
},
|
|
3414
|
+
},
|
|
3415
|
+
},
|
|
3416
|
+
},
|
|
3417
|
+
orderBy: asc(contentResourceResource.position),
|
|
3418
|
+
},
|
|
3419
|
+
tags: {
|
|
3420
|
+
with: {
|
|
3421
|
+
tag: true,
|
|
3422
|
+
},
|
|
3423
|
+
orderBy: asc(contentResourceTag.position),
|
|
3424
|
+
},
|
|
3425
|
+
resourceProducts: {
|
|
3426
|
+
with: {
|
|
3427
|
+
product: {
|
|
3428
|
+
with: {
|
|
3429
|
+
price: true,
|
|
3430
|
+
},
|
|
3431
|
+
},
|
|
3432
|
+
},
|
|
3433
|
+
},
|
|
3434
|
+
},
|
|
3435
|
+
})
|
|
3436
|
+
|
|
3437
|
+
const parsedEventSeries =
|
|
3438
|
+
ContentResourceSchema.safeParse(eventSeriesResource)
|
|
3439
|
+
if (!parsedEventSeries.success) {
|
|
3440
|
+
throw new Error('Error parsing event series resource')
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
// Step 2: Associate tags with event series
|
|
3444
|
+
if (eventSeriesInput.fields.tagIds) {
|
|
3445
|
+
await tx.insert(contentResourceTag).values(
|
|
3446
|
+
eventSeriesInput.fields.tagIds.map((tag) => ({
|
|
3447
|
+
contentResourceId: eventSeriesResourceId,
|
|
3448
|
+
tagId: tag.id,
|
|
3449
|
+
createdAt: new Date(),
|
|
3450
|
+
updatedAt: new Date(),
|
|
3451
|
+
position: 0,
|
|
3452
|
+
})),
|
|
3453
|
+
)
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
// Step 3: Create child events and associate them
|
|
3457
|
+
const createdChildEvents = []
|
|
3458
|
+
|
|
3459
|
+
for (let i = 0; i < childEventsInput.length; i++) {
|
|
3460
|
+
const childEventInput = childEventsInput[i]
|
|
3461
|
+
|
|
3462
|
+
if (!childEventInput) {
|
|
3463
|
+
throw new Error(`Child event input is required`)
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
// Create child event
|
|
3467
|
+
const childEventHash = guid()
|
|
3468
|
+
const childEventResourceId = slugify(`event~${childEventHash}`)
|
|
3469
|
+
|
|
3470
|
+
const newChildEvent = {
|
|
3471
|
+
id: childEventResourceId,
|
|
3472
|
+
type: 'event',
|
|
3473
|
+
fields: {
|
|
3474
|
+
title: childEventInput.fields.title,
|
|
3475
|
+
startsAt: childEventInput.fields.startsAt,
|
|
3476
|
+
endsAt: childEventInput.fields.endsAt,
|
|
3477
|
+
description: childEventInput.fields.description,
|
|
3478
|
+
state: 'draft',
|
|
3479
|
+
visibility: 'public',
|
|
3480
|
+
slug: slugify(
|
|
3481
|
+
`${childEventInput.fields.title}~${childEventHash}`,
|
|
3482
|
+
),
|
|
3483
|
+
// No price/quantity - they're on the event series
|
|
3484
|
+
price: null,
|
|
3485
|
+
quantity: null,
|
|
3486
|
+
},
|
|
3487
|
+
createdById: userId,
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
await tx.insert(contentResource).values(newChildEvent)
|
|
3491
|
+
|
|
3492
|
+
// Fetch the created child event with relations
|
|
3493
|
+
const childEventResource = await tx.query.contentResource.findFirst({
|
|
3494
|
+
where: eq(contentResource.id, childEventResourceId),
|
|
3495
|
+
with: {
|
|
3496
|
+
resources: {
|
|
3497
|
+
with: {
|
|
3498
|
+
resource: {
|
|
3499
|
+
with: {
|
|
3500
|
+
resources: {
|
|
3501
|
+
with: {
|
|
3502
|
+
resource: true,
|
|
3503
|
+
},
|
|
3504
|
+
orderBy: asc(contentResourceResource.position),
|
|
3505
|
+
},
|
|
3506
|
+
},
|
|
3507
|
+
},
|
|
3508
|
+
},
|
|
3509
|
+
orderBy: asc(contentResourceResource.position),
|
|
3510
|
+
},
|
|
3511
|
+
tags: {
|
|
3512
|
+
with: {
|
|
3513
|
+
tag: true,
|
|
3514
|
+
},
|
|
3515
|
+
orderBy: asc(contentResourceTag.position),
|
|
3516
|
+
},
|
|
3517
|
+
resourceProducts: {
|
|
3518
|
+
with: {
|
|
3519
|
+
product: {
|
|
3520
|
+
with: {
|
|
3521
|
+
price: true,
|
|
3522
|
+
},
|
|
3523
|
+
},
|
|
3524
|
+
},
|
|
3525
|
+
},
|
|
3526
|
+
},
|
|
3527
|
+
})
|
|
3528
|
+
|
|
3529
|
+
const parsedChildEvent =
|
|
3530
|
+
ContentResourceSchema.safeParse(childEventResource)
|
|
3531
|
+
if (!parsedChildEvent.success) {
|
|
3532
|
+
throw new Error(`Error parsing child event resource ${i + 1}`)
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
// Associate tags with child event
|
|
3536
|
+
if (childEventInput.fields.tagIds) {
|
|
3537
|
+
await tx.insert(contentResourceTag).values(
|
|
3538
|
+
childEventInput.fields.tagIds.map((tag) => ({
|
|
3539
|
+
contentResourceId: childEventResourceId,
|
|
3540
|
+
tagId: tag.id,
|
|
3541
|
+
createdAt: new Date(),
|
|
3542
|
+
updatedAt: new Date(),
|
|
3543
|
+
position: 0,
|
|
3544
|
+
})),
|
|
3545
|
+
)
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// Associate child event with event series
|
|
3549
|
+
await tx.insert(contentResourceResource).values({
|
|
3550
|
+
resourceOfId: eventSeriesResourceId,
|
|
3551
|
+
resourceId: childEventResourceId,
|
|
3552
|
+
position: i,
|
|
3553
|
+
})
|
|
3554
|
+
|
|
3555
|
+
createdChildEvents.push(parsedChildEvent.data)
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
return {
|
|
3559
|
+
eventSeries: parsedEventSeries.data,
|
|
3560
|
+
childEvents: createdChildEvents,
|
|
3561
|
+
}
|
|
3562
|
+
})
|
|
3563
|
+
|
|
3564
|
+
const { eventSeries, childEvents } = result
|
|
3565
|
+
|
|
3566
|
+
// Step 4: Handle product creation outside transaction
|
|
3567
|
+
if (
|
|
3568
|
+
eventSeriesInput.sharedFields.price &&
|
|
3569
|
+
eventSeriesInput.sharedFields.price > 0
|
|
3570
|
+
) {
|
|
3571
|
+
try {
|
|
3572
|
+
const product = await adapter.createProduct({
|
|
3573
|
+
name: eventSeriesInput.fields.title,
|
|
3574
|
+
price: eventSeriesInput.sharedFields.price,
|
|
3575
|
+
quantityAvailable: eventSeriesInput.sharedFields.quantity ?? -1,
|
|
3576
|
+
type: 'live',
|
|
3577
|
+
state: 'published',
|
|
3578
|
+
visibility: 'public',
|
|
3579
|
+
})
|
|
3580
|
+
if (product) {
|
|
3581
|
+
await adapter.addResourceToProduct({
|
|
3582
|
+
resource: eventSeries,
|
|
3583
|
+
productId: product.id,
|
|
3584
|
+
userId,
|
|
3585
|
+
})
|
|
3586
|
+
|
|
3587
|
+
// Create coupon if enabled
|
|
3588
|
+
if (
|
|
3589
|
+
input.coupon?.enabled &&
|
|
3590
|
+
input.coupon.percentageDiscount &&
|
|
3591
|
+
input.coupon.expires
|
|
3592
|
+
) {
|
|
3593
|
+
try {
|
|
3594
|
+
let finalExpires = input.coupon.expires
|
|
3595
|
+
if (finalExpires instanceof Date) {
|
|
3596
|
+
finalExpires = new Date(
|
|
3597
|
+
Date.UTC(
|
|
3598
|
+
finalExpires.getUTCFullYear(),
|
|
3599
|
+
finalExpires.getUTCMonth(),
|
|
3600
|
+
finalExpires.getUTCDate(),
|
|
3601
|
+
23,
|
|
3602
|
+
59,
|
|
3603
|
+
59,
|
|
3604
|
+
0,
|
|
3605
|
+
),
|
|
3606
|
+
)
|
|
3607
|
+
}
|
|
3608
|
+
const couponInput = {
|
|
3609
|
+
quantity: '1',
|
|
3610
|
+
maxUses: 1,
|
|
3611
|
+
expires: finalExpires,
|
|
3612
|
+
restrictedToProductId: product.id,
|
|
3613
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
3614
|
+
status: 1,
|
|
3615
|
+
default: true,
|
|
3616
|
+
fields: {},
|
|
3617
|
+
}
|
|
3618
|
+
await adapter.createCoupon(couponInput)
|
|
3619
|
+
logger.debug('event.series.coupon.success', {
|
|
3620
|
+
eventSeriesId: eventSeries.id,
|
|
3621
|
+
productId: product.id,
|
|
3622
|
+
userId,
|
|
3623
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
3624
|
+
})
|
|
3625
|
+
} catch (couponError) {
|
|
3626
|
+
logger.error(
|
|
3627
|
+
new Error(
|
|
3628
|
+
`Error creating coupon for event series: ${couponError}`,
|
|
3629
|
+
),
|
|
3630
|
+
)
|
|
3631
|
+
logger.debug('event.series.coupon.failed', {
|
|
3632
|
+
eventSeriesId: eventSeries.id,
|
|
3633
|
+
productId: product.id,
|
|
3634
|
+
userId,
|
|
3635
|
+
})
|
|
3636
|
+
// Don't throw here - event series creation should succeed even if coupon fails
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
} else {
|
|
3640
|
+
logger.error(new Error('Failed to create product'))
|
|
3641
|
+
logger.debug('event.series.product.failed', {
|
|
3642
|
+
eventSeriesId: eventSeries.id,
|
|
3643
|
+
userId,
|
|
3644
|
+
price: eventSeriesInput.sharedFields.price,
|
|
3645
|
+
})
|
|
3646
|
+
}
|
|
3647
|
+
} catch (error) {
|
|
3648
|
+
logger.error(
|
|
3649
|
+
new Error(`Error creating and associating product: ${error}`),
|
|
3650
|
+
)
|
|
3651
|
+
logger.debug('event.series.product.creation.failed', {
|
|
3652
|
+
eventSeriesId: eventSeries.id,
|
|
3653
|
+
userId,
|
|
3654
|
+
})
|
|
3655
|
+
// Note: We don't throw here since the core data is already committed
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
return { eventSeries, childEvents }
|
|
3660
|
+
},
|
|
3661
|
+
async createCohort(
|
|
3662
|
+
input: {
|
|
3663
|
+
cohort: {
|
|
3664
|
+
title: string
|
|
3665
|
+
description?: string
|
|
3666
|
+
tagIds?:
|
|
3667
|
+
| { id: string; fields: { label: string; name: string } }[]
|
|
3668
|
+
| null
|
|
3669
|
+
}
|
|
3670
|
+
dates: {
|
|
3671
|
+
start: Date
|
|
3672
|
+
end: Date
|
|
3673
|
+
}
|
|
3674
|
+
createProduct?: boolean
|
|
3675
|
+
pricing: {
|
|
3676
|
+
price?: number | null
|
|
3677
|
+
}
|
|
3678
|
+
coupon?: {
|
|
3679
|
+
enabled: boolean
|
|
3680
|
+
percentageDiscount?: string
|
|
3681
|
+
expires?: Date
|
|
3682
|
+
}
|
|
3683
|
+
workshops: { id: string }[]
|
|
3684
|
+
},
|
|
3685
|
+
userId: string,
|
|
3686
|
+
) {
|
|
3687
|
+
const hash = guid()
|
|
3688
|
+
const cohortId = `cohort~${hash}`
|
|
3689
|
+
|
|
3690
|
+
// Transaction only handles database operations
|
|
3691
|
+
const cohort = await client.transaction(async (tx) => {
|
|
3692
|
+
// Create cohort content resource
|
|
3693
|
+
await tx.insert(contentResource).values({
|
|
3694
|
+
id: cohortId,
|
|
3695
|
+
type: 'cohort',
|
|
3696
|
+
createdById: userId,
|
|
3697
|
+
fields: {
|
|
3698
|
+
title: input.cohort.title,
|
|
3699
|
+
description: input.cohort.description,
|
|
3700
|
+
state: 'draft',
|
|
3701
|
+
visibility: 'unlisted',
|
|
3702
|
+
slug: slugify(`${input.cohort.title}~${hash}`),
|
|
3703
|
+
startsAt: input.dates.start,
|
|
3704
|
+
endsAt: input.dates.end,
|
|
3705
|
+
},
|
|
3706
|
+
})
|
|
3707
|
+
|
|
3708
|
+
// Fetch created cohort
|
|
3709
|
+
const cohort = await tx.query.contentResource.findFirst({
|
|
3710
|
+
where: eq(contentResource.id, cohortId),
|
|
3711
|
+
with: {
|
|
3712
|
+
resources: {
|
|
3713
|
+
with: {
|
|
3714
|
+
resource: true,
|
|
3715
|
+
},
|
|
3716
|
+
orderBy: asc(contentResourceResource.position),
|
|
3717
|
+
},
|
|
3718
|
+
},
|
|
3719
|
+
})
|
|
3720
|
+
|
|
3721
|
+
if (!cohort) {
|
|
3722
|
+
throw new Error('Failed to create cohort')
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3725
|
+
const parsedCohort = ContentResourceSchema.safeParse(cohort)
|
|
3726
|
+
if (!parsedCohort.success) {
|
|
3727
|
+
throw new Error('Invalid cohort data')
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// Associate tags with cohort
|
|
3731
|
+
if (input.cohort.tagIds) {
|
|
3732
|
+
await tx.insert(contentResourceTag).values(
|
|
3733
|
+
input.cohort.tagIds.map((tag) => ({
|
|
3734
|
+
contentResourceId: cohortId,
|
|
3735
|
+
tagId: tag.id,
|
|
3736
|
+
createdAt: new Date(),
|
|
3737
|
+
updatedAt: new Date(),
|
|
3738
|
+
position: 0,
|
|
3739
|
+
})),
|
|
3740
|
+
)
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
// Link workshops to cohort
|
|
3744
|
+
let position = 0
|
|
3745
|
+
for (const workshop of input.workshops) {
|
|
3746
|
+
await tx.insert(contentResourceResource).values({
|
|
3747
|
+
resourceOfId: cohortId,
|
|
3748
|
+
resourceId: workshop.id,
|
|
3749
|
+
position,
|
|
3750
|
+
})
|
|
3751
|
+
position++
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
return parsedCohort.data
|
|
3755
|
+
})
|
|
3756
|
+
|
|
3757
|
+
// Handle product and coupon creation outside transaction
|
|
3758
|
+
let product: any = null
|
|
3759
|
+
if (
|
|
3760
|
+
input.createProduct &&
|
|
3761
|
+
input.pricing.price &&
|
|
3762
|
+
input.pricing.price > 0
|
|
3763
|
+
) {
|
|
3764
|
+
try {
|
|
3765
|
+
product = await adapter.createProduct({
|
|
3766
|
+
name: input.cohort.title,
|
|
3767
|
+
price: input.pricing.price,
|
|
3768
|
+
quantityAvailable: -1,
|
|
3769
|
+
type: 'cohort',
|
|
3770
|
+
state: 'published',
|
|
3771
|
+
visibility: 'public',
|
|
3772
|
+
openEnrollment: new Date().toISOString(),
|
|
3773
|
+
closeEnrollment: input.dates.start.toISOString(),
|
|
3774
|
+
})
|
|
3775
|
+
|
|
3776
|
+
if (product) {
|
|
3777
|
+
// Link product to cohort resource
|
|
3778
|
+
await client.insert(contentResourceProduct).values({
|
|
3779
|
+
resourceId: cohortId,
|
|
3780
|
+
productId: product.id,
|
|
3781
|
+
position: 0,
|
|
3782
|
+
metadata: {
|
|
3783
|
+
addedBy: userId,
|
|
3784
|
+
},
|
|
3785
|
+
})
|
|
3786
|
+
|
|
3787
|
+
// Create coupon if enabled
|
|
3788
|
+
if (
|
|
3789
|
+
input.coupon?.enabled &&
|
|
3790
|
+
input.coupon.percentageDiscount &&
|
|
3791
|
+
input.coupon.expires
|
|
3792
|
+
) {
|
|
3793
|
+
try {
|
|
3794
|
+
const finalExpires = normalizeExpirationDate(
|
|
3795
|
+
input.coupon.expires,
|
|
3796
|
+
)
|
|
3797
|
+
await adapter.createCoupon({
|
|
3798
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
3799
|
+
expires: finalExpires || null,
|
|
3800
|
+
restrictedToProductId: product.id,
|
|
3801
|
+
default: true,
|
|
3802
|
+
maxUses: -1,
|
|
3803
|
+
quantity: '-1',
|
|
3804
|
+
status: 1,
|
|
3805
|
+
fields: {},
|
|
3806
|
+
})
|
|
3807
|
+
logger.debug('cohort.create.coupon.success', {
|
|
3808
|
+
cohortId,
|
|
3809
|
+
productId: product.id,
|
|
3810
|
+
userId,
|
|
3811
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
3812
|
+
})
|
|
3813
|
+
} catch (couponError) {
|
|
3814
|
+
logger.error(
|
|
3815
|
+
new Error(
|
|
3816
|
+
`Failed to create coupon for cohort: ${couponError}`,
|
|
3817
|
+
),
|
|
3818
|
+
)
|
|
3819
|
+
logger.debug('cohort.create.coupon.failed', {
|
|
3820
|
+
cohortId,
|
|
3821
|
+
productId: product.id,
|
|
3822
|
+
userId,
|
|
3823
|
+
})
|
|
3824
|
+
// Don't throw - cohort creation should succeed even if coupon fails
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
} catch (productError) {
|
|
3829
|
+
logger.error(
|
|
3830
|
+
new Error(`Failed to create product for cohort: ${productError}`),
|
|
3831
|
+
)
|
|
3832
|
+
logger.debug('cohort.create.product.failed', {
|
|
3833
|
+
cohortId,
|
|
3834
|
+
userId,
|
|
3835
|
+
price: input.pricing.price,
|
|
3836
|
+
})
|
|
3837
|
+
// Don't throw - cohort creation should succeed even if product fails
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
return {
|
|
3842
|
+
cohort,
|
|
3843
|
+
product,
|
|
3844
|
+
}
|
|
3845
|
+
},
|
|
3846
|
+
async createWorkshop(
|
|
3847
|
+
input: {
|
|
3848
|
+
workshop: {
|
|
3849
|
+
title: string
|
|
3850
|
+
description?: string
|
|
3851
|
+
tagIds?:
|
|
3852
|
+
| { id: string; fields: { label: string; name: string } }[]
|
|
3853
|
+
| null
|
|
3854
|
+
}
|
|
3855
|
+
createProduct?: boolean
|
|
3856
|
+
pricing: {
|
|
3857
|
+
price?: number | null
|
|
3858
|
+
quantity?: number | null
|
|
3859
|
+
}
|
|
3860
|
+
coupon?: {
|
|
3861
|
+
enabled: boolean
|
|
3862
|
+
percentageDiscount?: string
|
|
3863
|
+
expires?: Date
|
|
3864
|
+
}
|
|
3865
|
+
structure: Array<
|
|
3866
|
+
| {
|
|
3867
|
+
type: 'section'
|
|
3868
|
+
title: string
|
|
3869
|
+
lessons: { title: string; videoResourceId?: string }[]
|
|
3870
|
+
}
|
|
3871
|
+
| { type: 'lesson'; title: string; videoResourceId?: string }
|
|
3872
|
+
>
|
|
3873
|
+
},
|
|
3874
|
+
userId: string,
|
|
3875
|
+
) {
|
|
3876
|
+
const hash = guid()
|
|
3877
|
+
const workshopId = `workshop~${hash}`
|
|
3878
|
+
|
|
3879
|
+
// Transaction only handles database operations
|
|
3880
|
+
const result = await client.transaction(async (tx) => {
|
|
3881
|
+
// Create workshop content resource
|
|
3882
|
+
await tx.insert(contentResource).values({
|
|
3883
|
+
id: workshopId,
|
|
3884
|
+
type: 'workshop',
|
|
3885
|
+
createdById: userId,
|
|
3886
|
+
fields: {
|
|
3887
|
+
title: input.workshop.title,
|
|
3888
|
+
description: input.workshop.description,
|
|
3889
|
+
state: 'draft',
|
|
3890
|
+
visibility: 'unlisted',
|
|
3891
|
+
slug: slugify(`${input.workshop.title}~${hash}`),
|
|
3892
|
+
},
|
|
3893
|
+
})
|
|
3894
|
+
|
|
3895
|
+
// Fetch created workshop
|
|
3896
|
+
const workshop = await tx.query.contentResource.findFirst({
|
|
3897
|
+
where: eq(contentResource.id, workshopId),
|
|
3898
|
+
with: {
|
|
3899
|
+
resources: {
|
|
3900
|
+
with: {
|
|
3901
|
+
resource: {
|
|
3902
|
+
with: {
|
|
3903
|
+
resources: {
|
|
3904
|
+
with: {
|
|
3905
|
+
resource: true,
|
|
3906
|
+
},
|
|
3907
|
+
orderBy: asc(contentResourceResource.position),
|
|
3908
|
+
},
|
|
3909
|
+
},
|
|
3910
|
+
},
|
|
3911
|
+
},
|
|
3912
|
+
orderBy: asc(contentResourceResource.position),
|
|
3913
|
+
},
|
|
3914
|
+
},
|
|
3915
|
+
})
|
|
3916
|
+
|
|
3917
|
+
if (!workshop) {
|
|
3918
|
+
throw new Error('Failed to create workshop')
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
const parsedWorkshop = ContentResourceSchema.safeParse(workshop)
|
|
3922
|
+
if (!parsedWorkshop.success) {
|
|
3923
|
+
throw new Error('Invalid workshop data')
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// Associate tags with workshop
|
|
3927
|
+
if (input.workshop.tagIds) {
|
|
3928
|
+
await tx.insert(contentResourceTag).values(
|
|
3929
|
+
input.workshop.tagIds.map((tag) => ({
|
|
3930
|
+
contentResourceId: workshopId,
|
|
3931
|
+
tagId: tag.id,
|
|
3932
|
+
createdAt: new Date(),
|
|
3933
|
+
updatedAt: new Date(),
|
|
3934
|
+
position: 0,
|
|
3935
|
+
})),
|
|
3936
|
+
)
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
// Create sections and lessons
|
|
3940
|
+
const createdSections: ContentResource[] = []
|
|
3941
|
+
const createdLessons: ContentResource[] = []
|
|
3942
|
+
let position = 0
|
|
3943
|
+
|
|
3944
|
+
for (const item of input.structure) {
|
|
3945
|
+
if (item.type === 'section') {
|
|
3946
|
+
// Create section
|
|
3947
|
+
const sectionHash = guid()
|
|
3948
|
+
const sectionId = `section~${sectionHash}`
|
|
3949
|
+
|
|
3950
|
+
await tx.insert(contentResource).values({
|
|
3951
|
+
id: sectionId,
|
|
3952
|
+
type: 'section',
|
|
3953
|
+
createdById: userId,
|
|
3954
|
+
fields: {
|
|
3955
|
+
title: item.title,
|
|
3956
|
+
state: 'draft',
|
|
3957
|
+
visibility: 'unlisted',
|
|
3958
|
+
slug: slugify(`${item.title}~${sectionHash}`),
|
|
3959
|
+
},
|
|
3960
|
+
})
|
|
3961
|
+
|
|
3962
|
+
// Link section to workshop
|
|
3963
|
+
await tx.insert(contentResourceResource).values({
|
|
3964
|
+
resourceOfId: workshopId,
|
|
3965
|
+
resourceId: sectionId,
|
|
3966
|
+
position,
|
|
3967
|
+
})
|
|
3968
|
+
|
|
3969
|
+
const section = await tx.query.contentResource.findFirst({
|
|
3970
|
+
where: eq(contentResource.id, sectionId),
|
|
3971
|
+
})
|
|
3972
|
+
|
|
3973
|
+
if (section) {
|
|
3974
|
+
createdSections.push(section as ContentResource)
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
// Create lessons in section
|
|
3978
|
+
let lessonPosition = 0
|
|
3979
|
+
for (const lessonData of item.lessons) {
|
|
3980
|
+
const lessonHash = guid()
|
|
3981
|
+
const lessonId = `lesson_${lessonHash}`
|
|
3982
|
+
|
|
3983
|
+
await tx.insert(contentResource).values({
|
|
3984
|
+
id: lessonId,
|
|
3985
|
+
type: 'lesson',
|
|
3986
|
+
createdById: userId,
|
|
3987
|
+
fields: {
|
|
3988
|
+
title: lessonData.title,
|
|
3989
|
+
state: 'draft',
|
|
3990
|
+
visibility: 'unlisted',
|
|
3991
|
+
slug: slugify(`${lessonData.title}~${lessonHash}`),
|
|
3992
|
+
lessonType: 'lesson',
|
|
3993
|
+
},
|
|
3994
|
+
})
|
|
3995
|
+
|
|
3996
|
+
// Link video if provided
|
|
3997
|
+
if (lessonData.videoResourceId) {
|
|
3998
|
+
await tx.insert(contentResourceResource).values({
|
|
3999
|
+
resourceOfId: lessonId,
|
|
4000
|
+
resourceId: lessonData.videoResourceId,
|
|
4001
|
+
position: 0,
|
|
4002
|
+
})
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
// Link lesson to section
|
|
4006
|
+
await tx.insert(contentResourceResource).values({
|
|
4007
|
+
resourceOfId: sectionId,
|
|
4008
|
+
resourceId: lessonId,
|
|
4009
|
+
position: lessonPosition,
|
|
4010
|
+
})
|
|
4011
|
+
|
|
4012
|
+
const lesson = await tx.query.contentResource.findFirst({
|
|
4013
|
+
where: eq(contentResource.id, lessonId),
|
|
4014
|
+
})
|
|
4015
|
+
|
|
4016
|
+
if (lesson) {
|
|
4017
|
+
createdLessons.push(lesson as ContentResource)
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
lessonPosition++
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
position++
|
|
4024
|
+
} else {
|
|
4025
|
+
// Create top-level lesson
|
|
4026
|
+
const lessonHash = guid()
|
|
4027
|
+
const lessonId = `lesson_${lessonHash}`
|
|
4028
|
+
|
|
4029
|
+
await tx.insert(contentResource).values({
|
|
4030
|
+
id: lessonId,
|
|
4031
|
+
type: 'lesson',
|
|
4032
|
+
createdById: userId,
|
|
4033
|
+
fields: {
|
|
4034
|
+
title: item.title,
|
|
4035
|
+
state: 'draft',
|
|
4036
|
+
visibility: 'unlisted',
|
|
4037
|
+
slug: slugify(`${item.title}~${lessonHash}`),
|
|
4038
|
+
lessonType: 'lesson',
|
|
4039
|
+
},
|
|
4040
|
+
})
|
|
4041
|
+
|
|
4042
|
+
// Link video if provided
|
|
4043
|
+
if (item.videoResourceId) {
|
|
4044
|
+
await tx.insert(contentResourceResource).values({
|
|
4045
|
+
resourceOfId: lessonId,
|
|
4046
|
+
resourceId: item.videoResourceId,
|
|
4047
|
+
position: 0,
|
|
4048
|
+
})
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
// Link lesson to workshop
|
|
4052
|
+
await tx.insert(contentResourceResource).values({
|
|
4053
|
+
resourceOfId: workshopId,
|
|
4054
|
+
resourceId: lessonId,
|
|
4055
|
+
position,
|
|
4056
|
+
})
|
|
4057
|
+
|
|
4058
|
+
const lesson = await tx.query.contentResource.findFirst({
|
|
4059
|
+
where: eq(contentResource.id, lessonId),
|
|
4060
|
+
})
|
|
4061
|
+
|
|
4062
|
+
if (lesson) {
|
|
4063
|
+
createdLessons.push(lesson as ContentResource)
|
|
4064
|
+
}
|
|
4065
|
+
|
|
4066
|
+
position++
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
return {
|
|
4071
|
+
workshop: parsedWorkshop.data,
|
|
4072
|
+
sections: createdSections,
|
|
4073
|
+
lessons: createdLessons,
|
|
4074
|
+
}
|
|
4075
|
+
})
|
|
4076
|
+
|
|
4077
|
+
// Handle product and coupon creation outside transaction
|
|
4078
|
+
let product: any = null
|
|
4079
|
+
if (
|
|
4080
|
+
input.createProduct &&
|
|
4081
|
+
input.pricing.price &&
|
|
4082
|
+
input.pricing.price > 0
|
|
4083
|
+
) {
|
|
4084
|
+
try {
|
|
4085
|
+
product = await adapter.createProduct({
|
|
4086
|
+
name: input.workshop.title,
|
|
4087
|
+
price: input.pricing.price,
|
|
4088
|
+
quantityAvailable: input.pricing.quantity ?? -1,
|
|
4089
|
+
type: 'self-paced',
|
|
4090
|
+
state: 'published',
|
|
4091
|
+
visibility: 'public',
|
|
4092
|
+
})
|
|
4093
|
+
|
|
4094
|
+
if (product) {
|
|
4095
|
+
// Link product to workshop resource
|
|
4096
|
+
await client.insert(contentResourceProduct).values({
|
|
4097
|
+
resourceId: workshopId,
|
|
4098
|
+
productId: product.id,
|
|
4099
|
+
position: 0,
|
|
4100
|
+
metadata: {
|
|
4101
|
+
addedBy: userId,
|
|
4102
|
+
},
|
|
4103
|
+
})
|
|
4104
|
+
|
|
4105
|
+
// Create coupon if enabled
|
|
4106
|
+
if (
|
|
4107
|
+
input.coupon?.enabled &&
|
|
4108
|
+
input.coupon.percentageDiscount &&
|
|
4109
|
+
input.coupon.expires
|
|
4110
|
+
) {
|
|
4111
|
+
try {
|
|
4112
|
+
const finalExpires = normalizeExpirationDate(
|
|
4113
|
+
input.coupon.expires,
|
|
4114
|
+
)
|
|
4115
|
+
await adapter.createCoupon({
|
|
4116
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
4117
|
+
expires: finalExpires || null,
|
|
4118
|
+
restrictedToProductId: product.id,
|
|
4119
|
+
default: true,
|
|
4120
|
+
maxUses: -1,
|
|
4121
|
+
quantity: '-1',
|
|
4122
|
+
status: 1,
|
|
4123
|
+
fields: {},
|
|
4124
|
+
})
|
|
4125
|
+
logger.debug('workshop.create.coupon.success', {
|
|
4126
|
+
workshopId,
|
|
4127
|
+
productId: product.id,
|
|
4128
|
+
userId,
|
|
4129
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
4130
|
+
})
|
|
4131
|
+
} catch (couponError) {
|
|
4132
|
+
logger.error(
|
|
4133
|
+
new Error(
|
|
4134
|
+
`Failed to create coupon for workshop: ${couponError}`,
|
|
4135
|
+
),
|
|
4136
|
+
)
|
|
4137
|
+
logger.debug('workshop.create.coupon.failed', {
|
|
4138
|
+
workshopId,
|
|
4139
|
+
productId: product.id,
|
|
4140
|
+
userId,
|
|
4141
|
+
})
|
|
4142
|
+
// Don't throw - workshop creation should succeed even if coupon fails
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
} catch (productError) {
|
|
4147
|
+
logger.error(
|
|
4148
|
+
new Error(`Failed to create product for workshop: ${productError}`),
|
|
4149
|
+
)
|
|
4150
|
+
logger.debug('workshop.create.product.failed', {
|
|
4151
|
+
workshopId,
|
|
4152
|
+
userId,
|
|
4153
|
+
price: input.pricing.price,
|
|
4154
|
+
})
|
|
4155
|
+
// Don't throw - workshop creation should succeed even if product fails
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
return {
|
|
4160
|
+
...result,
|
|
4161
|
+
product,
|
|
4162
|
+
}
|
|
4163
|
+
},
|
|
4164
|
+
async createCoupon(input: {
|
|
4165
|
+
quantity: string
|
|
4166
|
+
maxUses: number
|
|
4167
|
+
expires: Date | null
|
|
4168
|
+
restrictedToProductId: string | null
|
|
4169
|
+
percentageDiscount: string
|
|
4170
|
+
status: number
|
|
4171
|
+
default: boolean
|
|
4172
|
+
fields: Record<string, any>
|
|
4173
|
+
}) {
|
|
4174
|
+
function findClosestDiscount(percentOff: number) {
|
|
4175
|
+
// we want a fraction so if it is whole number, we make it fractional
|
|
4176
|
+
percentOff = percentOff <= 1 ? percentOff : percentOff / 100
|
|
4177
|
+
return [1, 0.95, 0.9, 0.75, 0.6, 0.5, 0.4, 0.25, 0.1].reduce((a, b) => {
|
|
4178
|
+
let aDiff = Math.abs(a - percentOff)
|
|
4179
|
+
let bDiff = Math.abs(b - percentOff)
|
|
4180
|
+
|
|
4181
|
+
if (aDiff === bDiff) {
|
|
4182
|
+
// Choose largest vs smallest (> vs <)
|
|
4183
|
+
return a > b ? a : b
|
|
4184
|
+
} else {
|
|
4185
|
+
return bDiff < aDiff ? b : a
|
|
4186
|
+
}
|
|
4187
|
+
})
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
const percentageDiscount = findClosestDiscount(
|
|
4191
|
+
Number(input.percentageDiscount) * 100,
|
|
4192
|
+
)
|
|
4193
|
+
|
|
4194
|
+
const merchantCouponResult =
|
|
4195
|
+
percentageDiscount < 1
|
|
4196
|
+
? await client.query.merchantCoupon.findFirst({
|
|
4197
|
+
where: and(
|
|
4198
|
+
eq(
|
|
4199
|
+
merchantCoupon.percentageDiscount,
|
|
4200
|
+
percentageDiscount.toString(),
|
|
4201
|
+
),
|
|
4202
|
+
eq(merchantCoupon.type, 'special'),
|
|
4203
|
+
),
|
|
4204
|
+
})
|
|
4205
|
+
: null
|
|
4206
|
+
|
|
4207
|
+
if (!merchantCouponResult) {
|
|
4208
|
+
return []
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
const codesArray: string[] = []
|
|
4212
|
+
await client.transaction(async (trx) => {
|
|
4213
|
+
// insert coupon for CouponInput quantity
|
|
4214
|
+
for (let i = 0; i < Number(input.quantity); i++) {
|
|
4215
|
+
const id = `coupon_${guid()}`
|
|
4216
|
+
await trx.insert(coupon).values({
|
|
4217
|
+
...input,
|
|
4218
|
+
merchantCouponId: merchantCouponResult.id as string,
|
|
4219
|
+
id,
|
|
4220
|
+
})
|
|
4221
|
+
codesArray.push(id)
|
|
4222
|
+
}
|
|
4223
|
+
})
|
|
4224
|
+
|
|
4225
|
+
return codesArray
|
|
4226
|
+
},
|
|
4227
|
+
async createUser(data) {
|
|
4228
|
+
const id = crypto.randomUUID()
|
|
4229
|
+
try {
|
|
4230
|
+
await client.insert(users).values({ ...data, id })
|
|
4231
|
+
|
|
4232
|
+
// creating a personal organization for the user
|
|
4233
|
+
// every user gets an organization of their very own
|
|
4234
|
+
const personalOrganization = await adapter.createOrganization({
|
|
4235
|
+
name: `Personal (${data.email})`,
|
|
4236
|
+
})
|
|
4237
|
+
|
|
4238
|
+
if (!personalOrganization) {
|
|
4239
|
+
throw new Error('Failed to create personal organization')
|
|
4240
|
+
}
|
|
4241
|
+
|
|
4242
|
+
const membership = await adapter.addMemberToOrganization({
|
|
4243
|
+
organizationId: personalOrganization.id,
|
|
4244
|
+
userId: id,
|
|
4245
|
+
invitedById: id,
|
|
2525
4246
|
})
|
|
2526
4247
|
|
|
2527
4248
|
if (!membership) {
|
|
@@ -2542,7 +4263,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2542
4263
|
|
|
2543
4264
|
return user
|
|
2544
4265
|
} catch (error) {
|
|
2545
|
-
|
|
4266
|
+
logger.error(toError(error), {
|
|
4267
|
+
email: data.email,
|
|
4268
|
+
event: 'adapter.user.create.failed',
|
|
4269
|
+
userId: id,
|
|
4270
|
+
})
|
|
2546
4271
|
throw error
|
|
2547
4272
|
}
|
|
2548
4273
|
},
|
|
@@ -2978,6 +4703,15 @@ export function mySqlDrizzleAdapter(
|
|
|
2978
4703
|
|
|
2979
4704
|
return subscriptionParsed.data
|
|
2980
4705
|
},
|
|
4706
|
+
updateSubscriptionStatus: async (
|
|
4707
|
+
subscriptionId: string,
|
|
4708
|
+
status: string,
|
|
4709
|
+
) => {
|
|
4710
|
+
await client
|
|
4711
|
+
.update(subscriptionTable)
|
|
4712
|
+
.set({ status })
|
|
4713
|
+
.where(eq(subscriptionTable.id, subscriptionId))
|
|
4714
|
+
},
|
|
2981
4715
|
}
|
|
2982
4716
|
|
|
2983
4717
|
return adapter
|