UNPKG

@coursebuilder/core

Version:

Core package for Course Builder

1 lines 369 kB
{"version":3,"sources":["../../src/inngest/create-inngest-middleware.ts","../../src/adapters.ts","../../src/providers/index.ts","../../src/providers/openai.ts","../../src/providers/partykit.ts","../../src/providers/slack.ts","../../src/lib/pricing/stripe-subscription-utils.ts","../../../../node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.mjs","../../src/schemas/stripe/checkout-session-metadata.ts","../../src/schemas/subscription-info.ts","../../src/providers/stripe.ts","../../src/lib/pricing/stripe-checkout.ts","../../src/lib/pricing/format-prices-for-product.ts","../../src/lib/pricing/determine-coupon-to-apply.ts","../../src/schemas/purchase-type.ts","../../src/schemas/purchase-info.ts","../../src/lib/pricing/stripe-purchase-utils.ts"],"sourcesContent":["import { AuthConfig } from '@auth/core'\nimport { NodemailerConfig } from '@auth/core/providers/nodemailer'\nimport {\n\tEventSchemas,\n\tGetEvents,\n\tGetFunctionInput,\n\tHandler,\n\tInngest,\n\tInngestFunction,\n\tInngestMiddleware,\n} from 'inngest'\n\nimport {\n\tMockCourseBuilderAdapter,\n\ttype CourseBuilderAdapter,\n} from '../adapters'\nimport {\n\tMockTranscriptionProvider,\n\ttype TranscriptionConfig,\n} from '../providers'\nimport { LlmProviderConfig, MockOpenAIProvider } from '../providers/openai'\nimport {\n\tMockPartykitProvider,\n\tPartyProviderConfig,\n} from '../providers/partykit'\nimport {\n\tmockSlackProvider,\n\tNotificationProviderConfig,\n} from '../providers/slack'\nimport { MockStripeProvider } from '../providers/stripe'\nimport { PaymentsProviderConfig } from '../types'\nimport { CourseBuilderCoreEvents } from './index'\n\nexport interface CoreInngestContext {\n\tdb: CourseBuilderAdapter\n\tsiteRootUrl: string\n\ttranscriptProvider: TranscriptionConfig\n\topenaiProvider: LlmProviderConfig\n\tpartyProvider: PartyProviderConfig\n\tpaymentProvider?: PaymentsProviderConfig\n\temailProvider?: NodemailerConfig\n\tnotificationProvider?: NotificationProviderConfig\n\tgetAuthConfig: () => AuthConfig\n\tmediaUploadProvider: {\n\t\tdeleteFiles: (fileKey: string) => Promise<{ success: boolean }>\n\t}\n}\n\nexport type CoreInngestFunctionInput = GetFunctionInput<CoreInngest>\nexport type CoreInngestTrigger = InngestFunction.Trigger<\n\tkeyof GetEvents<CoreInngest>\n>\nexport type CoreInngestHandler = Handler.Any\n\nexport const createInngestMiddleware = <\n\tTCourseBuilderContext extends CoreInngestContext = CoreInngestContext,\n>(\n\tcontext: TCourseBuilderContext,\n) => {\n\treturn new InngestMiddleware({\n\t\tname: 'Course Builder Middleware',\n\t\tinit() {\n\t\t\treturn {\n\t\t\t\tonFunctionRun() {\n\t\t\t\t\treturn {\n\t\t\t\t\t\ttransformInput() {\n\t\t\t\t\t\t\t// kill me - this middleware is just for types\n\t\t\t\t\t\t\ttype MappedCoreContext = {\n\t\t\t\t\t\t\t\t[K in keyof TCourseBuilderContext]: TCourseBuilderContext[K]\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn { ctx: context as unknown as MappedCoreContext }\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t})\n}\n\nconst schemas = new EventSchemas().fromRecord<CourseBuilderCoreEvents>()\n\nexport const coreInngest = new Inngest({\n\tid: 'core-inngest',\n\tschemas,\n\tmiddleware: [\n\t\tcreateInngestMiddleware({\n\t\t\tdb: MockCourseBuilderAdapter,\n\t\t\tsiteRootUrl: '',\n\t\t\tpartyKitRootUrl: '',\n\t\t\ttranscriptProvider: MockTranscriptionProvider,\n\t\t\topenaiProvider: MockOpenAIProvider,\n\t\t\tpartyProvider: MockPartykitProvider,\n\t\t\tpaymentProvider: MockStripeProvider,\n\t\t\temailProvider: {} as NodemailerConfig,\n\t\t\tnotificationProvider: mockSlackProvider,\n\t\t\tgetAuthConfig: () => ({}) as AuthConfig,\n\t\t\tmediaUploadProvider: {\n\t\t\t\tdeleteFiles: async (_) => Promise.resolve({ success: true }),\n\t\t\t},\n\t\t}),\n\t],\n})\n\nexport type CoreInngest = typeof coreInngest\n","import { type Adapter } from '@auth/core/adapters'\n\nimport {\n\tContentResourceResource,\n\tMerchantCharge,\n\tNewProduct,\n\tUpgradableProduct,\n} from './schemas'\nimport { type ContentResource } from './schemas/content-resource-schema'\nimport { Coupon } from './schemas/coupon-schema'\nimport { MerchantAccount } from './schemas/merchant-account-schema'\nimport { MerchantCoupon } from './schemas/merchant-coupon-schema'\nimport { MerchantCustomer } from './schemas/merchant-customer-schema'\nimport { MerchantPrice } from './schemas/merchant-price-schema'\nimport { MerchantProduct } from './schemas/merchant-product-schema'\nimport { MerchantSession } from './schemas/merchant-session'\nimport { MerchantSubscription } from './schemas/merchant-subscription'\nimport { OrganizationMember } from './schemas/organization-member'\nimport { Organization } from './schemas/organization-schema'\nimport { Price } from './schemas/price-schema'\nimport { Product } from './schemas/product-schema'\nimport { Purchase } from './schemas/purchase-schema'\nimport {\n\tPurchaseUserTransfer,\n\tPurchaseUserTransferState,\n} from './schemas/purchase-user-transfer-schema'\nimport {\n\tModuleProgress,\n\tResourceProgress,\n} from './schemas/resource-progress-schema'\nimport { Subscription } from './schemas/subscription'\nimport { User } from './schemas/user-schema'\nimport { VideoResource } from './schemas/video-resource'\nimport { type Awaitable } from './types'\n\nexport interface CourseBuilderAdapter<\n\tTDatabaseInstance extends abstract new (...args: any) => any = any,\n> extends Adapter,\n\t\tSkillProductsCommerceSdk {\n\tclient: InstanceType<TDatabaseInstance>\n\tarchiveProduct(productId: string): Promise<Product | null>\n\tcreateProduct(product: NewProduct): Promise<Product | null>\n\tupdateProduct(product: Product): Promise<Product | null>\n\tcreatePurchase(options: {\n\t\tid?: string\n\t\tuserId: string\n\t\tproductId: string\n\t\tmerchantChargeId?: string\n\t\tmerchantSessionId?: string\n\t\ttotalAmount: string\n\t\tcouponId?: string | null\n\t\tredeemedBulkCouponId?: string | null\n\t\tstatus?: string\n\t\tmetadata?: Record<string, any>\n\t\torganizationId?: string\n\t}): Promise<Purchase>\n\taddResourceToResource(options: {\n\t\tchildResourceId: string\n\t\tparentResourceId: string\n\t}): Awaitable<ContentResourceResource | null>\n\tremoveResourceFromResource(options: {\n\t\tchildResourceId: string\n\t\tparentResourceId: string\n\t}): Promise<ContentResource | null>\n\tcreateContentResource(resource: {\n\t\tid: string\n\t\ttype: string\n\t\tfields: Record<string, any>\n\t\tcreatedById: string\n\t}): Awaitable<ContentResource>\n\tgetContentResource(id: string): Promise<ContentResource | null>\n\tgetVideoResource(id: string | null | undefined): Promise<VideoResource | null>\n\tupdateContentResourceFields(options: {\n\t\tid: string\n\t\tfields: Record<string, any>\n\t}): Awaitable<ContentResource | null>\n\tgetPriceForProduct(productId: string): Promise<Price | null>\n\tgetUpgradableProducts(options: {\n\t\tupgradableFromId: string\n\t\tupgradableToId: string\n\t}): Promise<UpgradableProduct[]>\n\tgetMerchantCustomerForUserId(userId: string): Promise<MerchantCustomer | null>\n\tgetMerchantAccount(options: {\n\t\tprovider: 'stripe'\n\t}): Promise<MerchantAccount | null>\n\tcreateMerchantSession(options: {\n\t\tidentifier: string\n\t\tmerchantAccountId: string\n\t\torganizationId?: string\n\t}): Promise<MerchantSession>\n\tcreateMerchantCustomer(options: {\n\t\tuserId: string\n\t\tidentifier: string\n\t\tmerchantAccountId: string\n\t}): Promise<MerchantCustomer | null>\n\tgetMerchantPriceForProductId(productId: string): Promise<MerchantPrice | null>\n\ttransferPurchaseToUser(options: {\n\t\tpurchaseId: string\n\t\ttargetUserId: string\n\t\tsourceUserId: string\n\t}): Promise<PurchaseUserTransfer | null>\n\tcreateOrganization(options: { name: string }): Promise<Organization | null>\n\tgetOrganization(organizationId: string): Promise<Organization | null>\n\taddMemberToOrganization(options: {\n\t\torganizationId: string\n\t\tuserId: string\n\t\tinvitedById: string\n\t}): Promise<OrganizationMember | null>\n\tremoveMemberFromOrganization(options: {\n\t\torganizationId: string\n\t\tuserId: string\n\t}): Promise<void>\n\taddRoleForMember(options: {\n\t\torganizationId: string\n\t\tmemberId: string\n\t\trole: string\n\t}): Promise<void>\n\tremoveRoleForMember(options: {\n\t\torganizationId: string\n\t\tmemberId: string\n\t\trole: string\n\t}): Promise<void>\n\tgetMembershipsForUser(userId: string): Promise<OrganizationMember[]>\n\tgetOrganizationMembers(organizationId: string): Promise<OrganizationMember[]>\n\tgetMerchantSubscription(\n\t\tmerchantSubscriptionId: string,\n\t): Promise<MerchantSubscription | null>\n\tcreateMerchantSubscription(options: {\n\t\tmerchantAccountId: string\n\t\tmerchantCustomerId: string\n\t\tmerchantProductId: string\n\t\tidentifier: string\n\t}): Promise<MerchantSubscription | null>\n\tupdateMerchantSubscription(options: {\n\t\tmerchantSubscriptionId: string\n\t\tstatus: string\n\t}): Promise<MerchantSubscription | null>\n\tdeleteMerchantSubscription(merchantSubscriptionId: string): Promise<void>\n\tcreateSubscription(options: {\n\t\torganizationId: string\n\t\tmerchantSubscriptionId: string\n\t\tproductId: string\n\t}): Promise<Subscription | null>\n\tgetSubscriptionForStripeId(\n\t\tstripeSubscriptionId: string,\n\t): Promise<Subscription | null>\n\tgetPurchasesForBulkCouponId(\n\t\tbulkCouponId: string,\n\t): Promise<(Purchase & { user: User })[]>\n}\n\nexport const MockCourseBuilderAdapter: CourseBuilderAdapter = {\n\tclient: null,\n\ttransferPurchaseToUser: async () => {\n\t\treturn null\n\t},\n\tredeemFullPriceCoupon: async () => {\n\t\treturn {} as any\n\t},\n\tarchiveProduct: async (productId) => {\n\t\treturn {} as any\n\t},\n\tupdateProduct: async (product) => {\n\t\treturn {} as any\n\t},\n\tcreateProduct: async (product) => {\n\t\treturn {} as any\n\t},\n\tcreatePurchaseTransfer: async () => {\n\t\treturn Promise.resolve()\n\t},\n\tincrementCouponUsedCount(_) {\n\t\treturn Promise.resolve()\n\t},\n\tgetExistingNonBulkValidPurchasesOfProduct: async () => {\n\t\treturn []\n\t},\n\tgetMerchantPriceForProductId: async (productId) => null,\n\tgetMerchantProductForProductId: async (productId) => null,\n\tgetMerchantAccount: async () => null,\n\tcreateMerchantCustomer: async () => null,\n\tgetMerchantCustomerForUserId: async () => null,\n\tgetUpgradableProducts: async () => [],\n\tcreatePurchase: async (options) => {\n\t\tthrow new Error('Method not implemented.')\n\t},\n\tavailableUpgradesForProduct(\n\t\tpurchases: any,\n\t\tproductId: string,\n\t): Promise<\n\t\t{\n\t\t\tupgradableTo: { id: string; name: string }\n\t\t\tupgradableFrom: { id: string; name: string }\n\t\t}[]\n\t> {\n\t\treturn Promise.resolve([])\n\t},\n\tclearLessonProgressForUser(options: {\n\t\tuserId: string\n\t\tlessons: { id: string; slug: string }[]\n\t}): Promise<void> {\n\t\treturn Promise.resolve(undefined)\n\t},\n\tcompleteLessonProgressForUser(options: {\n\t\tuserId: string\n\t\tlessonId?: string\n\t}): Promise<ResourceProgress | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tcouponForIdOrCode(options: {\n\t\tcode?: string\n\t\tcouponId?: string\n\t}): Promise<(Coupon & { merchantCoupon: MerchantCoupon }) | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tcreateMerchantChargeAndPurchase(options: {\n\t\tuserId: string\n\t\tproductId: string\n\t\tstripeChargeId: string\n\t\tstripeCouponId?: string\n\t\tmerchantAccountId: string\n\t\tmerchantProductId: string\n\t\tmerchantCustomerId: string\n\t\tstripeChargeAmount: number\n\t\tquantity?: number\n\t\tbulk?: boolean\n\t\tcheckoutSessionId: string\n\t\tappliedPPPStripeCouponId?: string\n\t\tupgradedFromPurchaseId?: string\n\t\tusedCouponId?: string\n\t\tcountry?: string\n\t\torganizationId?: string\n\t}): Promise<Purchase> {\n\t\tthrow new Error('Method not implemented.')\n\t},\n\tfindOrCreateMerchantCustomer(options: {\n\t\tuser: User\n\t\tidentifier: string\n\t\tmerchantAccountId: string\n\t}): Promise<MerchantCustomer | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tfindOrCreateUser(\n\t\temail: string,\n\t\tname?: string | null,\n\t): Promise<{\n\t\tuser: User\n\t\tisNewUser: boolean\n\t}> {\n\t\tthrow new Error('Method not implemented.')\n\t},\n\tgetCoupon(couponIdOrCode: string): Promise<Coupon | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetCouponWithBulkPurchases(couponId: string): Promise<\n\t\t| (Coupon & {\n\t\t\t\tredeemedBulkCouponPurchases: Purchase[]\n\t\t\t\tbulkPurchase?: Purchase | null\n\t\t })\n\t\t| null\n\t> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetDefaultCoupon(productIds?: string[]): Promise<{\n\t\tdefaultMerchantCoupon: MerchantCoupon\n\t\tdefaultCoupon: Coupon\n\t} | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetLessonProgressCountsByDate(): Promise<\n\t\t{\n\t\t\tcount: number\n\t\t\tcompletedAt: string\n\t\t}[]\n\t> {\n\t\treturn Promise.resolve([])\n\t},\n\tgetLessonProgressForUser(userId: string): Promise<ResourceProgress[]> {\n\t\treturn Promise.resolve([])\n\t},\n\tgetModuleProgressForUser(\n\t\tuserId: string,\n\t\tmoduleId: string,\n\t): Promise<ModuleProgress> {\n\t\treturn Promise.resolve({\n\t\t\tcompletedLessons: [],\n\t\t\tnextResource: null,\n\t\t\tpercentCompleted: 0,\n\t\t\tcompletedLessonsCount: 0,\n\t\t\ttotalLessonsCount: 0,\n\t\t})\n\t},\n\tgetLessonProgresses(): Promise<ResourceProgress[]> {\n\t\treturn Promise.resolve([])\n\t},\n\tgetMerchantCharge(merchantChargeId: string): Promise<MerchantCharge | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetMerchantCouponForTypeAndPercent(params: {\n\t\ttype: string\n\t\tpercentageDiscount: number\n\t}): Promise<MerchantCoupon | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetMerchantCouponsForTypeAndPercent(params: {\n\t\ttype: string\n\t\tpercentageDiscount: number\n\t}): Promise<MerchantCoupon[]> {\n\t\treturn Promise.resolve([])\n\t},\n\tgetMerchantCoupon(merchantCouponId: string): Promise<MerchantCoupon | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetMerchantProduct(stripeProductId: string): Promise<MerchantProduct | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetPrice(productId: string): Promise<Price | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetPriceForProduct(productId: string): Promise<Price | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetProduct(\n\t\tproductId?: string,\n\t\twithResources: boolean = true,\n\t): Promise<Product | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetProductResources(productId: string): Promise<ContentResource[] | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetPurchaseCountForProduct(productId: string): Promise<number> {\n\t\treturn Promise.resolve(0)\n\t},\n\tgetPurchase(purchaseId: string): Promise<Purchase | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetPurchaseDetails(\n\t\tpurchaseId: string,\n\t\tuserId: string,\n\t): Promise<{\n\t\tpurchase?: Purchase\n\t\texistingPurchase?: Purchase | null\n\t\tavailableUpgrades: UpgradableProduct[]\n\t}> {\n\t\treturn Promise.resolve({ availableUpgrades: [] })\n\t},\n\tgetPurchaseForStripeCharge(stripeChargeId: string): Promise<Purchase | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetPurchaseUserTransferById(options: { id: string }): Promise<\n\t\t| (PurchaseUserTransfer & {\n\t\t\t\tsourceUser: User\n\t\t\t\ttargetUser: User | null\n\t\t\t\tpurchase: Purchase\n\t\t })\n\t\t| null\n\t> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetPurchaseWithUser(purchaseId: string): Promise<\n\t\t| (Purchase & {\n\t\t\t\tuser: User\n\t\t })\n\t\t| null\n\t> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetUserWithPurchasersByEmail(email: string): Promise<any | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tgetPurchasesForUser(userId?: string): Promise<Purchase[]> {\n\t\treturn Promise.resolve([])\n\t},\n\tgetUserById(userId: string): Promise<User | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\tpricesOfPurchasesTowardOneBundle(options: {\n\t\tuserId: string | undefined\n\t\tbundleId: string\n\t}): Promise<Price[]> {\n\t\treturn Promise.resolve([])\n\t},\n\ttoggleLessonProgressForUser(options: {\n\t\tuserId: string\n\t\tlessonId?: string\n\t\tlessonSlug?: string\n\t}): Promise<ResourceProgress | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\ttransferPurchasesToNewUser(options: {\n\t\tmerchantCustomerId: string\n\t\tuserId: string\n\t}): Promise<unknown> {\n\t\treturn Promise.resolve(undefined)\n\t},\n\tupdatePurchaseStatusForCharge(\n\t\tchargeId: string,\n\t\tstatus: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',\n\t): Promise<Purchase | undefined> {\n\t\treturn Promise.resolve(undefined)\n\t},\n\tupdatePurchaseUserTransferTransferState(options: {\n\t\tid: string\n\t\ttransferState: PurchaseUserTransferState\n\t}): Promise<PurchaseUserTransfer | null> {\n\t\treturn Promise.resolve(null)\n\t},\n\taddResourceToResource: async (options) => {\n\t\treturn {} as ContentResourceResource\n\t},\n\tremoveResourceFromResource: async (options) => {\n\t\treturn null\n\t},\n\tcreateContentResource: async (resource) => {\n\t\treturn resource as ContentResource\n\t},\n\tgetContentResource: async (_) => {\n\t\treturn null\n\t},\n\tgetVideoResource: async (_) => {\n\t\treturn null\n\t},\n\tupdateContentResourceFields(_) {\n\t\treturn null\n\t},\n\tcreateOrganization: async () => null,\n\tgetOrganization: async () => null,\n\taddMemberToOrganization: async () => null,\n\tremoveMemberFromOrganization: async () => undefined,\n\taddRoleForMember: async () => undefined,\n\tremoveRoleForMember: async () => undefined,\n\tgetMembershipsForUser: async () => [],\n\tgetOrganizationMembers: async () => [],\n\tgetMerchantSubscription: async () => null,\n\tcreateMerchantSubscription: async () => null,\n\tupdateMerchantSubscription: async () => null,\n\tdeleteMerchantSubscription: async () => undefined,\n\tcreateMerchantSession: async () => Promise.resolve({} as MerchantSession),\n\tcreateSubscription: async () => null,\n\tgetSubscriptionForStripeId: async () => null,\n\tgetPurchasesForBulkCouponId: async () => [],\n}\n\ninterface SkillProductsCommerceSdk {\n\tredeemFullPriceCoupon(options: {\n\t\temail: string\n\t\tcouponId?: string\n\t\tredeemingProductId?: string\n\t\tproductIds?: string[]\n\t\tcurrentUserId?: string | null\n\t}): Promise<{\n\t\tpurchase: Purchase | null\n\t\tredeemingForCurrentUser: boolean\n\t\terror?: {\n\t\t\tmessage: string\n\t\t}\n\t} | null>\n\tcreatePurchaseTransfer(options: {\n\t\tsourceUserId: string\n\t\tpurchaseId: string\n\t\texpiresAt: Date\n\t}): Promise<void>\n\tincrementCouponUsedCount(couponId: string): Promise<void>\n\tgetExistingNonBulkValidPurchasesOfProduct(options: {\n\t\tuserId: string\n\t\tproductId?: string\n\t}): Promise<Purchase[]>\n\tgetPurchaseDetails(\n\t\tpurchaseId: string,\n\t\tuserId: string,\n\t): Promise<{\n\t\tpurchase?: Purchase\n\t\texistingPurchase?: Purchase | null\n\t\tavailableUpgrades: UpgradableProduct[]\n\t}>\n\tgetPurchaseForStripeCharge(stripeChargeId: string): Promise<Purchase | null>\n\tupdatePurchaseStatusForCharge(\n\t\tchargeId: string,\n\t\tstatus: 'Valid' | 'Refunded' | 'Disputed' | 'Banned' | 'Restricted',\n\t): Promise<Purchase | undefined>\n\tcouponForIdOrCode(options: {\n\t\tcode?: string | null | undefined\n\t\tcouponId?: string | null | undefined\n\t}): Promise<(Coupon & { merchantCoupon: MerchantCoupon }) | null>\n\tavailableUpgradesForProduct(\n\t\tpurchases: any,\n\t\tproductId: string,\n\t): Promise<\n\t\t{\n\t\t\tupgradableTo: {\n\t\t\t\tid: string\n\t\t\t\tname: string\n\t\t\t}\n\t\t\tupgradableFrom: {\n\t\t\t\tid: string\n\t\t\t\tname: string\n\t\t\t}\n\t\t}[]\n\t>\n\tcompleteLessonProgressForUser(options: {\n\t\tuserId: string\n\t\tlessonId?: string\n\t}): Promise<ResourceProgress | null>\n\tclearLessonProgressForUser(options: {\n\t\tuserId: string\n\t\tlessons: { id: string; slug: string }[]\n\t}): Promise<void>\n\ttoggleLessonProgressForUser(options: {\n\t\tuserId: string\n\t\tlessonId?: string\n\t\tlessonSlug?: string\n\t}): Promise<ResourceProgress | null>\n\tgetLessonProgressForUser(userId: string): Promise<ResourceProgress[] | null>\n\tgetModuleProgressForUser(\n\t\tuserId: string,\n\t\tmoduleId: string,\n\t): Promise<ModuleProgress | null>\n\tgetLessonProgresses(): Promise<ResourceProgress[]>\n\tgetLessonProgressCountsByDate(): Promise<\n\t\t{\n\t\t\tcount: number\n\t\t\tcompletedAt: string\n\t\t}[]\n\t>\n\tgetPurchaseWithUser(\n\t\tpurchaseId: string,\n\t): Promise<(Purchase & { user: User }) | null>\n\tgetCouponWithBulkPurchases(couponId?: string): Promise<\n\t\t| (Coupon & {\n\t\t\t\tredeemedBulkCouponPurchases?: Purchase[] | null\n\t\t\t\tbulkPurchase?: Purchase | null\n\t\t })\n\t\t| null\n\t>\n\tgetPurchase(purchaseId: string): Promise<Purchase | null>\n\tgetPurchaseCountForProduct(productId: string): Promise<number>\n\tgetPurchasesForUser(userId?: string): Promise<Purchase[]>\n\tgetMerchantProduct(stripeProductId: string): Promise<MerchantProduct | null>\n\tgetMerchantProductForProductId(\n\t\tproductId: string,\n\t): Promise<MerchantProduct | null>\n\tgetMerchantCharge(merchantChargeId: string): Promise<MerchantCharge | null>\n\tcreateMerchantChargeAndPurchase(options: {\n\t\tuserId: string\n\t\tproductId: string\n\t\tstripeChargeId: string\n\t\tstripeCouponId?: string\n\t\tmerchantAccountId: string\n\t\tmerchantProductId: string\n\t\tmerchantCustomerId: string\n\t\tstripeChargeAmount: number\n\t\tquantity?: number\n\t\tbulk?: boolean\n\t\tcheckoutSessionId: string\n\t\tappliedPPPStripeCouponId: string | undefined\n\t\tupgradedFromPurchaseId: string | undefined\n\t\tusedCouponId: string | undefined\n\t\tcountry?: string\n\t\torganizationId?: string\n\t}): Promise<Purchase>\n\tfindOrCreateMerchantCustomer(options: {\n\t\tuser: User\n\t\tidentifier: string\n\t\tmerchantAccountId: string\n\t}): Promise<MerchantCustomer | null>\n\tfindOrCreateUser(\n\t\temail: string,\n\t\tname?: string | null,\n\t): Promise<{ user: User; isNewUser: boolean }>\n\tgetUserById(userId: string): Promise<User | null>\n\tgetUserWithPurchasersByEmail(email: string): Promise<any | null>\n\tgetProduct(\n\t\tproductId?: string,\n\t\twithResources?: boolean,\n\t): Promise<Product | null>\n\tgetProductResources(productId: string): Promise<ContentResource[] | null>\n\tgetPrice(productId: string): Promise<Price | null>\n\tgetMerchantCoupon(merchantCouponId: string): Promise<MerchantCoupon | null>\n\tgetMerchantCouponForTypeAndPercent(params: {\n\t\ttype: string\n\t\tpercentageDiscount: number\n\t}): Promise<MerchantCoupon | null>\n\tgetMerchantCouponsForTypeAndPercent(params: {\n\t\ttype: string\n\t\tpercentageDiscount: number\n\t}): Promise<MerchantCoupon[]>\n\tgetCoupon(couponIdOrCode: string): Promise<Coupon | null>\n\tgetDefaultCoupon(productIds?: string[]): Promise<{\n\t\tdefaultMerchantCoupon: MerchantCoupon\n\t\tdefaultCoupon: Coupon\n\t} | null>\n\ttransferPurchasesToNewUser(options: {\n\t\tmerchantCustomerId: string\n\t\tuserId: string\n\t}): Promise<unknown>\n\tgetPurchaseUserTransferById(options: { id: string }): Promise<\n\t\t| (PurchaseUserTransfer & {\n\t\t\t\tsourceUser: User\n\t\t\t\ttargetUser: User | null\n\t\t\t\tpurchase: Purchase\n\t\t })\n\t\t| null\n\t>\n\tupdatePurchaseUserTransferTransferState(options: {\n\t\tid: string\n\t\ttransferState: PurchaseUserTransferState\n\t}): Promise<PurchaseUserTransfer | null>\n\tpricesOfPurchasesTowardOneBundle(options: {\n\t\tuserId: string | undefined\n\t\tbundleId: string\n\t}): Promise<Price[]>\n}\n","import { AdapterUser } from '@auth/core/adapters'\nimport { EmailConfig } from '@auth/core/providers'\nimport { NodemailerConfig } from '@auth/core/providers/nodemailer'\n\nimport { Subscriber } from '../schemas/subscriber-schema'\nimport { CookieOption, PaymentsProviderConfig } from '../types'\nimport { LlmProviderConfig } from './openai'\nimport { PartyProviderConfig } from './partykit'\nimport { NotificationProviderConfig } from './slack'\n\nexport interface EmailListSubscribeOptions {\n\tlistId?: string | number\n\tuser: AdapterUser\n\tfields: Record<string, string>\n\tlistType: string\n}\n\nexport interface EmailListConfig {\n\tid: string\n\tname: string\n\ttype: string\n\toptions: EmailListConsumerConfig\n\tapiKey: string\n\tapiSecret: string\n\tdefaultListType: string\n\tdefaultListId?: number | string\n\tsubscribeToList: (options: EmailListSubscribeOptions) => Promise<any>\n\tgetSubscriber: (\n\t\tsubscriberId: string | null | CookieOption,\n\t) => Promise<Subscriber | null>\n\tgetSubscriberByEmail: (email: string) => Promise<Subscriber | null>\n\ttagSubscriber?: (options: {\n\t\ttag: string\n\t\tsubscriberId: string\n\t}) => Promise<any>\n\tupdateSubscriberFields?: (options: {\n\t\tfields: Record<string, string>\n\t\tsubscriberId?: string\n\t\tsubscriberEmail?: string\n\t}) => Promise<Subscriber | null>\n}\n\nexport type EmailListConsumerConfig = Omit<\n\tPartial<EmailListConfig>,\n\t'options' | 'type'\n> & {\n\tapiKey: string\n\tapiSecret: string\n}\n\n/**\n * The configuration object for a transcription service provider.\n */\nexport interface TranscriptionConfig {\n\tid: string\n\tname: string\n\ttype: string\n\toptions: TranscriptionUserConfig\n\tapiKey: string\n\tcallbackUrl: string\n\tgetCallbackUrl?: (options: {\n\t\tbaseUrl: string\n\t\tparams: Record<string, string>\n\t}) => string\n\tinitiateTranscription: (options: {\n\t\tmediaUrl: string\n\t\tresourceId: string\n\t}) => Promise<any>\n\thandleCallback: (callbackData: any) => {\n\t\tsrt: string\n\t\ttranscript: string\n\t\twordLevelSrt: string\n\t}\n}\n\nexport const MockTranscriptionProvider: TranscriptionConfig = {\n\tid: 'mock-transcription' as const,\n\tname: 'Mock Transcription',\n\ttype: 'transcription',\n\toptions: {\n\t\tapiKey: 'mock-api-key',\n\t\tcallbackUrl: 'mock-callback-url',\n\t},\n\tapiKey: 'mock-api-key',\n\tcallbackUrl: 'mock-callback-url',\n\tgetCallbackUrl: () => 'mock-callback-url',\n\tinitiateTranscription: async () => ({ mock: 'transcription' }),\n\thandleCallback: () => ({\n\t\tsrt: 'mock-srt',\n\t\ttranscript: 'mock-transcript',\n\t\twordLevelSrt: 'mock-word-level-srt',\n\t}),\n}\n\nexport type TranscriptionUserConfig = Omit<\n\tPartial<TranscriptionConfig>,\n\t'options' | 'type'\n> & {\n\tapiKey: string\n\tcallbackUrl: string\n}\n/**\n * The user configuration object for a transcription service provider.\n */\nexport type ProviderType =\n\t| 'transcription'\n\t| 'email-list'\n\t| 'payment'\n\t| 'party'\n\t| 'checkout'\n\t| 'email'\n\t| 'notification'\n\ninterface InternalProviderOptions {\n\t/** Used to deep merge user-provided config with the default config\n\t */\n\toptions?: Record<string, any>\n}\n\nexport interface CommonProviderOptions {\n\t/**\n\t * Uniquely identifies the provider in {@link CourseBuilderConfig.providers}.\n\t * It's also part of the URL\n\t */\n\tid: string\n\t/**\n\t * The provider name used on the default sign-in page's sign-in button.\n\t * For example if it's \"Google\", the corresponding button will say:\n\t * \"Sign in with Google\"\n\t */\n\tname: string\n\t/** See {@link ProviderType} */\n\ttype: ProviderType\n}\n\nexport type Provider<P = any> = (\n\t| ((\n\t\t\t| TranscriptionConfig\n\t\t\t| EmailListConfig\n\t\t\t| LlmProviderConfig\n\t\t\t| PartyProviderConfig\n\t\t\t| PaymentsProviderConfig\n\t\t\t| EmailConfig\n\t\t\t| NodemailerConfig\n\t\t\t| NotificationProviderConfig\n\t ) &\n\t\t\tInternalProviderOptions)\n\t| ((\n\t\t\t...args: any\n\t ) => (\n\t\t\t| TranscriptionConfig\n\t\t\t| EmailListConfig\n\t\t\t| LlmProviderConfig\n\t\t\t| PartyProviderConfig\n\t\t\t| PaymentsProviderConfig\n\t\t\t| EmailConfig\n\t\t\t| NodemailerConfig\n\t\t\t| NotificationProviderConfig\n\t ) &\n\t\t\tInternalProviderOptions)\n) &\n\tInternalProviderOptions\n","import { createOpenAI } from '@ai-sdk/openai'\nimport { streamText, type CoreMessage } from 'ai'\n\nimport { AIOutput } from '../types'\n\nexport const STREAM_COMPLETE = `\\\\ok`\n\n/**\n * PartyKit chunk publisher that buffers and sends chunks at intervals\n * to maintain the expected streaming behavior for the UI\n */\nclass PartyKitChunkPublisher {\n\trequestId: string\n\tinterval = 250\n\tbuffer: {\n\t\tcontents: string\n\t\tsignal?: Promise<unknown>\n\t}\n\tpartyUrl: string\n\n\tconstructor(requestId: string, partyUrlBase: string) {\n\t\tthis.requestId = requestId\n\t\tthis.buffer = {\n\t\t\tcontents: '',\n\t\t}\n\t\tthis.partyUrl = `${partyUrlBase}/party/${requestId}`\n\t}\n\n\tasync publishMessage(message: string) {\n\t\tawait this.sendToPartyKit(message, this.requestId, this.partyUrl)\n\t}\n\n\tasync appendToBufferAndPublish(text: string) {\n\t\tlet resolve = (_val?: any) => {}\n\t\tthis.buffer.contents += text\n\n\t\tif (this.buffer.signal) {\n\t\t\t// Already enqueued.\n\t\t\treturn\n\t\t}\n\n\t\tthis.buffer.signal = new Promise((r) => {\n\t\t\tresolve = r\n\t\t})\n\n\t\tsetTimeout(() => {\n\t\t\tif (this.buffer.contents.length === 0) {\n\t\t\t\tresolve()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sendToPartyKit(this.buffer.contents, this.requestId, this.partyUrl)\n\t\t\tresolve()\n\t\t\tthis.buffer = {\n\t\t\t\tcontents: '',\n\t\t\t}\n\t\t}, this.interval)\n\t}\n\n\tasync waitForBuffer() {\n\t\tawait this.buffer.signal\n\t}\n\n\tprivate async sendToPartyKit(\n\t\tbody: string,\n\t\trequestId: string,\n\t\tpartyUrl: string,\n\t) {\n\t\treturn await fetch(partyUrl, {\n\t\t\tmethod: 'POST',\n\t\t\tbody: JSON.stringify({\n\t\t\t\tbody,\n\t\t\t\trequestId,\n\t\t\t\tname: 'ai.message',\n\t\t\t}),\n\t\t}).catch((e) => {\n\t\t\tconsole.error('Failed to send chunk to PartyKit:', e)\n\t\t})\n\t}\n}\n\nexport interface LlmProviderConfig {\n\tid: string\n\tname: string\n\ttype: string\n\toptions: LlmProviderConsumerConfig\n\tapiKey: string\n\tpartyUrlBase: string\n\tbaseUrl?: string\n\tdefaultModel?: string\n\tcreateChatCompletion: (\n\t\toptions: CreateChatCompletionOptions,\n\t) => Promise<AIOutput | null>\n}\n\nexport type LlmProviderConsumerConfig = Omit<\n\tPartial<LlmProviderConfig>,\n\t'options' | 'type'\n> & {\n\tapiKey: string\n\tpartyUrlBase: string\n\tbaseUrl?: string\n\tdefaultModel?: string\n}\n\nexport type CreateChatCompletionOptions = {\n\tmessages: CoreMessage[]\n\tchatId: string\n\tmodel: string\n}\n\nexport default function OpenAIProvider(\n\toptions: LlmProviderConsumerConfig,\n): LlmProviderConfig {\n\tconst client = createOpenAI({\n\t\tapiKey: options.apiKey,\n\t\t...(options.baseUrl && {\n\t\t\tbaseURL: options.baseUrl,\n\t\t}),\n\t})\n\n\treturn {\n\t\tid: 'openai',\n\t\tname: 'OpenAI',\n\t\ttype: 'llm',\n\t\toptions,\n\t\t...options,\n\t\tcreateChatCompletion: async (\n\t\t\tcreateChatOptions: CreateChatCompletionOptions,\n\t\t) => {\n\t\t\ttry {\n\t\t\t\tconst modelName =\n\t\t\t\t\tcreateChatOptions.model || options.defaultModel || 'gpt-4o'\n\n\t\t\t\t// Create PartyKit publisher with buffering behavior\n\t\t\t\tconst publisher = new PartyKitChunkPublisher(\n\t\t\t\t\tcreateChatOptions.chatId,\n\t\t\t\t\toptions.partyUrlBase,\n\t\t\t\t)\n\n\t\t\t\tconst result = await streamText({\n\t\t\t\t\tmodel: client(modelName),\n\t\t\t\t\tmessages: createChatOptions.messages,\n\t\t\t\t\tonChunk: async ({ chunk }) => {\n\t\t\t\t\t\tif (chunk.type === 'text-delta') {\n\t\t\t\t\t\t\t// Use the buffered publisher to maintain expected streaming behavior\n\t\t\t\t\t\t\tawait publisher.appendToBufferAndPublish(chunk.textDelta)\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\t// We need to consume the stream to make result.text resolve\n\t\t\t\t// Since we're already handling chunks in onChunk, we can consume textStream to completion\n\t\t\t\tlet fullText = ''\n\t\t\t\tfor await (const textPart of result.textStream) {\n\t\t\t\t\tfullText += textPart\n\t\t\t\t}\n\n\t\t\t\t// Wait for any remaining buffered content to be sent\n\t\t\t\tawait publisher.waitForBuffer()\n\n\t\t\t\t// Send completion signal using the expected format\n\t\t\t\tawait publisher.publishMessage(STREAM_COMPLETE)\n\n\t\t\t\treturn {\n\t\t\t\t\trole: 'assistant',\n\t\t\t\t\tcontent: fullText,\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error('OpenAI streaming error:', error)\n\t\t\t\tthrow error\n\t\t\t}\n\t\t},\n\t} as const\n}\n\nexport const MockOpenAIProvider: LlmProviderConfig = {\n\tid: 'mock-openai' as const,\n\tname: 'Mock OpenAI',\n\ttype: 'llm',\n\toptions: {\n\t\tapiKey: 'mock-api-key',\n\t\tpartyUrlBase: 'mock-callback-url',\n\t},\n\tapiKey: 'mock-api-key',\n\tpartyUrlBase: 'mock-callback-url',\n\tcreateChatCompletion: () => Promise.resolve(null),\n}\n","export interface PartyProviderConfig {\n\tid: string\n\tname: string\n\ttype: 'party'\n\toptions: PartyProviderConsumerConfig\n\tpartyUrlBase: string\n\tbroadcastMessage: (options: BroadcastMessageOptions) => Promise<string>\n}\n\nexport type PartyProviderConsumerConfig = Omit<\n\tPartial<PartyProviderConfig>,\n\t'options' | 'type'\n> & {\n\tpartyUrlBase: string\n}\n\nexport type BroadcastMessageOptions = {\n\tbody: Record<string, any>\n\troomId: string\n}\n\nexport default function PartykitProvider(\n\toptions: PartyProviderConsumerConfig,\n): PartyProviderConfig {\n\treturn {\n\t\tid: 'partykit',\n\t\tname: 'PartyKit',\n\t\ttype: 'party',\n\t\toptions,\n\t\t...options,\n\t\tbroadcastMessage: async (\n\t\t\tbroadcastMessageOptions: BroadcastMessageOptions,\n\t\t) => {\n\t\t\treturn await fetch(\n\t\t\t\t`${options.partyUrlBase}/party/${broadcastMessageOptions.roomId}`,\n\t\t\t\t{\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\tbody: JSON.stringify(broadcastMessageOptions.body),\n\t\t\t\t},\n\t\t\t)\n\t\t\t\t.then((res) => {\n\t\t\t\t\treturn res.text()\n\t\t\t\t})\n\t\t\t\t.catch((e) => {\n\t\t\t\t\tconsole.error(e)\n\t\t\t\t\tthrow e\n\t\t\t\t})\n\t\t},\n\t} as const\n}\n\nexport const MockPartykitProvider: PartyProviderConfig = {\n\tid: 'mock-partykit' as const,\n\tname: 'Mock Partykit',\n\ttype: 'party',\n\toptions: {\n\t\tpartyUrlBase: 'mock-callback-url',\n\t},\n\tpartyUrlBase: 'mock-callback-url',\n\tbroadcastMessage: () => Promise.resolve(''),\n}\n","export interface ChatPostMessageResponse {\n\tok: boolean\n\tchannel: string\n\tts: string\n\tmessage: {\n\t\ttext: string\n\t\tusername: string\n\t\tbot_id?: string\n\t\tattachments?: MessageAttachment[]\n\t\ttype: string\n\t\tsubtype?: string\n\t}\n}\n\nexport interface MessageAttachment {\n\tfallback?: string\n\tcolor?: string\n\tpretext?: string\n\tauthor_name?: string\n\tauthor_link?: string\n\tauthor_icon?: string\n\tmrkdwn_in?: string[]\n\ttitle?: string\n\ttitle_link?: string\n\ttext?: string\n\tfields?: {\n\t\ttitle: string\n\t\tvalue: string\n\t\tshort?: boolean\n\t}[]\n\timage_url?: string\n\tthumb_url?: string\n\tfooter?: string\n\tfooter_icon?: string\n\tts?: number\n}\n\nexport interface PostEphemeralResponse {\n\tok: boolean\n\tmessage_ts: string\n}\n\nexport interface PostEphemeralOptions {\n\tchannel?: string\n\tuser: string\n\ttext?: string\n\ticon_emoji?: string\n\tusername?: string\n\tas_user?: boolean\n\tthread_ts?: string\n\tattachments?: MessageAttachment[]\n}\n\nexport interface NotificationProviderConfig {\n\tid: string\n\tname: string\n\ttype: 'notification'\n\toptions: NotificationProviderConsumerConfig\n\tapiKey?: string\n\tdefaultChannelId?: string\n\tsendNotification: (\n\t\toptions: NotificationOptions,\n\t) => Promise<ChatPostMessageResponse>\n\tcreateChannel: (\n\t\toptions: CreateChannelOptions,\n\t) => Promise<CreateChannelResponse>\n\tinviteToChannel: (\n\t\toptions: InviteToChannelOptions,\n\t) => Promise<InviteToChannelResponse>\n\tpostEphemeral: (\n\t\toptions: PostEphemeralOptions,\n\t) => Promise<PostEphemeralResponse>\n}\n\nexport type NotificationProviderConsumerConfig = Omit<\n\tPartial<NotificationProviderConfig>,\n\t'options' | 'type'\n> & {\n\tapiKey?: string\n\tdefaultChannelId?: string\n}\n\nexport type NotificationOptions = {\n\tattachments?: MessageAttachment[]\n\tchannel?: string\n\ticon_emoji?: string\n\tusername?: string\n\ttext?: string\n}\n\nexport interface CreateChannelResponse {\n\tok: boolean\n\tchannel: {\n\t\tid: string\n\t\tname: string\n\t\tis_channel: boolean\n\t\tcreated: number\n\t\tis_archived: boolean\n\t\tis_general: boolean\n\t\tunlinked: number\n\t\tcreator: string\n\t\tname_normalized: string\n\t\tis_shared: boolean\n\t\tis_org_shared: boolean\n\t\tis_member: boolean\n\t\tis_private: boolean\n\t\tis_mpim: boolean\n\t\tlast_read: string\n\t\tlatest: any\n\t\tunread_count: number\n\t\tunread_count_display: number\n\t\tmembers: string[]\n\t\ttopic: {\n\t\t\tvalue: string\n\t\t\tcreator: string\n\t\t\tlast_set: number\n\t\t}\n\t\tpurpose: {\n\t\t\tvalue: string\n\t\t\tcreator: string\n\t\t\tlast_set: number\n\t\t}\n\t}\n}\n\nexport interface CreateChannelOptions {\n\tname: string\n\tis_private?: boolean\n}\n\nexport interface InviteToChannelResponse {\n\tok: boolean\n\tchannel: {\n\t\tid: string\n\t\tname: string\n\t\tis_channel: boolean\n\t\tcreated: number\n\t\tcreator: string\n\t\tis_archived: boolean\n\t\tis_general: boolean\n\t\tname_normalized: string\n\t\tis_shared: boolean\n\t\tis_org_shared: boolean\n\t\tis_member: boolean\n\t\tis_private: boolean\n\t\tis_mpim: boolean\n\t\tis_read_only: boolean\n\t\tis_pending_ext_shared: boolean\n\t\tpending_shared: string[]\n\t\tunlinked: number\n\t}\n}\n\nexport interface InviteToChannelOptions {\n\t/**\n\t * The ID of the public or private channel to invite users to\n\t */\n\tchannel: string\n\t/**\n\t * A comma separated list of user IDs. Up to 1000 users may be listed.\n\t */\n\tusers: string | string[]\n\t/**\n\t * When set to true and multiple user IDs are provided, continue inviting the valid ones while disregarding invalid IDs\n\t */\n\tforce?: boolean\n}\n\nexport default function SlackProvider(\n\toptions: NotificationProviderConsumerConfig,\n): NotificationProviderConfig {\n\treturn {\n\t\tid: 'slack',\n\t\tname: 'Slack',\n\t\ttype: 'notification',\n\t\toptions,\n\t\t...options,\n\t\tsendNotification: async (sendOptions) => {\n\t\t\tif (!options.apiKey) {\n\t\t\t\tthrow new Error('No apiKey found for notification provider')\n\t\t\t}\n\t\t\tconst channel = sendOptions.channel || options.defaultChannelId\n\t\t\tif (!channel) {\n\t\t\t\tthrow new Error('No channel found')\n\t\t\t}\n\t\t\tconst {\n\t\t\t\tattachments,\n\t\t\t\ttext,\n\t\t\t\ticon_emoji = ':robot_face:',\n\t\t\t\tusername = 'Announce Bot',\n\t\t\t} = sendOptions\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch('https://slack.com/api/chat.postMessage', {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\tAuthorization: `Bearer ${options.apiKey}`,\n\t\t\t\t\t\t'Content-Type': 'application/json; charset=utf-8',\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\ticon_emoji,\n\t\t\t\t\t\tusername,\n\t\t\t\t\t\tattachments,\n\t\t\t\t\t\tchannel,\n\t\t\t\t\t\ttext,\n\t\t\t\t\t}),\n\t\t\t\t})\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Slack API error: ${response.statusText}`)\n\t\t\t\t}\n\n\t\t\t\treturn (await response.json()) as ChatPostMessageResponse\n\t\t\t} catch (e) {\n\t\t\t\tconsole.log(e)\n\t\t\t\treturn Promise.reject(e)\n\t\t\t}\n\t\t},\n\t\tcreateChannel: async (createOptions) => {\n\t\t\tif (!options.apiKey) {\n\t\t\t\tthrow new Error('No apiKey found for notification provider')\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\t'https://slack.com/api/conversations.create',\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\tAuthorization: `Bearer ${options.apiKey}`,\n\t\t\t\t\t\t\t'Content-Type': 'application/json; charset=utf-8',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: JSON.stringify(createOptions),\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Slack API error: ${response.statusText}`)\n\t\t\t\t}\n\n\t\t\t\tconst data = await response.json()\n\n\t\t\t\tif (!data.ok) {\n\t\t\t\t\tthrow new Error(`Slack API error: ${data.error}`)\n\t\t\t\t}\n\n\t\t\t\treturn data as CreateChannelResponse\n\t\t\t} catch (e) {\n\t\t\t\tconsole.log(e)\n\t\t\t\treturn Promise.reject(e)\n\t\t\t}\n\t\t},\n\t\tinviteToChannel: async (inviteOptions) => {\n\t\t\tif (!options.apiKey) {\n\t\t\t\tthrow new Error('No apiKey found for notification provider')\n\t\t\t}\n\n\t\t\tconst users = Array.isArray(inviteOptions.users)\n\t\t\t\t? inviteOptions.users.join(',')\n\t\t\t\t: inviteOptions.users\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\t'https://slack.com/api/conversations.invite',\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\tAuthorization: `Bearer ${options.apiKey}`,\n\t\t\t\t\t\t\t'Content-Type': 'application/json; charset=utf-8',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\tchannel: inviteOptions.channel,\n\t\t\t\t\t\t\tusers,\n\t\t\t\t\t\t\tforce: inviteOptions.force,\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Slack API error: ${response.statusText}`)\n\t\t\t\t}\n\n\t\t\t\tconst data = await response.json()\n\n\t\t\t\tif (!data.ok) {\n\t\t\t\t\tthrow new Error(`Slack API error: ${data.error}`)\n\t\t\t\t}\n\n\t\t\t\treturn data as InviteToChannelResponse\n\t\t\t} catch (e) {\n\t\t\t\tconsole.log(e)\n\t\t\t\treturn Promise.reject(e)\n\t\t\t}\n\t\t},\n\t\tpostEphemeral: async (ephemeralOptions) => {\n\t\t\tif (!options.apiKey) {\n\t\t\t\tthrow new Error('No apiKey found for notification provider')\n\t\t\t}\n\t\t\tconst channel = ephemeralOptions.channel || options.defaultChannelId\n\t\t\tif (!channel) {\n\t\t\t\tthrow new Error('No channel found')\n\t\t\t}\n\n\t\t\tconst {\n\t\t\t\ttext,\n\t\t\t\ticon_emoji = ':eggo:',\n\t\t\t\tusername = 'eggo',\n\t\t\t\tuser,\n\t\t\t\tas_user,\n\t\t\t\tthread_ts,\n\t\t\t\tattachments,\n\t\t\t} = ephemeralOptions\n\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(\n\t\t\t\t\t'https://slack.com/api/chat.postEphemeral',\n\t\t\t\t\t{\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\tAuthorization: `Bearer ${options.apiKey}`,\n\t\t\t\t\t\t\t'Content-Type': 'application/json; charset=utf-8',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\t\t\ticon_emoji,\n\t\t\t\t\t\t\tusername,\n\t\t\t\t\t\t\tchannel,\n\t\t\t\t\t\t\ttext,\n\t\t\t\t\t\t\tuser,\n\t\t\t\t\t\t\tas_user,\n\t\t\t\t\t\t\tthread_ts,\n\t\t\t\t\t\t\tattachments,\n\t\t\t\t\t\t}),\n\t\t\t\t\t},\n\t\t\t\t)\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Slack API error: ${response.statusText}`)\n\t\t\t\t}\n\n\t\t\t\tconst data = await response.json()\n\n\t\t\t\tif (!data.ok) {\n\t\t\t\t\tthrow new Error(`Slack API error: ${data.error}`)\n\t\t\t\t}\n\n\t\t\t\treturn data as PostEphemeralResponse\n\t\t\t} catch (e) {\n\t\t\t\tconsole.log(e)\n\t\t\t\treturn Promise.reject(e)\n\t\t\t}\n\t\t},\n\t} as const\n}\n\nexport const mockSlackProvider: NotificationProviderConfig = {\n\tid: 'mock-slack' as const,\n\tname: 'Mock Slack',\n\ttype: 'notification',\n\toptions: {\n\t\tapiKey: 'mock-api-key',\n\t},\n\tapiKey: 'mock-api-key',\n\tsendNotification: async () => Promise.resolve({} as ChatPostMessageResponse),\n\tcreateChannel: async () => Promise.resolve({} as CreateChannelResponse),\n\tinviteToChannel: async () => Promise.resolve({} as InviteToChannelResponse),\n\tpostEphemeral: async () => Promise.resolve({} as PostEphemeralResponse),\n}\n","import type Stripe from 'stripe'\n\nimport { first } from '@coursebuilder/nodash'\n\nimport {\n\tCheckoutSessionMetadata,\n\tCheckoutSessionMetadataSchema,\n} from '../../schemas/stripe/checkout-session-metadata'\nimport {\n\tSubscriptionInfo,\n\tSubscriptionInfoSchema,\n} from '../../schemas/subscription-info'\nimport { logger } from '../utils/logger'\n\nexport async function parseSubscriptionInfoFromCheckoutSession(\n\tcheckoutSession: Stripe.Checkout.Session,\n) {\n\tlogger.debug('Parsing subscription info from checkout session', {\n\t\tcheckoutSession,\n\t})\n\n\tconst { customer, subscription, metadata } = checkoutSession\n\tconst { email, name, id: stripeCustomerId } = customer as Stripe.Customer\n\tconst stripeSubscription = subscription as Stripe.Subscription\n\n\tconst subscriptionItem = first(stripeSubscription.items.data)\n\tif (!subscriptionItem) {\n\t\tlogger.error(new Error('No subscription item found in checkout session'))\n\t\tthrow new Error('No subscription item found')\n\t}\n\n\tconst stripePrice = subscriptionItem.price\n\tconst quantity = subscriptionItem.quantity || 1\n\tconst stripeProduct = (stripeSubscription as any).plan\n\t\t?.product as Stripe.Product\n\n\tlogger.debug('Found subscription details', {\n\t\tstripeCustomerId,\n\t\tsubscriptionId: stripeSubscription.id,\n\t\tproductId: stripeProduct.id,\n\t\tquantity,\n\t})\n\n\t// Add required fields to metadata before parsing\n\tconst enrichedMetadata = {\n\t\tbulk: quantity > 1 ? 'true' : 'false',\n\t\tcountry: 'US',\n\t\tip_address: '127.0.0.1',\n\t\tproductId: stripeProduct.id,\n\t\tproduct: stripeProduct.name,\n\t\tsiteName: 'default',\n\t\t...metadata,\n\t}\n\n\tconst parsedMetadata = enrichedMetadata\n\t\t? CheckoutSessionMetadataSchema.parse(enrichedMetadata)\n\t\t: undefined\n\n\tlogger.debug('Enriched metadata', { enrichedMetadata, parsedMetadata })\n\n\tconst info: SubscriptionInfo = {\n\t\tcustomerIdentifier: stripeCustomerId,\n\t\temail,\n\t\tname,\n\t\tproductIdentifier: stripeProduct.id,\n\t\tproduct: stripeProduct,\n\t\tsubscriptionIdentifier: stripeSubscription.id,\n\t\tpriceIdentifier: stripePrice.id,\n\t\tquantity,\n\t\tstatus: stripeSubscription.status,\n\t\tcurrentPeriodStart: new Date(\n\t\t\tstripeSubscription.current_period_start * 1000,\n\t\t),\n\t\tcurrentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000),\n\t\tmetadata: parsedMetadata,\n\t}\n\n\tconst parsedInfo = SubscriptionInfoSchema.parse(info)\n\tlogger.debug('Successfully parsed subscription info', { parsedInfo })\n\treturn parsedInfo\n}\n\nexport interface SubscriptionPermissions {\n\torganizationId: string\n\tpurchasingMemberId: string\n\tisMultiUser: boolean\n\tassignToMember?: string // only for single-user subscriptions\n}\n\nexport function determineSubscriptionPermissions(\n\tmetadata: CheckoutSessionMetadata,\n\torganizationId: string,\n\tpurchasingMemberId: string,\n): SubscriptionPermissions {\n\tlogger.debug('Determining subscription permissions', {\n\t\tmetadata,\n\t\torganizationId,\n\t\tpurchasingMemberId,\n\t})\n\n\tconst isMultiUser = metadata.bulk === 'true'\n\n\tconst permissions = {\n\t\torganizationId,\n\t\tpurchasingMemberId,\n\t\tisMultiUser,\n\t\tassignToMember: !isMultiUser ? purchasingMemberId : undefined,\n\t}\n\n\tlogger.debug('Determined subscription permissions', { permissions })\n\treturn permissions\n}\n","var util;\n(function (util) {\n util.assertEqual = (val) => val;\n function assertIs(_arg) { }\n util.assertIs = assertIs;\n function assertNever(_x) {\n throw new Error();\n }\n util.assertNever = assertNever;\n util.arrayToEnum = (items) => {\n const obj = {};\n for (const item of items) {\n obj[item] = item;\n }\n return obj;\n };\n util.getValidEnumValues = (obj) => {\n const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== \"number\");\n const filtered = {};\n for (const k of validKeys) {\n filtered[k] = obj[k];\n }\n return util.objectValues(filtered);\n };\n util.objectValues = (obj) => {\n return util.objectKeys(obj).map(function (e) {\n return obj[e];\n });\n };\n util.objectKeys = typeof Object.keys === \"function\" // eslint-disable-line ban/ban\n ? (obj) => Object.keys(obj) // eslint-disable-line ban/ban\n : (object) => {\n const keys = [];\n for (const key in object) {\n if (Object.prototype.hasOwnProperty.call(object, key)) {\n keys.push(key);\n }\n }\n return keys;\n };\n util.find = (arr, checker) => {\n for (const item of arr) {\n if (checker(item))\n return item;\n }\n return undefined;\n };\n util.isInteger = typeof Number.isInteger === \"function\"\n ? (val) => Number.isInteger(val) // eslint-disable-line ban/ban\n : (val) => typeof val === \"number\" && isFinite(val) && Math.floor(val) === val;\n function joinValues(array, separator = \" | \") {\n return array\n .map((val) => (typeof val === \"string\" ? `'${val}'` : val))\n .join(separator);\n }\n util.joinValues = joinValues;\n util.jsonStringifyReplacer = (_, value) => {\n if (typeof value === \"bigint\") {\n return value.toString();\n }\n return value;\n };\n})(util || (util = {}));\nvar objectUtil;\n(function (objectUtil) {\n objectUtil.mergeShapes = (first, second) => {\n return {\n ...first,\n ...second, // second overwrites first\n };\n };\n})(objectUtil || (objectUtil = {}));\nconst ZodParsedType = util.arrayToEnum([\n \"string\",\n \"nan\",\n \"number\",\n \"integer\",\n \"float\",\n \"boolean\",\n \"date\",\n \"bigint\",\n \"symbol\",\n \"function\",\n \"undefined\",\n \"null\",\n \"array\",\n \"object\",\n \"unknown\",\n \"promise\",\n \"void\",\n \"never\",\n \"map\",\n \"set\",\n]);\nconst getParsedType = (data) => {\n const t = typeof data;\n switch (t) {\n case \"undefined\":\n return ZodParsedType.undefined;\n case \"string\":\n return ZodParsedType.string;\n case \"number\":\n return isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;\n case \"boolean\":\n return ZodParsedType.boolean;\n case \"function\":\n return ZodParsedType.function;\n case \"bigint\":\n return ZodParsedType.bigint;\n case \"symbol\":\n return ZodParsedType.symbol;\n case \"object\":\n if (Array.isArray(data)) {\n return ZodParsedType.array;\n }\n if