@coursebuilder/adapter-drizzle 1.0.3 → 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-5QEGBX2O.js → chunk-HEQ5MII4.js} +3665 -1340
- 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 +4114 -1631
- 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 +1910 -152
- 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,
|
|
@@ -432,6 +555,11 @@ export function mySqlDrizzleAdapter(
|
|
|
432
555
|
}
|
|
433
556
|
|
|
434
557
|
const purchaseId = `purchase-${v4()}`
|
|
558
|
+
const userMemberships = await adapter.getMembershipsForUser(user.id)
|
|
559
|
+
const organizationId =
|
|
560
|
+
coupon.organizationId ||
|
|
561
|
+
userMemberships.find((m) => m.organization.name?.includes(user.email))
|
|
562
|
+
?.organizationId // safer way to make sure we are using personal organization
|
|
435
563
|
|
|
436
564
|
await adapter.createPurchase({
|
|
437
565
|
id: purchaseId,
|
|
@@ -440,6 +568,7 @@ export function mySqlDrizzleAdapter(
|
|
|
440
568
|
redeemedBulkCouponId: bulkCouponRedemption ? coupon.id : null,
|
|
441
569
|
productId,
|
|
442
570
|
totalAmount: '0',
|
|
571
|
+
organizationId,
|
|
443
572
|
metadata: {
|
|
444
573
|
couponUsedId: bulkCouponRedemption ? null : coupon.id,
|
|
445
574
|
},
|
|
@@ -504,6 +633,21 @@ export function mySqlDrizzleAdapter(
|
|
|
504
633
|
}),
|
|
505
634
|
)
|
|
506
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
|
+
},
|
|
507
651
|
getMerchantAccount: async (options) => {
|
|
508
652
|
return merchantAccountSchema.parse(
|
|
509
653
|
await client.query.merchantAccount.findFirst({
|
|
@@ -523,10 +667,10 @@ export function mySqlDrizzleAdapter(
|
|
|
523
667
|
merchantPriceSchema.safeParse(merchantPriceData)
|
|
524
668
|
|
|
525
669
|
if (!parsedMerchantPrice.success) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
)
|
|
670
|
+
logger.error(parsedMerchantPrice.error, {
|
|
671
|
+
event: 'adapter.merchantPrice.parse_failed',
|
|
672
|
+
productId,
|
|
673
|
+
})
|
|
530
674
|
return null
|
|
531
675
|
}
|
|
532
676
|
|
|
@@ -632,7 +776,11 @@ export function mySqlDrizzleAdapter(
|
|
|
632
776
|
resourceProgressSchema.safeParse(lessonProgress)
|
|
633
777
|
|
|
634
778
|
if (!parsedLessonProgress.success) {
|
|
635
|
-
|
|
779
|
+
logger.error(parsedLessonProgress.error, {
|
|
780
|
+
event: 'adapter.lessonProgress.parse_failed',
|
|
781
|
+
lessonId: options.lessonId,
|
|
782
|
+
userId: options.userId,
|
|
783
|
+
})
|
|
636
784
|
return null
|
|
637
785
|
}
|
|
638
786
|
|
|
@@ -674,10 +822,11 @@ export function mySqlDrizzleAdapter(
|
|
|
674
822
|
.safeParse(couponForIdOrCode)
|
|
675
823
|
|
|
676
824
|
if (!parsedCoupon.success) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
825
|
+
logger.error(parsedCoupon.error, {
|
|
826
|
+
code: options.code,
|
|
827
|
+
couponId: options.couponId,
|
|
828
|
+
event: 'adapter.coupon.parse_failed',
|
|
829
|
+
})
|
|
681
830
|
return null
|
|
682
831
|
}
|
|
683
832
|
|
|
@@ -811,15 +960,35 @@ export function mySqlDrizzleAdapter(
|
|
|
811
960
|
})
|
|
812
961
|
.where(eq(coupon.id, bulkCouponId))
|
|
813
962
|
} else {
|
|
814
|
-
|
|
963
|
+
// Try to find merchant coupon by stripeCouponId first
|
|
964
|
+
let merchantCouponToUse = stripeCouponId
|
|
815
965
|
? merchantCouponSchema.nullable().parse(
|
|
816
|
-
await client.query.merchantCoupon.findFirst({
|
|
966
|
+
(await client.query.merchantCoupon.findFirst({
|
|
817
967
|
where: eq(merchantCoupon.identifier, stripeCouponId),
|
|
818
|
-
}),
|
|
968
|
+
})) || null,
|
|
819
969
|
)
|
|
820
970
|
: null
|
|
821
971
|
|
|
822
|
-
|
|
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 = {
|
|
823
992
|
id: bulkCouponId as string,
|
|
824
993
|
percentageDiscount: '1.0',
|
|
825
994
|
restrictedToProductId: productId,
|
|
@@ -831,7 +1000,11 @@ export function mySqlDrizzleAdapter(
|
|
|
831
1000
|
merchantCouponId: merchantCouponToUse.id,
|
|
832
1001
|
}
|
|
833
1002
|
: {}),
|
|
834
|
-
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
couponToUpdate = await client
|
|
1006
|
+
.insert(coupon)
|
|
1007
|
+
.values(bulkCouponValues)
|
|
835
1008
|
}
|
|
836
1009
|
}
|
|
837
1010
|
|
|
@@ -903,7 +1076,14 @@ export function mySqlDrizzleAdapter(
|
|
|
903
1076
|
|
|
904
1077
|
return purchaseId
|
|
905
1078
|
} catch (error) {
|
|
906
|
-
|
|
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
|
+
})
|
|
907
1087
|
trx.rollback()
|
|
908
1088
|
throw error
|
|
909
1089
|
}
|
|
@@ -916,11 +1096,12 @@ export function mySqlDrizzleAdapter(
|
|
|
916
1096
|
)
|
|
917
1097
|
|
|
918
1098
|
if (!parsedPurchase.success) {
|
|
919
|
-
|
|
920
|
-
'
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
+
})
|
|
924
1105
|
throw new Error('Error parsing purchase')
|
|
925
1106
|
}
|
|
926
1107
|
|
|
@@ -951,6 +1132,45 @@ export function mySqlDrizzleAdapter(
|
|
|
951
1132
|
userId: options.user.id,
|
|
952
1133
|
})
|
|
953
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
|
+
},
|
|
954
1174
|
async findOrCreateUser(
|
|
955
1175
|
email: string,
|
|
956
1176
|
name?: string | null,
|
|
@@ -970,6 +1190,10 @@ export function mySqlDrizzleAdapter(
|
|
|
970
1190
|
if (!newUser) {
|
|
971
1191
|
throw new Error('Could not create user')
|
|
972
1192
|
}
|
|
1193
|
+
logger.info('adapter.user.created', {
|
|
1194
|
+
email,
|
|
1195
|
+
userId: newUser.id,
|
|
1196
|
+
})
|
|
973
1197
|
return {
|
|
974
1198
|
user: newUser as User,
|
|
975
1199
|
isNewUser: true,
|
|
@@ -990,10 +1214,25 @@ export function mySqlDrizzleAdapter(
|
|
|
990
1214
|
),
|
|
991
1215
|
})) || null
|
|
992
1216
|
|
|
993
|
-
logger.debug('
|
|
1217
|
+
logger.debug('getCoupon', {
|
|
1218
|
+
couponIdOrCode,
|
|
1219
|
+
found: !!loadedCoupon,
|
|
1220
|
+
})
|
|
994
1221
|
|
|
995
1222
|
return couponSchema.nullable().parse(loadedCoupon)
|
|
996
1223
|
},
|
|
1224
|
+
async getPurchasesForBulkCouponId(
|
|
1225
|
+
bulkCouponId: string,
|
|
1226
|
+
): Promise<(Purchase & { user: User })[]> {
|
|
1227
|
+
return z.array(purchaseSchema.extend({ user: userSchema })).parse(
|
|
1228
|
+
await client.query.purchases.findMany({
|
|
1229
|
+
where: eq(purchaseTable.bulkCouponId, bulkCouponId),
|
|
1230
|
+
with: {
|
|
1231
|
+
user: true,
|
|
1232
|
+
},
|
|
1233
|
+
}),
|
|
1234
|
+
)
|
|
1235
|
+
},
|
|
997
1236
|
async getCouponWithBulkPurchases(couponId: string): Promise<
|
|
998
1237
|
| (Coupon & {
|
|
999
1238
|
bulkPurchases?: Purchase[] | null
|
|
@@ -1003,7 +1242,7 @@ export function mySqlDrizzleAdapter(
|
|
|
1003
1242
|
> {
|
|
1004
1243
|
logger.debug('getCouponWithBulkPurchases', { couponId })
|
|
1005
1244
|
let couponData
|
|
1006
|
-
let bulkCouponPurchases
|
|
1245
|
+
let bulkCouponPurchases: unknown[] = []
|
|
1007
1246
|
try {
|
|
1008
1247
|
couponData =
|
|
1009
1248
|
(await client.query.coupon.findFirst({
|
|
@@ -1014,8 +1253,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1014
1253
|
},
|
|
1015
1254
|
})) || null
|
|
1016
1255
|
} catch (e) {
|
|
1017
|
-
|
|
1018
|
-
|
|
1256
|
+
logger.error(e as Error, {
|
|
1257
|
+
method: 'getCouponWithBulkPurchases',
|
|
1258
|
+
couponId,
|
|
1259
|
+
step: 'load-coupon',
|
|
1260
|
+
})
|
|
1019
1261
|
}
|
|
1020
1262
|
|
|
1021
1263
|
try {
|
|
@@ -1025,10 +1267,16 @@ export function mySqlDrizzleAdapter(
|
|
|
1025
1267
|
user: true,
|
|
1026
1268
|
},
|
|
1027
1269
|
})
|
|
1028
|
-
|
|
1270
|
+
logger.debug('getCouponWithBulkPurchases.redeemed', {
|
|
1271
|
+
couponId,
|
|
1272
|
+
count: bulkCouponPurchases?.length ?? 0,
|
|
1273
|
+
})
|
|
1029
1274
|
} catch (e) {
|
|
1030
|
-
|
|
1031
|
-
|
|
1275
|
+
logger.error(e as Error, {
|
|
1276
|
+
method: 'getCouponWithBulkPurchases',
|
|
1277
|
+
couponId,
|
|
1278
|
+
step: 'load-redeemed-purchases',
|
|
1279
|
+
})
|
|
1032
1280
|
}
|
|
1033
1281
|
|
|
1034
1282
|
if (!couponData) {
|
|
@@ -1054,11 +1302,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1054
1302
|
.safeParse(couponWithBulkPurchases)
|
|
1055
1303
|
|
|
1056
1304
|
if (!parsedCoupon.success) {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
)
|
|
1305
|
+
logger.error(parsedCoupon.error, {
|
|
1306
|
+
couponId: couponData?.id ?? couponId,
|
|
1307
|
+
event: 'adapter.coupon.parse_failed',
|
|
1308
|
+
redeemedBulkCouponPurchasesCount: bulkCouponPurchases?.length ?? 0,
|
|
1309
|
+
})
|
|
1062
1310
|
return null
|
|
1063
1311
|
}
|
|
1064
1312
|
|
|
@@ -1122,7 +1370,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1122
1370
|
})
|
|
1123
1371
|
const parsed = z.array(resourceProgressSchema).safeParse(userProgress)
|
|
1124
1372
|
if (!parsed.success) {
|
|
1125
|
-
|
|
1373
|
+
logger.error(parsed.error, {
|
|
1374
|
+
event: 'adapter.userProgress.parse_failed',
|
|
1375
|
+
userId,
|
|
1376
|
+
})
|
|
1126
1377
|
return []
|
|
1127
1378
|
}
|
|
1128
1379
|
return parsed.data
|
|
@@ -1153,41 +1404,63 @@ export function mySqlDrizzleAdapter(
|
|
|
1153
1404
|
.transform((val) => (val ? new Date(val) : null)),
|
|
1154
1405
|
})
|
|
1155
1406
|
|
|
1156
|
-
// Execute the optimized query
|
|
1407
|
+
// Execute the optimized query - create a flattened, properly ordered list
|
|
1157
1408
|
const results: any = await client.execute(sql`
|
|
1158
|
-
|
|
1159
|
-
|
|
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,
|
|
1160
1420
|
cr.type AS resource_type,
|
|
1161
1421
|
cr.fields->>'$.slug' AS resource_slug,
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
+
`)
|
|
1191
1464
|
// Process the results
|
|
1192
1465
|
const completedLessons: ResourceProgress[] = []
|
|
1193
1466
|
let nextResource: Partial<ContentResource> | null = null
|
|
@@ -1197,7 +1470,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1197
1470
|
const parsedRows = z.array(ResultRowSchema).safeParse(results.rows)
|
|
1198
1471
|
|
|
1199
1472
|
if (!parsedRows.success) {
|
|
1200
|
-
|
|
1473
|
+
logger.error(parsedRows.error, {
|
|
1474
|
+
event: 'adapter.moduleProgress.rows.parse_failed',
|
|
1475
|
+
moduleIdOrSlug,
|
|
1476
|
+
userId: user.id,
|
|
1477
|
+
})
|
|
1201
1478
|
return {
|
|
1202
1479
|
completedLessons: [],
|
|
1203
1480
|
nextResource: null,
|
|
@@ -1251,7 +1528,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1251
1528
|
})
|
|
1252
1529
|
const parsed = merchantChargeSchema.safeParse(mCharge)
|
|
1253
1530
|
if (!parsed.success) {
|
|
1254
|
-
|
|
1531
|
+
logger.error(parsed.error, {
|
|
1532
|
+
event: 'adapter.merchantCharge.parse_failed',
|
|
1533
|
+
merchantChargeId,
|
|
1534
|
+
})
|
|
1255
1535
|
return null
|
|
1256
1536
|
}
|
|
1257
1537
|
return parsed.data
|
|
@@ -1295,6 +1575,26 @@ export function mySqlDrizzleAdapter(
|
|
|
1295
1575
|
|
|
1296
1576
|
return null
|
|
1297
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
|
+
},
|
|
1298
1598
|
async getMerchantCoupon(
|
|
1299
1599
|
merchantCouponId: string,
|
|
1300
1600
|
): Promise<MerchantCoupon | null> {
|
|
@@ -1426,26 +1726,56 @@ export function mySqlDrizzleAdapter(
|
|
|
1426
1726
|
currentProduct.price.unitAmount.toString() !==
|
|
1427
1727
|
input.price?.unitAmount.toString()
|
|
1428
1728
|
|
|
1429
|
-
if
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
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
|
+
)
|
|
1439
1739
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1740
|
+
if (!currentMerchantPrice || !currentMerchantPrice.identifier) {
|
|
1741
|
+
throw new Error(`Merchant price not found`)
|
|
1742
|
+
}
|
|
1443
1743
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1744
|
+
const currentStripePrice = await paymentProvider.getPrice(
|
|
1745
|
+
currentMerchantPrice.identifier,
|
|
1746
|
+
)
|
|
1447
1747
|
|
|
1448
|
-
|
|
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 = {
|
|
1449
1779
|
product: stripeProduct.id,
|
|
1450
1780
|
unit_amount: Math.floor(Number(input.price?.unitAmount || 0) * 100),
|
|
1451
1781
|
currency: 'usd',
|
|
@@ -1453,8 +1783,26 @@ export function mySqlDrizzleAdapter(
|
|
|
1453
1783
|
slug: input.fields.slug,
|
|
1454
1784
|
},
|
|
1455
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,
|
|
1456
1802
|
})
|
|
1457
1803
|
|
|
1804
|
+
const newStripePrice = await paymentProvider.createPrice(priceParams)
|
|
1805
|
+
|
|
1458
1806
|
await paymentProvider.updateProduct(stripeProduct.id, {
|
|
1459
1807
|
default_price: newStripePrice.id,
|
|
1460
1808
|
})
|
|
@@ -1543,12 +1891,22 @@ export function mySqlDrizzleAdapter(
|
|
|
1543
1891
|
id: newProductId,
|
|
1544
1892
|
name: input.name,
|
|
1545
1893
|
status: 1,
|
|
1546
|
-
type: 'self-paced',
|
|
1894
|
+
type: input.type || 'self-paced',
|
|
1547
1895
|
quantityAvailable: input.quantityAvailable,
|
|
1548
1896
|
fields: {
|
|
1549
|
-
state: 'draft',
|
|
1550
|
-
visibility: 'unlisted',
|
|
1897
|
+
state: input.state || 'draft',
|
|
1898
|
+
visibility: input.visibility || 'unlisted',
|
|
1551
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
|
+
}),
|
|
1552
1910
|
},
|
|
1553
1911
|
}
|
|
1554
1912
|
|
|
@@ -1581,6 +1939,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1581
1939
|
metadata: {
|
|
1582
1940
|
slug: product?.fields?.slug || null,
|
|
1583
1941
|
},
|
|
1942
|
+
// Membership products get recurring billing
|
|
1943
|
+
...(input.type === 'membership' && {
|
|
1944
|
+
recurring: { interval: input.billingInterval || 'year' },
|
|
1945
|
+
}),
|
|
1584
1946
|
})
|
|
1585
1947
|
|
|
1586
1948
|
const newMerchantProductId = `mproduct_${v4()}`
|
|
@@ -1643,16 +2005,20 @@ export function mySqlDrizzleAdapter(
|
|
|
1643
2005
|
})
|
|
1644
2006
|
const parsedProduct = productSchema.safeParse(productData)
|
|
1645
2007
|
if (!parsedProduct.success) {
|
|
1646
|
-
|
|
1647
|
-
'
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
)
|
|
2008
|
+
logger.error(parsedProduct.error, {
|
|
2009
|
+
event: 'adapter.product.parse_failed',
|
|
2010
|
+
productId: productData?.id,
|
|
2011
|
+
productSlugOrId,
|
|
2012
|
+
})
|
|
1651
2013
|
return null
|
|
1652
2014
|
}
|
|
1653
2015
|
return parsedProduct.data
|
|
1654
2016
|
} catch (e) {
|
|
1655
|
-
|
|
2017
|
+
logger.error(toError(e), {
|
|
2018
|
+
event: 'adapter.product.lookup.failed',
|
|
2019
|
+
productSlugOrId,
|
|
2020
|
+
withResources,
|
|
2021
|
+
})
|
|
1656
2022
|
return null
|
|
1657
2023
|
}
|
|
1658
2024
|
},
|
|
@@ -1723,11 +2089,14 @@ export function mySqlDrizzleAdapter(
|
|
|
1723
2089
|
)
|
|
1724
2090
|
|
|
1725
2091
|
if (!chargeForPurchase) {
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
+
})
|
|
1731
2100
|
return null
|
|
1732
2101
|
}
|
|
1733
2102
|
|
|
@@ -1748,6 +2117,40 @@ export function mySqlDrizzleAdapter(
|
|
|
1748
2117
|
|
|
1749
2118
|
return purchase.data
|
|
1750
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
|
+
},
|
|
1751
2154
|
async getPurchaseUserTransferById(options: { id: string }): Promise<
|
|
1752
2155
|
| (PurchaseUserTransfer & {
|
|
1753
2156
|
sourceUser: User
|
|
@@ -1772,6 +2175,7 @@ export function mySqlDrizzleAdapter(
|
|
|
1772
2175
|
sourceUser: userSchema,
|
|
1773
2176
|
targetUser: userSchema.nullable(),
|
|
1774
2177
|
purchase: purchaseSchema,
|
|
2178
|
+
organizationId: z.string().nullable(),
|
|
1775
2179
|
}),
|
|
1776
2180
|
)
|
|
1777
2181
|
.nullable()
|
|
@@ -1797,7 +2201,10 @@ export function mySqlDrizzleAdapter(
|
|
|
1797
2201
|
.safeParse(purchaseData)
|
|
1798
2202
|
|
|
1799
2203
|
if (!parsedPurchase.success) {
|
|
1800
|
-
|
|
2204
|
+
logger.error(parsedPurchase.error, {
|
|
2205
|
+
event: 'adapter.purchase.parse_failed',
|
|
2206
|
+
purchaseId,
|
|
2207
|
+
})
|
|
1801
2208
|
return null
|
|
1802
2209
|
}
|
|
1803
2210
|
return parsedPurchase.data
|
|
@@ -1825,15 +2232,85 @@ export function mySqlDrizzleAdapter(
|
|
|
1825
2232
|
const parsedPurchases = z.array(purchaseSchema).safeParse(userPurchases)
|
|
1826
2233
|
|
|
1827
2234
|
if (!parsedPurchases.success) {
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
2235
|
+
logger.error(parsedPurchases.error, {
|
|
2236
|
+
event: 'adapter.purchase.list.parse_failed',
|
|
2237
|
+
purchaseCount: userPurchases.length,
|
|
2238
|
+
userId,
|
|
2239
|
+
})
|
|
1832
2240
|
return []
|
|
1833
2241
|
}
|
|
1834
2242
|
|
|
1835
2243
|
return parsedPurchases.data
|
|
1836
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
|
+
},
|
|
1837
2314
|
async getPurchaseDetails(
|
|
1838
2315
|
purchaseId: string,
|
|
1839
2316
|
userId: string,
|
|
@@ -1876,7 +2353,11 @@ export function mySqlDrizzleAdapter(
|
|
|
1876
2353
|
const parsedPurchase = purchaseSchema.safeParse(thePurchase)
|
|
1877
2354
|
|
|
1878
2355
|
if (!parsedPurchase.success) {
|
|
1879
|
-
|
|
2356
|
+
logger.error(parsedPurchase.error, {
|
|
2357
|
+
event: 'adapter.purchase.details.parse_failed',
|
|
2358
|
+
purchaseId,
|
|
2359
|
+
userId,
|
|
2360
|
+
})
|
|
1880
2361
|
return {
|
|
1881
2362
|
availableUpgrades: [],
|
|
1882
2363
|
}
|
|
@@ -2075,7 +2556,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2075
2556
|
resourceProgressSchema.safeParse(lessonProgress)
|
|
2076
2557
|
|
|
2077
2558
|
if (!parsedLessonProgress.success) {
|
|
2078
|
-
|
|
2559
|
+
logger.error(parsedLessonProgress.error, {
|
|
2560
|
+
event: 'adapter.lessonProgress.parse_failed',
|
|
2561
|
+
lessonId: options.lessonId,
|
|
2562
|
+
userId: options.userId,
|
|
2563
|
+
})
|
|
2079
2564
|
return null
|
|
2080
2565
|
}
|
|
2081
2566
|
|
|
@@ -2186,7 +2671,10 @@ export function mySqlDrizzleAdapter(
|
|
|
2186
2671
|
status: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',
|
|
2187
2672
|
): Promise<Purchase | undefined> {
|
|
2188
2673
|
try {
|
|
2189
|
-
|
|
2674
|
+
logger.debug('adapter.purchase.status_update.started', {
|
|
2675
|
+
chargeId,
|
|
2676
|
+
status,
|
|
2677
|
+
})
|
|
2190
2678
|
const merchantChargeForPurchase =
|
|
2191
2679
|
await client.query.merchantCharge.findFirst({
|
|
2192
2680
|
where: or(
|
|
@@ -2195,7 +2683,12 @@ export function mySqlDrizzleAdapter(
|
|
|
2195
2683
|
),
|
|
2196
2684
|
})
|
|
2197
2685
|
|
|
2198
|
-
|
|
2686
|
+
logger.debug('adapter.purchase.status_update.charge_resolved', {
|
|
2687
|
+
chargeId,
|
|
2688
|
+
merchantChargeId: merchantChargeForPurchase?.id ?? null,
|
|
2689
|
+
merchantChargeIdentifier:
|
|
2690
|
+
merchantChargeForPurchase?.identifier ?? null,
|
|
2691
|
+
})
|
|
2199
2692
|
|
|
2200
2693
|
const parsedMerchantChargeForPurchase = merchantChargeSchema.parse(
|
|
2201
2694
|
merchantChargeForPurchase,
|
|
@@ -2228,7 +2721,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2228
2721
|
throw new Error(`no-purchase-found-for-charge ${chargeId}`)
|
|
2229
2722
|
}
|
|
2230
2723
|
} catch (e) {
|
|
2231
|
-
|
|
2724
|
+
logger.error(toError(e), {
|
|
2725
|
+
chargeId,
|
|
2726
|
+
event: 'adapter.purchase.status_update.failed',
|
|
2727
|
+
status,
|
|
2728
|
+
})
|
|
2232
2729
|
throw e
|
|
2233
2730
|
}
|
|
2234
2731
|
},
|
|
@@ -2363,7 +2860,10 @@ export function mySqlDrizzleAdapter(
|
|
|
2363
2860
|
const parsedResource = ContentResourceSchema.safeParse(resource)
|
|
2364
2861
|
|
|
2365
2862
|
if (!parsedResource.success) {
|
|
2366
|
-
|
|
2863
|
+
logger.error(parsedResource.error, {
|
|
2864
|
+
event: 'adapter.contentResource.parse_failed',
|
|
2865
|
+
resourceId: options.id,
|
|
2866
|
+
})
|
|
2367
2867
|
return null
|
|
2368
2868
|
}
|
|
2369
2869
|
|
|
@@ -2398,10 +2898,63 @@ export function mySqlDrizzleAdapter(
|
|
|
2398
2898
|
return parsedResource.success ? parsedResource.data : null
|
|
2399
2899
|
})
|
|
2400
2900
|
.catch((error) => {
|
|
2401
|
-
|
|
2901
|
+
logger.error(toError(error), {
|
|
2902
|
+
event: 'adapter.videoResource.lookup.failed',
|
|
2903
|
+
resourceId: id,
|
|
2904
|
+
})
|
|
2402
2905
|
throw error
|
|
2403
2906
|
})
|
|
2404
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
|
+
},
|
|
2405
2958
|
async createContentResource(data) {
|
|
2406
2959
|
const id = data.id || crypto.randomUUID()
|
|
2407
2960
|
|
|
@@ -2431,7 +2984,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2431
2984
|
const parsedResource = ContentResourceSchema.safeParse(resource)
|
|
2432
2985
|
|
|
2433
2986
|
if (!parsedResource.success) {
|
|
2434
|
-
|
|
2987
|
+
logger.error(parsedResource.error, {
|
|
2988
|
+
event: 'adapter.contentResource.parse_failed',
|
|
2989
|
+
resourceId: id,
|
|
2990
|
+
resourceType: data.type,
|
|
2991
|
+
})
|
|
2435
2992
|
throw new Error('Error parsing resource')
|
|
2436
2993
|
}
|
|
2437
2994
|
|
|
@@ -2476,28 +3033,1212 @@ export function mySqlDrizzleAdapter(
|
|
|
2476
3033
|
const parsedResource = ContentResourceSchema.safeParse(resource)
|
|
2477
3034
|
|
|
2478
3035
|
if (!parsedResource.success) {
|
|
2479
|
-
|
|
3036
|
+
logger.error(parsedResource.error, {
|
|
3037
|
+
event: 'adapter.contentResource.parse_failed',
|
|
3038
|
+
resourceId: resource?.id,
|
|
3039
|
+
resourceIdOrSlug: data,
|
|
3040
|
+
})
|
|
2480
3041
|
return null
|
|
2481
3042
|
}
|
|
2482
3043
|
|
|
2483
3044
|
return parsedResource.data
|
|
2484
3045
|
},
|
|
2485
|
-
async
|
|
2486
|
-
|
|
2487
|
-
|
|
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 || {}
|
|
2488
3061
|
|
|
2489
|
-
|
|
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
|
+
})
|
|
2490
3102
|
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
3103
|
+
const parsedEvent = ContentResourceSchema.safeParse(eventQuery)
|
|
3104
|
+
if (!parsedEvent.success) {
|
|
3105
|
+
logger.debug('Error parsing event', {
|
|
3106
|
+
eventQuery,
|
|
3107
|
+
error: parsedEvent.error,
|
|
2495
3108
|
})
|
|
3109
|
+
return null
|
|
3110
|
+
}
|
|
3111
|
+
|
|
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)
|
|
3121
|
+
|
|
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
|
+
}
|
|
2496
4241
|
|
|
2497
|
-
if (!personalOrganization) {
|
|
2498
|
-
throw new Error('Failed to create personal organization')
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
4242
|
const membership = await adapter.addMemberToOrganization({
|
|
2502
4243
|
organizationId: personalOrganization.id,
|
|
2503
4244
|
userId: id,
|
|
@@ -2522,7 +4263,11 @@ export function mySqlDrizzleAdapter(
|
|
|
2522
4263
|
|
|
2523
4264
|
return user
|
|
2524
4265
|
} catch (error) {
|
|
2525
|
-
|
|
4266
|
+
logger.error(toError(error), {
|
|
4267
|
+
email: data.email,
|
|
4268
|
+
event: 'adapter.user.create.failed',
|
|
4269
|
+
userId: id,
|
|
4270
|
+
})
|
|
2526
4271
|
throw error
|
|
2527
4272
|
}
|
|
2528
4273
|
},
|
|
@@ -2736,6 +4481,10 @@ export function mySqlDrizzleAdapter(
|
|
|
2736
4481
|
),
|
|
2737
4482
|
eq(organizationMembershipTable.userId, options.userId),
|
|
2738
4483
|
),
|
|
4484
|
+
with: {
|
|
4485
|
+
organization: true,
|
|
4486
|
+
user: true,
|
|
4487
|
+
},
|
|
2739
4488
|
})
|
|
2740
4489
|
|
|
2741
4490
|
if (currentMembership) {
|
|
@@ -2954,6 +4703,15 @@ export function mySqlDrizzleAdapter(
|
|
|
2954
4703
|
|
|
2955
4704
|
return subscriptionParsed.data
|
|
2956
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
|
+
},
|
|
2957
4715
|
}
|
|
2958
4716
|
|
|
2959
4717
|
return adapter
|