@coursebuilder/adapter-drizzle 2.1.1 → 3.0.0-canary.0.0c9beb2ae
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-KIQLUFJX.js → chunk-2GW3BJE2.js} +2 -2
- package/dist/{chunk-M3WM5Q7W.js → chunk-33ZN7FN7.js} +2 -2
- package/dist/chunk-4VZJQAPV.js +74 -0
- package/dist/chunk-5FLS4ZBX.js +109 -0
- package/dist/chunk-5YR5M22S.js +147 -0
- package/dist/chunk-6XQ6C3IX.js +18 -0
- package/dist/chunk-AIDM2CH6.js +146 -0
- package/dist/chunk-ALTODDUX.js +173 -0
- package/dist/chunk-AZY2VRAE.js +63 -0
- package/dist/chunk-BS7T27VL.js +239 -0
- package/dist/chunk-CN3FCRLR.js +55 -0
- package/dist/chunk-EAXC4CQD.js +245 -0
- package/dist/chunk-EDDG2YFS.js +249 -0
- package/dist/chunk-EDKWCMA7.js +210 -0
- package/dist/{chunk-7FBSP3VD.js → chunk-EDM6DKA5.js} +2 -2
- package/dist/{chunk-EZEQQJNH.js → chunk-EIQBTF5J.js} +2 -2
- package/dist/chunk-FYT7RTVW.js +248 -0
- package/dist/{chunk-F56AI52P.js → chunk-INZTF6QV.js} +2 -2
- package/dist/{chunk-AP5HBIV4.js → chunk-JVHX4JHA.js} +2 -2
- package/dist/chunk-LWEKWDLC.js +300 -0
- package/dist/chunk-OJPEBGGE.js +139 -0
- package/dist/chunk-QKOSV34V.js +16268 -0
- package/dist/chunk-QO3QE3LE.js +330 -0
- package/dist/chunk-QQKNNFFB.js +399 -0
- package/dist/chunk-RBHCV3UW.js +20 -0
- package/dist/chunk-TNH2C27F.js +252 -0
- package/dist/chunk-TNWNW666.js +133 -0
- package/dist/chunk-VBEXYQ6L.js +156 -0
- package/dist/chunk-W3ILNMLD.js +265 -0
- package/dist/chunk-WJ2ZUXL3.js +71 -0
- package/dist/{chunk-PJORQUQP.js → chunk-WUMDFFN3.js} +2 -2
- package/dist/{chunk-2AW5KYAK.js → chunk-X7DRYXB7.js} +2 -2
- package/dist/{chunk-PNPTKVJ4.js → chunk-XUBZKYH6.js} +24 -15
- package/dist/{chunk-WWBSNLRC.js → chunk-ZY4AFKUQ.js} +2 -2
- package/dist/index.d.ts +5 -11
- package/dist/index.js +41 -18
- package/dist/lib/mysql/cms-resource-actions.cjs +15912 -4127
- package/dist/lib/mysql/cms-resource-actions.cjs.map +1 -1
- package/dist/lib/mysql/cms-resource-actions.d.cts +8 -38
- package/dist/lib/mysql/cms-resource-actions.d.ts +8 -38
- package/dist/lib/mysql/cms-resource-actions.js +2 -1
- package/dist/lib/mysql/context.cjs +3380 -0
- package/dist/lib/mysql/context.cjs.map +1 -0
- package/dist/lib/mysql/context.d.cts +15 -0
- package/dist/lib/mysql/context.d.ts +15 -0
- package/dist/lib/mysql/context.js +34 -0
- package/dist/lib/mysql/context.js.map +1 -0
- package/dist/lib/mysql/index.cjs +21875 -15261
- package/dist/lib/mysql/index.cjs.map +1 -1
- package/dist/lib/mysql/index.d.cts +24 -14300
- package/dist/lib/mysql/index.d.ts +24 -14300
- package/dist/lib/mysql/index.js +46 -22
- package/dist/lib/mysql/ports/auth-storage.cjs +84 -0
- package/dist/lib/mysql/ports/auth-storage.cjs.map +1 -0
- package/dist/lib/mysql/ports/auth-storage.d.cts +22 -0
- package/dist/lib/mysql/ports/auth-storage.d.ts +22 -0
- package/dist/lib/mysql/ports/auth-storage.js +8 -0
- package/dist/lib/mysql/ports/auth-storage.js.map +1 -0
- package/dist/lib/mysql/ports/catalog.cjs +16688 -0
- package/dist/lib/mysql/ports/catalog.cjs.map +1 -0
- package/dist/lib/mysql/ports/catalog.d.cts +24 -0
- package/dist/lib/mysql/ports/catalog.d.ts +24 -0
- package/dist/lib/mysql/ports/catalog.js +9 -0
- package/dist/lib/mysql/ports/catalog.js.map +1 -0
- package/dist/lib/mysql/ports/content-resource.cjs +321 -0
- package/dist/lib/mysql/ports/content-resource.cjs.map +1 -0
- package/dist/lib/mysql/ports/content-resource.d.cts +23 -0
- package/dist/lib/mysql/ports/content-resource.d.ts +23 -0
- package/dist/lib/mysql/ports/content-resource.js +8 -0
- package/dist/lib/mysql/ports/content-resource.js.map +1 -0
- package/dist/lib/mysql/ports/coupon.cjs +16524 -0
- package/dist/lib/mysql/ports/coupon.cjs.map +1 -0
- package/dist/lib/mysql/ports/coupon.d.cts +22 -0
- package/dist/lib/mysql/ports/coupon.d.ts +22 -0
- package/dist/lib/mysql/ports/coupon.js +9 -0
- package/dist/lib/mysql/ports/coupon.js.map +1 -0
- package/dist/lib/mysql/ports/entitlement.cjs +16350 -0
- package/dist/lib/mysql/ports/entitlement.cjs.map +1 -0
- package/dist/lib/mysql/ports/entitlement.d.cts +22 -0
- package/dist/lib/mysql/ports/entitlement.d.ts +22 -0
- package/dist/lib/mysql/ports/entitlement.js +9 -0
- package/dist/lib/mysql/ports/entitlement.js.map +1 -0
- package/dist/lib/mysql/ports/identity.cjs +154 -0
- package/dist/lib/mysql/ports/identity.cjs.map +1 -0
- package/dist/lib/mysql/ports/identity.d.cts +22 -0
- package/dist/lib/mysql/ports/identity.d.ts +22 -0
- package/dist/lib/mysql/ports/identity.js +8 -0
- package/dist/lib/mysql/ports/identity.js.map +1 -0
- package/dist/lib/mysql/ports/merchant.cjs +194 -0
- package/dist/lib/mysql/ports/merchant.cjs.map +1 -0
- package/dist/lib/mysql/ports/merchant.d.cts +22 -0
- package/dist/lib/mysql/ports/merchant.d.ts +22 -0
- package/dist/lib/mysql/ports/merchant.js +8 -0
- package/dist/lib/mysql/ports/merchant.js.map +1 -0
- package/dist/lib/mysql/ports/organization.cjs +16426 -0
- package/dist/lib/mysql/ports/organization.cjs.map +1 -0
- package/dist/lib/mysql/ports/organization.d.cts +22 -0
- package/dist/lib/mysql/ports/organization.d.ts +22 -0
- package/dist/lib/mysql/ports/organization.js +9 -0
- package/dist/lib/mysql/ports/organization.js.map +1 -0
- package/dist/lib/mysql/ports/progress.cjs +16544 -0
- package/dist/lib/mysql/ports/progress.cjs.map +1 -0
- package/dist/lib/mysql/ports/progress.d.cts +22 -0
- package/dist/lib/mysql/ports/progress.d.ts +22 -0
- package/dist/lib/mysql/ports/progress.js +9 -0
- package/dist/lib/mysql/ports/progress.js.map +1 -0
- package/dist/lib/mysql/ports/purchase.cjs +16609 -0
- package/dist/lib/mysql/ports/purchase.cjs.map +1 -0
- package/dist/lib/mysql/ports/purchase.d.cts +23 -0
- package/dist/lib/mysql/ports/purchase.d.ts +23 -0
- package/dist/lib/mysql/ports/purchase.js +9 -0
- package/dist/lib/mysql/ports/purchase.js.map +1 -0
- package/dist/lib/mysql/ports/subscription.cjs +76 -0
- package/dist/lib/mysql/ports/subscription.cjs.map +1 -0
- package/dist/lib/mysql/ports/subscription.d.cts +22 -0
- package/dist/lib/mysql/ports/subscription.d.ts +22 -0
- package/dist/lib/mysql/ports/subscription.js +8 -0
- package/dist/lib/mysql/ports/subscription.js.map +1 -0
- package/dist/lib/mysql/schema.cjs +3373 -0
- package/dist/lib/mysql/schema.cjs.map +1 -0
- package/dist/lib/mysql/schema.d.cts +14385 -0
- package/dist/lib/mysql/schema.d.ts +14385 -0
- package/dist/lib/mysql/schema.js +35 -0
- package/dist/lib/mysql/schema.js.map +1 -0
- package/dist/lib/mysql/schemas/auth/accounts.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/accounts.js +4 -4
- package/dist/lib/mysql/schemas/auth/device-access-token.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/device-access-token.js +5 -5
- package/dist/lib/mysql/schemas/auth/device-verification.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/device-verification.js +5 -5
- package/dist/lib/mysql/schemas/auth/personal-access-token.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/personal-access-token.js +5 -5
- package/dist/lib/mysql/schemas/auth/profiles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/profiles.js +4 -4
- package/dist/lib/mysql/schemas/auth/role-permissions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/role-permissions.js +5 -5
- package/dist/lib/mysql/schemas/auth/roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/roles.js +4 -4
- package/dist/lib/mysql/schemas/auth/sessions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/sessions.js +5 -5
- package/dist/lib/mysql/schemas/auth/user-permissions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-permissions.js +4 -4
- package/dist/lib/mysql/schemas/auth/user-prefs.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-prefs.js +4 -4
- package/dist/lib/mysql/schemas/auth/user-roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/user-roles.js +4 -4
- package/dist/lib/mysql/schemas/auth/users.cjs +5 -1
- package/dist/lib/mysql/schemas/auth/users.cjs.map +1 -1
- package/dist/lib/mysql/schemas/auth/users.js +4 -4
- package/dist/lib/mysql/schemas/commerce/coupon.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/coupon.js +4 -4
- package/dist/lib/mysql/schemas/commerce/merchant-charge.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/merchant-charge.js +4 -4
- package/dist/lib/mysql/schemas/commerce/merchant-subscription.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/merchant-subscription.js +4 -4
- package/dist/lib/mysql/schemas/commerce/price.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/price.js +4 -4
- package/dist/lib/mysql/schemas/commerce/product.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/product.js +4 -4
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/purchase-user-transfer.js +5 -5
- package/dist/lib/mysql/schemas/commerce/purchase.cjs +11 -2
- package/dist/lib/mysql/schemas/commerce/purchase.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/purchase.js +4 -4
- package/dist/lib/mysql/schemas/commerce/subscription.cjs +6 -1
- package/dist/lib/mysql/schemas/commerce/subscription.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/subscription.js +4 -4
- package/dist/lib/mysql/schemas/commerce/upgradable-products.cjs.map +1 -1
- package/dist/lib/mysql/schemas/commerce/upgradable-products.js +5 -5
- package/dist/lib/mysql/schemas/communication/comment.cjs +5 -1
- package/dist/lib/mysql/schemas/communication/comment.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/comment.js +4 -4
- package/dist/lib/mysql/schemas/communication/communication-preferences.cjs +5 -1
- package/dist/lib/mysql/schemas/communication/communication-preferences.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/communication-preferences.js +4 -4
- package/dist/lib/mysql/schemas/communication/question-response.cjs.map +1 -1
- package/dist/lib/mysql/schemas/communication/question-response.js +5 -5
- package/dist/lib/mysql/schemas/content/content-contributions.cjs +5 -1
- package/dist/lib/mysql/schemas/content/content-contributions.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-contributions.js +4 -4
- package/dist/lib/mysql/schemas/content/content-resource-product.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-product.js +4 -4
- package/dist/lib/mysql/schemas/content/content-resource-resource.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-resource.js +4 -4
- package/dist/lib/mysql/schemas/content/content-resource-tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-tag.js +4 -4
- package/dist/lib/mysql/schemas/content/content-resource-version.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource-version.js +4 -4
- package/dist/lib/mysql/schemas/content/content-resource.cjs +5 -1
- package/dist/lib/mysql/schemas/content/content-resource.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/content-resource.js +4 -4
- package/dist/lib/mysql/schemas/content/tag-tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/tag-tag.js +4 -4
- package/dist/lib/mysql/schemas/content/tag.cjs.map +1 -1
- package/dist/lib/mysql/schemas/content/tag.js +4 -4
- package/dist/lib/mysql/schemas/entitlements/entitlement.cjs +11 -2
- package/dist/lib/mysql/schemas/entitlements/entitlement.cjs.map +1 -1
- package/dist/lib/mysql/schemas/entitlements/entitlement.js +5 -5
- package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs +11 -2
- package/dist/lib/mysql/schemas/org/organization-membership-roles.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organization-membership-roles.js +4 -4
- package/dist/lib/mysql/schemas/org/organization-memberships.cjs +11 -2
- package/dist/lib/mysql/schemas/org/organization-memberships.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organization-memberships.d.cts +16 -0
- package/dist/lib/mysql/schemas/org/organization-memberships.d.ts +16 -0
- package/dist/lib/mysql/schemas/org/organization-memberships.js +4 -4
- package/dist/lib/mysql/schemas/org/organizations.cjs +11 -2
- package/dist/lib/mysql/schemas/org/organizations.cjs.map +1 -1
- package/dist/lib/mysql/schemas/org/organizations.d.cts +16 -0
- package/dist/lib/mysql/schemas/org/organizations.d.ts +16 -0
- package/dist/lib/mysql/schemas/org/organizations.js +4 -4
- package/dist/lib/mysql/workflows/content-resource-workflow-utils.cjs +40 -0
- package/dist/lib/mysql/workflows/content-resource-workflow-utils.cjs.map +1 -0
- package/dist/lib/mysql/workflows/content-resource-workflow-utils.d.cts +4 -0
- package/dist/lib/mysql/workflows/content-resource-workflow-utils.d.ts +4 -0
- package/dist/lib/mysql/workflows/content-resource-workflow-utils.js +10 -0
- package/dist/lib/mysql/workflows/content-resource-workflow-utils.js.map +1 -0
- package/dist/lib/mysql/workflows/create-cohort.cjs +184 -0
- package/dist/lib/mysql/workflows/create-cohort.cjs.map +1 -0
- package/dist/lib/mysql/workflows/create-cohort.d.cts +24 -0
- package/dist/lib/mysql/workflows/create-cohort.d.ts +24 -0
- package/dist/lib/mysql/workflows/create-cohort.js +9 -0
- package/dist/lib/mysql/workflows/create-cohort.js.map +1 -0
- package/dist/lib/mysql/workflows/create-event-series.cjs +283 -0
- package/dist/lib/mysql/workflows/create-event-series.cjs.map +1 -0
- package/dist/lib/mysql/workflows/create-event-series.d.cts +24 -0
- package/dist/lib/mysql/workflows/create-event-series.d.ts +24 -0
- package/dist/lib/mysql/workflows/create-event-series.js +9 -0
- package/dist/lib/mysql/workflows/create-event-series.js.map +1 -0
- package/dist/lib/mysql/workflows/create-event.cjs +194 -0
- package/dist/lib/mysql/workflows/create-event.cjs.map +1 -0
- package/dist/lib/mysql/workflows/create-event.d.cts +24 -0
- package/dist/lib/mysql/workflows/create-event.d.ts +24 -0
- package/dist/lib/mysql/workflows/create-event.js +9 -0
- package/dist/lib/mysql/workflows/create-event.js.map +1 -0
- package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.cjs +231 -0
- package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.cjs.map +1 -0
- package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.d.cts +22 -0
- package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.d.ts +22 -0
- package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.js +8 -0
- package/dist/lib/mysql/workflows/create-merchant-charge-and-purchase.js.map +1 -0
- package/dist/lib/mysql/workflows/create-workshop.cjs +286 -0
- package/dist/lib/mysql/workflows/create-workshop.cjs.map +1 -0
- package/dist/lib/mysql/workflows/create-workshop.d.cts +24 -0
- package/dist/lib/mysql/workflows/create-workshop.d.ts +24 -0
- package/dist/lib/mysql/workflows/create-workshop.js +9 -0
- package/dist/lib/mysql/workflows/create-workshop.js.map +1 -0
- package/dist/lib/mysql/workflows/redeem-full-price-coupon.cjs +130 -0
- package/dist/lib/mysql/workflows/redeem-full-price-coupon.cjs.map +1 -0
- package/dist/lib/mysql/workflows/redeem-full-price-coupon.d.cts +24 -0
- package/dist/lib/mysql/workflows/redeem-full-price-coupon.d.ts +24 -0
- package/dist/lib/mysql/workflows/redeem-full-price-coupon.js +8 -0
- package/dist/lib/mysql/workflows/redeem-full-price-coupon.js.map +1 -0
- package/dist/lib/mysql/workflows/transfer-purchase-to-user.cjs +95 -0
- package/dist/lib/mysql/workflows/transfer-purchase-to-user.cjs.map +1 -0
- package/dist/lib/mysql/workflows/transfer-purchase-to-user.d.cts +30 -0
- package/dist/lib/mysql/workflows/transfer-purchase-to-user.d.ts +30 -0
- package/dist/lib/mysql/workflows/transfer-purchase-to-user.js +8 -0
- package/dist/lib/mysql/workflows/transfer-purchase-to-user.js.map +1 -0
- package/dist/lib/utils.d.cts +1 -10
- package/dist/lib/utils.d.ts +1 -10
- package/dist/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/package.json +9 -5
- package/src/index.ts +3 -2
- package/src/lib/mysql/context.ts +20 -0
- package/src/lib/mysql/index.ts +118 -4873
- package/src/lib/mysql/ports/auth-storage.ts +139 -0
- package/src/lib/mysql/ports/catalog.ts +610 -0
- package/src/lib/mysql/ports/content-resource.ts +442 -0
- package/src/lib/mysql/ports/coupon.ts +387 -0
- package/src/lib/mysql/ports/entitlement.ts +96 -0
- package/src/lib/mysql/ports/identity.ts +160 -0
- package/src/lib/mysql/ports/merchant.ts +274 -0
- package/src/lib/mysql/ports/organization.ts +216 -0
- package/src/lib/mysql/ports/progress.ts +366 -0
- package/src/lib/mysql/ports/purchase.ts +517 -0
- package/src/lib/mysql/ports/subscription.ts +93 -0
- package/src/lib/mysql/schema.ts +374 -0
- package/src/lib/mysql/schemas/org/organization-memberships.ts +7 -0
- package/src/lib/mysql/schemas/org/organizations.ts +8 -0
- package/src/lib/mysql/workflows/content-resource-workflow-utils.ts +22 -0
- package/src/lib/mysql/workflows/create-cohort.ts +239 -0
- package/src/lib/mysql/workflows/create-event-series.ts +384 -0
- package/src/lib/mysql/workflows/create-event.ts +232 -0
- package/src/lib/mysql/workflows/create-merchant-charge-and-purchase.ts +365 -0
- package/src/lib/mysql/workflows/create-workshop.ts +372 -0
- package/src/lib/mysql/workflows/redeem-full-price-coupon.ts +179 -0
- package/src/lib/mysql/workflows/transfer-purchase-to-user.ts +142 -0
- package/src/types.ts +33 -0
- package/dist/authjs.d.ts +0 -27
- package/dist/authjs.js +0 -1
- package/dist/chunk-62VSIVME.js +0 -4714
- package/dist/chunk-RVXF3JZ7.js +0 -8732
- package/dist/chunk-WEJL5OFI.js +0 -8732
- package/src/authjs.ts +0 -34
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import slugify from '@sindresorhus/slugify'
|
|
2
|
+
import { asc, eq } from 'drizzle-orm'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
CatalogPort,
|
|
6
|
+
ContentResourcePort,
|
|
7
|
+
CouponPort,
|
|
8
|
+
} from '@coursebuilder/core/ports'
|
|
9
|
+
import {
|
|
10
|
+
ContentResourceSchema,
|
|
11
|
+
type ContentResource,
|
|
12
|
+
} from '@coursebuilder/core/schemas/content-resource-schema'
|
|
13
|
+
import { logger as baseLogger } from '@coursebuilder/utils/logger'
|
|
14
|
+
|
|
15
|
+
import type { MySqlContext } from '../context.js'
|
|
16
|
+
import {
|
|
17
|
+
guid,
|
|
18
|
+
normalizeExpirationDate,
|
|
19
|
+
} from './content-resource-workflow-utils.js'
|
|
20
|
+
|
|
21
|
+
const logger = baseLogger as unknown as {
|
|
22
|
+
debug: (message: string, attrs?: Record<string, unknown>) => void
|
|
23
|
+
error: (error: Error, attrs?: Record<string, unknown>) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type CreateWorkshopOperations = Pick<CatalogPort, 'createProduct'> &
|
|
27
|
+
Pick<CouponPort, 'createCoupon'>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates the private workshop creation workflow.
|
|
31
|
+
*
|
|
32
|
+
* @param context - The shared MySQL client and table set.
|
|
33
|
+
* @param operations - Narrow catalog, coupon, and content resource operations.
|
|
34
|
+
* @returns The content resource creation operation.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const workflow = createWorkshopWorkflow(context, operations)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function createWorkshopWorkflow(
|
|
42
|
+
{ client, tables }: MySqlContext,
|
|
43
|
+
operations: CreateWorkshopOperations,
|
|
44
|
+
): Pick<ContentResourcePort, 'createWorkshop'> {
|
|
45
|
+
const {
|
|
46
|
+
contentResource,
|
|
47
|
+
contentResourceResource,
|
|
48
|
+
contentResourceProduct,
|
|
49
|
+
contentResourceTag,
|
|
50
|
+
} = tables
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
async createWorkshop(
|
|
54
|
+
input: {
|
|
55
|
+
workshop: {
|
|
56
|
+
title: string
|
|
57
|
+
description?: string
|
|
58
|
+
tagIds?:
|
|
59
|
+
| { id: string; fields: { label: string; name: string } }[]
|
|
60
|
+
| null
|
|
61
|
+
}
|
|
62
|
+
createProduct?: boolean
|
|
63
|
+
pricing: {
|
|
64
|
+
price?: number | null
|
|
65
|
+
quantity?: number | null
|
|
66
|
+
}
|
|
67
|
+
coupon?: {
|
|
68
|
+
enabled: boolean
|
|
69
|
+
percentageDiscount?: string
|
|
70
|
+
expires?: Date
|
|
71
|
+
}
|
|
72
|
+
structure: Array<
|
|
73
|
+
| {
|
|
74
|
+
type: 'section'
|
|
75
|
+
title: string
|
|
76
|
+
lessons: { title: string; videoResourceId?: string }[]
|
|
77
|
+
}
|
|
78
|
+
| { type: 'lesson'; title: string; videoResourceId?: string }
|
|
79
|
+
>
|
|
80
|
+
},
|
|
81
|
+
userId: string,
|
|
82
|
+
) {
|
|
83
|
+
const hash = guid()
|
|
84
|
+
const workshopId = `workshop~${hash}`
|
|
85
|
+
|
|
86
|
+
// Transaction only handles database operations
|
|
87
|
+
const result = await client.transaction(async (tx) => {
|
|
88
|
+
// Create workshop content resource
|
|
89
|
+
await tx.insert(contentResource).values({
|
|
90
|
+
id: workshopId,
|
|
91
|
+
type: 'workshop',
|
|
92
|
+
createdById: userId,
|
|
93
|
+
fields: {
|
|
94
|
+
title: input.workshop.title,
|
|
95
|
+
description: input.workshop.description,
|
|
96
|
+
state: 'draft',
|
|
97
|
+
visibility: 'unlisted',
|
|
98
|
+
slug: slugify(`${input.workshop.title}~${hash}`),
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Fetch created workshop
|
|
103
|
+
const workshop = await tx.query.contentResource.findFirst({
|
|
104
|
+
where: eq(contentResource.id, workshopId),
|
|
105
|
+
with: {
|
|
106
|
+
resources: {
|
|
107
|
+
with: {
|
|
108
|
+
resource: {
|
|
109
|
+
with: {
|
|
110
|
+
resources: {
|
|
111
|
+
with: {
|
|
112
|
+
resource: true,
|
|
113
|
+
},
|
|
114
|
+
orderBy: asc(contentResourceResource.position),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
orderBy: asc(contentResourceResource.position),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (!workshop) {
|
|
125
|
+
throw new Error('Failed to create workshop')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parsedWorkshop = ContentResourceSchema.safeParse(workshop)
|
|
129
|
+
if (!parsedWorkshop.success) {
|
|
130
|
+
throw new Error('Invalid workshop data')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Associate tags with workshop
|
|
134
|
+
if (input.workshop.tagIds) {
|
|
135
|
+
await tx.insert(contentResourceTag).values(
|
|
136
|
+
input.workshop.tagIds.map((tag) => ({
|
|
137
|
+
contentResourceId: workshopId,
|
|
138
|
+
tagId: tag.id,
|
|
139
|
+
createdAt: new Date(),
|
|
140
|
+
updatedAt: new Date(),
|
|
141
|
+
position: 0,
|
|
142
|
+
})),
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Create sections and lessons
|
|
147
|
+
const createdSections: ContentResource[] = []
|
|
148
|
+
const createdLessons: ContentResource[] = []
|
|
149
|
+
let position = 0
|
|
150
|
+
|
|
151
|
+
for (const item of input.structure) {
|
|
152
|
+
if (item.type === 'section') {
|
|
153
|
+
// Create section
|
|
154
|
+
const sectionHash = guid()
|
|
155
|
+
const sectionId = `section~${sectionHash}`
|
|
156
|
+
|
|
157
|
+
await tx.insert(contentResource).values({
|
|
158
|
+
id: sectionId,
|
|
159
|
+
type: 'section',
|
|
160
|
+
createdById: userId,
|
|
161
|
+
fields: {
|
|
162
|
+
title: item.title,
|
|
163
|
+
state: 'draft',
|
|
164
|
+
visibility: 'unlisted',
|
|
165
|
+
slug: slugify(`${item.title}~${sectionHash}`),
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Link section to workshop
|
|
170
|
+
await tx.insert(contentResourceResource).values({
|
|
171
|
+
resourceOfId: workshopId,
|
|
172
|
+
resourceId: sectionId,
|
|
173
|
+
position,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const section = await tx.query.contentResource.findFirst({
|
|
177
|
+
where: eq(contentResource.id, sectionId),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if (section) {
|
|
181
|
+
createdSections.push(section as ContentResource)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create lessons in section
|
|
185
|
+
let lessonPosition = 0
|
|
186
|
+
for (const lessonData of item.lessons) {
|
|
187
|
+
const lessonHash = guid()
|
|
188
|
+
const lessonId = `lesson_${lessonHash}`
|
|
189
|
+
|
|
190
|
+
await tx.insert(contentResource).values({
|
|
191
|
+
id: lessonId,
|
|
192
|
+
type: 'lesson',
|
|
193
|
+
createdById: userId,
|
|
194
|
+
fields: {
|
|
195
|
+
title: lessonData.title,
|
|
196
|
+
state: 'draft',
|
|
197
|
+
visibility: 'unlisted',
|
|
198
|
+
slug: slugify(`${lessonData.title}~${lessonHash}`),
|
|
199
|
+
lessonType: 'lesson',
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Link video if provided
|
|
204
|
+
if (lessonData.videoResourceId) {
|
|
205
|
+
await tx.insert(contentResourceResource).values({
|
|
206
|
+
resourceOfId: lessonId,
|
|
207
|
+
resourceId: lessonData.videoResourceId,
|
|
208
|
+
position: 0,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Link lesson to section
|
|
213
|
+
await tx.insert(contentResourceResource).values({
|
|
214
|
+
resourceOfId: sectionId,
|
|
215
|
+
resourceId: lessonId,
|
|
216
|
+
position: lessonPosition,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const lesson = await tx.query.contentResource.findFirst({
|
|
220
|
+
where: eq(contentResource.id, lessonId),
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
if (lesson) {
|
|
224
|
+
createdLessons.push(lesson as ContentResource)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
lessonPosition++
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
position++
|
|
231
|
+
} else {
|
|
232
|
+
// Create top-level lesson
|
|
233
|
+
const lessonHash = guid()
|
|
234
|
+
const lessonId = `lesson_${lessonHash}`
|
|
235
|
+
|
|
236
|
+
await tx.insert(contentResource).values({
|
|
237
|
+
id: lessonId,
|
|
238
|
+
type: 'lesson',
|
|
239
|
+
createdById: userId,
|
|
240
|
+
fields: {
|
|
241
|
+
title: item.title,
|
|
242
|
+
state: 'draft',
|
|
243
|
+
visibility: 'unlisted',
|
|
244
|
+
slug: slugify(`${item.title}~${lessonHash}`),
|
|
245
|
+
lessonType: 'lesson',
|
|
246
|
+
},
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// Link video if provided
|
|
250
|
+
if (item.videoResourceId) {
|
|
251
|
+
await tx.insert(contentResourceResource).values({
|
|
252
|
+
resourceOfId: lessonId,
|
|
253
|
+
resourceId: item.videoResourceId,
|
|
254
|
+
position: 0,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Link lesson to workshop
|
|
259
|
+
await tx.insert(contentResourceResource).values({
|
|
260
|
+
resourceOfId: workshopId,
|
|
261
|
+
resourceId: lessonId,
|
|
262
|
+
position,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const lesson = await tx.query.contentResource.findFirst({
|
|
266
|
+
where: eq(contentResource.id, lessonId),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
if (lesson) {
|
|
270
|
+
createdLessons.push(lesson as ContentResource)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
position++
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
workshop: parsedWorkshop.data,
|
|
279
|
+
sections: createdSections,
|
|
280
|
+
lessons: createdLessons,
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
// Handle product and coupon creation outside transaction
|
|
285
|
+
let product: any = null
|
|
286
|
+
if (
|
|
287
|
+
input.createProduct &&
|
|
288
|
+
input.pricing.price &&
|
|
289
|
+
input.pricing.price > 0
|
|
290
|
+
) {
|
|
291
|
+
try {
|
|
292
|
+
product = await operations.createProduct({
|
|
293
|
+
name: input.workshop.title,
|
|
294
|
+
price: input.pricing.price,
|
|
295
|
+
quantityAvailable: input.pricing.quantity ?? -1,
|
|
296
|
+
type: 'self-paced',
|
|
297
|
+
state: 'published',
|
|
298
|
+
visibility: 'public',
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
if (product) {
|
|
302
|
+
// Link product to workshop resource
|
|
303
|
+
await client.insert(contentResourceProduct).values({
|
|
304
|
+
resourceId: workshopId,
|
|
305
|
+
productId: product.id,
|
|
306
|
+
position: 0,
|
|
307
|
+
metadata: {
|
|
308
|
+
addedBy: userId,
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
// Create coupon if enabled
|
|
313
|
+
if (
|
|
314
|
+
input.coupon?.enabled &&
|
|
315
|
+
input.coupon.percentageDiscount &&
|
|
316
|
+
input.coupon.expires
|
|
317
|
+
) {
|
|
318
|
+
try {
|
|
319
|
+
const finalExpires = normalizeExpirationDate(
|
|
320
|
+
input.coupon.expires,
|
|
321
|
+
)
|
|
322
|
+
await operations.createCoupon({
|
|
323
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
324
|
+
expires: finalExpires || null,
|
|
325
|
+
restrictedToProductId: product.id,
|
|
326
|
+
default: true,
|
|
327
|
+
maxUses: -1,
|
|
328
|
+
quantity: '-1',
|
|
329
|
+
status: 1,
|
|
330
|
+
fields: {},
|
|
331
|
+
})
|
|
332
|
+
logger.debug('workshop.create.coupon.success', {
|
|
333
|
+
workshopId,
|
|
334
|
+
productId: product.id,
|
|
335
|
+
userId,
|
|
336
|
+
percentageDiscount: input.coupon.percentageDiscount,
|
|
337
|
+
})
|
|
338
|
+
} catch (couponError) {
|
|
339
|
+
logger.error(
|
|
340
|
+
new Error(
|
|
341
|
+
`Failed to create coupon for workshop: ${couponError}`,
|
|
342
|
+
),
|
|
343
|
+
)
|
|
344
|
+
logger.debug('workshop.create.coupon.failed', {
|
|
345
|
+
workshopId,
|
|
346
|
+
productId: product.id,
|
|
347
|
+
userId,
|
|
348
|
+
})
|
|
349
|
+
// Don't throw - workshop creation should succeed even if coupon fails
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} catch (productError) {
|
|
354
|
+
logger.error(
|
|
355
|
+
new Error(`Failed to create product for workshop: ${productError}`),
|
|
356
|
+
)
|
|
357
|
+
logger.debug('workshop.create.product.failed', {
|
|
358
|
+
workshopId,
|
|
359
|
+
userId,
|
|
360
|
+
price: input.pricing.price,
|
|
361
|
+
})
|
|
362
|
+
// Don't throw - workshop creation should succeed even if product fails
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
...result,
|
|
368
|
+
product,
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { and, eq } from 'drizzle-orm'
|
|
2
|
+
import { v4 } from 'uuid'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
CouponPort,
|
|
6
|
+
IdentityPort,
|
|
7
|
+
OrganizationPort,
|
|
8
|
+
PurchasePort,
|
|
9
|
+
} from '@coursebuilder/core/ports'
|
|
10
|
+
import { logger } from '@coursebuilder/utils/logger'
|
|
11
|
+
import { validateCoupon } from '@coursebuilder/commerce/validate-coupon'
|
|
12
|
+
|
|
13
|
+
import type { MySqlContext } from '../context.js'
|
|
14
|
+
|
|
15
|
+
type RedeemFullPriceCouponOperations = Pick<
|
|
16
|
+
CouponPort,
|
|
17
|
+
'getCouponWithBulkPurchases' | 'incrementCouponUsedCount'
|
|
18
|
+
> &
|
|
19
|
+
Pick<IdentityPort, 'findOrCreateUser' | 'getUserById'> &
|
|
20
|
+
Pick<OrganizationPort, 'getMembershipsForUser'> &
|
|
21
|
+
Pick<
|
|
22
|
+
PurchasePort,
|
|
23
|
+
| 'createPurchase'
|
|
24
|
+
| 'createPurchaseTransfer'
|
|
25
|
+
| 'getExistingNonBulkValidPurchasesOfProduct'
|
|
26
|
+
| 'getPurchase'
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
const normalizeLearnerEmail = (email: string) =>
|
|
30
|
+
email.trim().toLowerCase().replace(/\s+/g, '+')
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Creates the private full-price coupon redemption workflow.
|
|
34
|
+
*
|
|
35
|
+
* @param context - The shared MySQL client and table set.
|
|
36
|
+
* @param operations - Narrow identity, organization, purchase, and coupon operations.
|
|
37
|
+
* @returns The purchase-owned coupon redemption operation.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const workflow = createRedeemFullPriceCouponWorkflow(context, operations)
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function createRedeemFullPriceCouponWorkflow(
|
|
45
|
+
{ client, tables }: MySqlContext,
|
|
46
|
+
operations: RedeemFullPriceCouponOperations,
|
|
47
|
+
): Pick<PurchasePort, 'redeemFullPriceCoupon'> {
|
|
48
|
+
const { purchases: purchaseTable } = tables
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
async redeemFullPriceCoupon(options) {
|
|
52
|
+
const {
|
|
53
|
+
email: baseEmail,
|
|
54
|
+
couponId,
|
|
55
|
+
productIds,
|
|
56
|
+
currentUserId,
|
|
57
|
+
redeemingProductId,
|
|
58
|
+
} = options
|
|
59
|
+
const email = normalizeLearnerEmail(String(baseEmail))
|
|
60
|
+
|
|
61
|
+
const coupon = await operations.getCouponWithBulkPurchases(couponId)
|
|
62
|
+
|
|
63
|
+
const productId =
|
|
64
|
+
(coupon && (coupon.restrictedToProductId as string)) ||
|
|
65
|
+
redeemingProductId
|
|
66
|
+
|
|
67
|
+
if (!productId) throw new Error(`unable-to-find-any-product-id`)
|
|
68
|
+
|
|
69
|
+
const couponValidation = validateCoupon(coupon, productIds)
|
|
70
|
+
|
|
71
|
+
if (coupon && couponValidation.isRedeemable) {
|
|
72
|
+
// if the Coupon is the Bulk Coupon of a Bulk Purchase,
|
|
73
|
+
// then a bulk coupon is being redeemed
|
|
74
|
+
const bulkCouponRedemption = Boolean(coupon.maxUses > 1)
|
|
75
|
+
|
|
76
|
+
const { user } = await operations.findOrCreateUser(email)
|
|
77
|
+
|
|
78
|
+
if (!user) throw new Error(`unable-to-create-user-${email}`)
|
|
79
|
+
|
|
80
|
+
const currentUser = currentUserId
|
|
81
|
+
? await operations.getUserById(currentUserId)
|
|
82
|
+
: null
|
|
83
|
+
|
|
84
|
+
const redeemingForCurrentUser = currentUser?.id === user.id
|
|
85
|
+
|
|
86
|
+
if (bulkCouponRedemption) {
|
|
87
|
+
const existingValidRedemption =
|
|
88
|
+
await client.query.purchases.findFirst({
|
|
89
|
+
where: and(
|
|
90
|
+
eq(purchaseTable.redeemedBulkCouponId, coupon.id),
|
|
91
|
+
eq(purchaseTable.userId, user.id),
|
|
92
|
+
eq(purchaseTable.status, 'Valid'),
|
|
93
|
+
),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (existingValidRedemption) {
|
|
97
|
+
logger.info('adapter.coupon.redeem.duplicate_team_seat', {
|
|
98
|
+
couponId: coupon.id,
|
|
99
|
+
currentUserId,
|
|
100
|
+
email,
|
|
101
|
+
productId,
|
|
102
|
+
purchaseId: existingValidRedemption.id,
|
|
103
|
+
userId: user.id,
|
|
104
|
+
})
|
|
105
|
+
return {
|
|
106
|
+
error: {
|
|
107
|
+
message: 'team-seat-redemption-already-claimed',
|
|
108
|
+
},
|
|
109
|
+
redeemingForCurrentUser,
|
|
110
|
+
purchase: null,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// To prevent double-purchasing, check if this user already has a
|
|
116
|
+
// Purchase record for this product that is valid and wasn't a bulk
|
|
117
|
+
// coupon purchase.
|
|
118
|
+
const existingPurchases =
|
|
119
|
+
await operations.getExistingNonBulkValidPurchasesOfProduct({
|
|
120
|
+
userId: user.id,
|
|
121
|
+
productId,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
if (existingPurchases.length > 0) {
|
|
125
|
+
const errorMessage = `already-purchased-${email}`
|
|
126
|
+
logger.info('adapter.coupon.redeem.already_purchased', {
|
|
127
|
+
couponId: coupon.id,
|
|
128
|
+
currentUserId,
|
|
129
|
+
email,
|
|
130
|
+
productId,
|
|
131
|
+
userId: user.id,
|
|
132
|
+
})
|
|
133
|
+
return {
|
|
134
|
+
error: {
|
|
135
|
+
message: errorMessage,
|
|
136
|
+
},
|
|
137
|
+
redeemingForCurrentUser,
|
|
138
|
+
purchase: null,
|
|
139
|
+
}
|
|
140
|
+
throw new Error(errorMessage)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const purchaseId = `purchase-${v4()}`
|
|
144
|
+
const userMemberships = await operations.getMembershipsForUser(user.id)
|
|
145
|
+
const organizationId =
|
|
146
|
+
coupon.organizationId ||
|
|
147
|
+
userMemberships.find((m) => m.organization.name?.includes(user.email))
|
|
148
|
+
?.organizationId // safer way to make sure we are using personal organization
|
|
149
|
+
|
|
150
|
+
await operations.createPurchase({
|
|
151
|
+
id: purchaseId,
|
|
152
|
+
userId: user.id,
|
|
153
|
+
couponId: bulkCouponRedemption ? null : coupon.id,
|
|
154
|
+
redeemedBulkCouponId: bulkCouponRedemption ? coupon.id : null,
|
|
155
|
+
productId,
|
|
156
|
+
totalAmount: '0',
|
|
157
|
+
organizationId,
|
|
158
|
+
metadata: {
|
|
159
|
+
couponUsedId: bulkCouponRedemption ? null : coupon.id,
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const newPurchase = await operations.getPurchase(purchaseId)
|
|
164
|
+
|
|
165
|
+
await operations.incrementCouponUsedCount(coupon.id)
|
|
166
|
+
|
|
167
|
+
await operations.createPurchaseTransfer({
|
|
168
|
+
sourceUserId: user.id,
|
|
169
|
+
purchaseId: purchaseId,
|
|
170
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return { purchase: newPurchase, redeemingForCurrentUser }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm'
|
|
2
|
+
import { v4 } from 'uuid'
|
|
3
|
+
|
|
4
|
+
import type { PurchasePort } from '@coursebuilder/core/ports'
|
|
5
|
+
import { merchantChargeSchema, userSchema } from '@coursebuilder/core/schemas'
|
|
6
|
+
import { merchantCustomerSchema } from '@coursebuilder/core/schemas/merchant-customer-schema'
|
|
7
|
+
import type { PaymentsProviderConfig } from '@coursebuilder/core/types'
|
|
8
|
+
|
|
9
|
+
import type { MySqlContext } from '../context.js'
|
|
10
|
+
|
|
11
|
+
type TransferPurchaseToUserOperations = Pick<
|
|
12
|
+
PurchasePort,
|
|
13
|
+
'getPurchaseUserTransferById'
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Creates the private purchase transfer workflow.
|
|
18
|
+
*
|
|
19
|
+
* @param context - The shared MySQL client and table set.
|
|
20
|
+
* @param paymentProvider - The configured external payment provider.
|
|
21
|
+
* @param operations - Narrow purchase, merchant, and identity operations.
|
|
22
|
+
* @returns The purchase-owned user transfer operation.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const workflow = createTransferPurchaseToUserWorkflow(
|
|
27
|
+
* context,
|
|
28
|
+
* paymentProvider,
|
|
29
|
+
* operations,
|
|
30
|
+
* )
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function createTransferPurchaseToUserWorkflow(
|
|
34
|
+
{ client, tables }: MySqlContext,
|
|
35
|
+
paymentProvider: PaymentsProviderConfig | undefined,
|
|
36
|
+
operations: TransferPurchaseToUserOperations,
|
|
37
|
+
): Pick<PurchasePort, 'transferPurchaseToUser'> {
|
|
38
|
+
const {
|
|
39
|
+
users,
|
|
40
|
+
purchases: purchaseTable,
|
|
41
|
+
purchaseUserTransfer,
|
|
42
|
+
merchantCharge,
|
|
43
|
+
merchantCustomer,
|
|
44
|
+
} = tables
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
transferPurchaseToUser: async (options: {
|
|
48
|
+
purchaseId: string
|
|
49
|
+
targetUserId: string
|
|
50
|
+
sourceUserId: string
|
|
51
|
+
}) => {
|
|
52
|
+
const { purchaseId, targetUserId, sourceUserId } = options
|
|
53
|
+
const transferId = `put_${v4()}`
|
|
54
|
+
|
|
55
|
+
await client.transaction(async (trx) => {
|
|
56
|
+
const purchase = await trx
|
|
57
|
+
.select()
|
|
58
|
+
.from(purchaseTable)
|
|
59
|
+
.where(eq(purchaseTable.id, purchaseId))
|
|
60
|
+
.for('update')
|
|
61
|
+
.then((rows) => rows[0])
|
|
62
|
+
|
|
63
|
+
if (!purchase) throw new Error('No purchase found')
|
|
64
|
+
if (purchase.userId !== sourceUserId)
|
|
65
|
+
throw new Error('Invalid source user')
|
|
66
|
+
|
|
67
|
+
await trx
|
|
68
|
+
.update(purchaseTable)
|
|
69
|
+
.set({ userId: targetUserId })
|
|
70
|
+
.where(eq(purchaseTable.id, purchase.id))
|
|
71
|
+
|
|
72
|
+
await trx.insert(purchaseUserTransfer).values({
|
|
73
|
+
id: transferId,
|
|
74
|
+
purchaseId: purchase.id,
|
|
75
|
+
sourceUserId: sourceUserId,
|
|
76
|
+
targetUserId: targetUserId,
|
|
77
|
+
transferState: 'COMPLETED',
|
|
78
|
+
completedAt: new Date(),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (paymentProvider && purchase.merchantChargeId) {
|
|
82
|
+
await trx
|
|
83
|
+
.update(merchantCharge)
|
|
84
|
+
.set({
|
|
85
|
+
userId: targetUserId,
|
|
86
|
+
})
|
|
87
|
+
.where(eq(merchantCharge.id, purchase.merchantChargeId))
|
|
88
|
+
|
|
89
|
+
const updatedMerchantCharge = merchantChargeSchema.parse(
|
|
90
|
+
await trx.query.merchantCharge.findFirst({
|
|
91
|
+
where: eq(merchantCharge.id, purchase.merchantChargeId),
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
await trx
|
|
96
|
+
.update(merchantCustomer)
|
|
97
|
+
.set({ userId: targetUserId })
|
|
98
|
+
.where(
|
|
99
|
+
eq(merchantCustomer.id, updatedMerchantCharge.merchantCustomerId),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const updatedMerchantCustomer = merchantCustomerSchema.parse(
|
|
103
|
+
await trx.query.merchantCustomer.findFirst({
|
|
104
|
+
where: eq(
|
|
105
|
+
merchantCustomer.id,
|
|
106
|
+
updatedMerchantCharge.merchantCustomerId,
|
|
107
|
+
),
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const targetUser = userSchema.parse(
|
|
112
|
+
await trx.query.users.findFirst({
|
|
113
|
+
where: eq(users.id, targetUserId),
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const sourceUser = userSchema.parse(
|
|
118
|
+
await trx.query.users.findFirst({
|
|
119
|
+
where: eq(users.id, sourceUserId),
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
await paymentProvider.updateCustomer(
|
|
124
|
+
updatedMerchantCustomer.identifier,
|
|
125
|
+
{
|
|
126
|
+
email: targetUser.email,
|
|
127
|
+
name: targetUser.name || sourceUser.name || '',
|
|
128
|
+
metadata: {
|
|
129
|
+
transferId,
|
|
130
|
+
transferredFrom: sourceUser.email,
|
|
131
|
+
transferredOn: new Date().toISOString(),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
return await operations.getPurchaseUserTransferById({
|
|
138
|
+
id: transferId,
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|