UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

287 lines (251 loc) 10.7 kB
import { RegisterClass } from '@memberjunction/global'; import { LearnWorldsBaseAction } from '../learnworlds-base.action'; import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { BaseAction } from '@memberjunction/actions'; import { UserInfo } from '@memberjunction/core'; import { OnboardLearnerParams, OnboardLearnerResult, OnboardLearnerEnrollmentResult } from '../interfaces'; import { CreateUserAction } from './create-user.action'; import { EnrollUserAction } from './enroll-user.action'; import { SSOLoginAction } from './sso-login.action'; /** * Action that orchestrates full learner onboarding: * create user, enroll in courses/bundles, and generate SSO URL. */ @RegisterClass(BaseAction, 'OnboardLearnerAction') export class OnboardLearnerAction extends LearnWorldsBaseAction { /** * Orchestrates the full onboarding flow for a new or existing learner: * 1. Find or create user * 2. Enroll in courses and bundles * 3. Generate SSO login URL */ public async OnboardLearner(params: OnboardLearnerParams, contextUser: UserInfo): Promise<OnboardLearnerResult> { this.SetCompanyContext(params.CompanyID); this.validateEmail(params.Email); if (params.Role) { this.validateRole(params.Role); } if (params.CourseIDs) { params.CourseIDs.forEach((id) => this.validatePathSegment(id, 'CourseID')); } if (params.BundleIDs) { params.BundleIDs.forEach((id) => this.validatePathSegment(id, 'BundleID')); } if (params.RedirectTo) { this.validateRedirectTo(params.RedirectTo); } // Step 1: Find or create the user const { userId, isNewUser, loginUrlFallback } = await this.resolveUser(params, contextUser); // Step 2: Enroll in courses and bundles const { enrollments, errors } = await this.enrollInProducts(params, userId, contextUser); // Step 3: Generate SSO login URL const loginURL = await this.generateLoginUrl(params, userId, loginUrlFallback, contextUser); const hasEnrollmentErrors = errors.length > 0; const allEnrollmentsFailed = enrollments.length > 0 && enrollments.every((e) => !e.success); return { Success: !allEnrollmentsFailed, LoginURL: loginURL, LearnWorldsUserId: userId, IsNewUser: isNewUser, Enrollments: enrollments, Errors: hasEnrollmentErrors ? errors : [], }; } /** * Finds an existing user by email or creates a new one. * Returns the user ID, whether they are new, and a fallback login URL from creation. */ private async resolveUser(params: OnboardLearnerParams, contextUser: UserInfo): Promise<{ userId: string; isNewUser: boolean; loginUrlFallback?: string }> { const existingUser = await this.FindUserByEmail(params.Email, contextUser); if (existingUser) { return { userId: existingUser.id, isNewUser: false }; } return this.createNewUser(params, contextUser); } /** * Creates a new user via the CreateUserAction typed public method. * Returns the user's ID and login URL. */ private async createNewUser(params: OnboardLearnerParams, contextUser: UserInfo): Promise<{ userId: string; isNewUser: boolean; loginUrlFallback?: string }> { const createAction = new CreateUserAction(); const createResult = await createAction.CreateUser( { CompanyID: params.CompanyID, Email: params.Email, FirstName: params.FirstName, LastName: params.LastName, Role: params.Role || 'student', Tags: params.Tags, CustomFields: params.CustomFields, SendWelcomeEmail: params.SendWelcomeEmail ?? false, }, contextUser, ); if (!createResult.UserDetails.id) { throw new Error('User creation succeeded but no user ID was returned'); } return { userId: createResult.UserDetails.id, isNewUser: true, loginUrlFallback: createResult.UserDetails.loginUrl, }; } /** * Enrolls the user in all specified courses and bundles in parallel, * collecting results and errors. */ private async enrollInProducts( params: OnboardLearnerParams, userId: string, contextUser: UserInfo, ): Promise<{ enrollments: OnboardLearnerEnrollmentResult[]; errors: string[] }> { const courseIDs = params.CourseIDs || []; const bundleIDs = params.BundleIDs || []; // Fire enrollment requests with controlled concurrency const allProducts = [ ...courseIDs.map((id) => ({ id, type: 'course' as const })), ...bundleIDs.map((id) => ({ id, type: 'bundle' as const })), ]; const enrollments = await this.processInBatches(allProducts, (product) => this.enrollInSingleProduct(params.CompanyID, userId, product.id, product.type, contextUser), ); const errors = enrollments.filter((e) => e.error).map((e) => e.error!); return { enrollments, errors }; } /** * Enrolls a user in a single course or bundle, catching errors for partial success. */ private async enrollInSingleProduct( companyId: string, userId: string, productId: string, productType: 'course' | 'bundle', contextUser: UserInfo, ): Promise<OnboardLearnerEnrollmentResult> { try { const enrollAction = new EnrollUserAction(); const enrollResult = await enrollAction.EnrollUser( { CompanyID: companyId, UserID: userId, CourseID: productId, ProductType: productType, Price: 0, Justification: 'Onboarding enrollment', NotifyUser: false, }, contextUser, ); return { productId, productType, success: true, enrollmentId: enrollResult.EnrollmentDetails.id, }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { productId, productType, success: false, error: `Enrollment failed for ${productType} ${productId}: ${errorMessage}`, }; } } /** * Generates an SSO login URL, falling back to the user creation login URL on failure. */ private async generateLoginUrl(params: OnboardLearnerParams, userId: string, loginUrlFallback: string | undefined, contextUser: UserInfo): Promise<string | undefined> { try { const ssoAction = new SSOLoginAction(); ssoAction.SetCompanyContext(params.CompanyID); const ssoResult = await ssoAction.GenerateSSOUrl( { CompanyID: params.CompanyID, Email: params.Email, RedirectTo: params.RedirectTo, }, contextUser, ); return ssoResult.LoginURL; } catch (error) { console.warn(`SSO URL generation failed for userId ${userId}, using fallback:`, error instanceof Error ? error.message : error); return loginUrlFallback; } } /** * Framework entry point that delegates to the typed public method */ protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> { const { Params, ContextUser } = params; this.params = Params; try { const onboardParams: OnboardLearnerParams = { CompanyID: this.getRequiredStringParam(Params, 'CompanyID'), Email: this.getRequiredStringParam(Params, 'Email'), FirstName: this.getOptionalStringParam(Params, 'FirstName'), LastName: this.getOptionalStringParam(Params, 'LastName'), Role: this.getOptionalStringParam(Params, 'Role'), Tags: this.getOptionalStringArrayParam(Params, 'Tags'), CustomFields: this.getParamValue(Params, 'CustomFields') as Record<string, unknown> | undefined, CourseIDs: this.getOptionalStringArrayParam(Params, 'CourseIDs'), BundleIDs: this.getOptionalStringArrayParam(Params, 'BundleIDs'), RedirectTo: this.getOptionalStringParam(Params, 'RedirectTo'), SendWelcomeEmail: this.getOptionalBooleanParam(Params, 'SendWelcomeEmail', false), }; if (!onboardParams.Email) { return this.buildErrorResult('VALIDATION_ERROR', 'Email is required', Params); } const result = await this.OnboardLearner(onboardParams, ContextUser); this.setOutputParam(Params, 'LoginURL', result.LoginURL); this.setOutputParam(Params, 'LearnWorldsUserId', result.LearnWorldsUserId); this.setOutputParam(Params, 'IsNewUser', result.IsNewUser); this.setOutputParam(Params, 'Enrollments', result.Enrollments); this.setOutputParam(Params, 'Errors', result.Errors); const successCount = result.Enrollments.filter((e) => e.success).length; const totalCount = result.Enrollments.length; const userStatus = result.IsNewUser ? 'new user created' : 'existing user found'; if (result.Success) { return this.buildSuccessResult(`Onboarding complete (${userStatus}): ${successCount}/${totalCount} enrollment(s) succeeded`, Params); } return this.buildErrorResult( 'PARTIAL_FAILURE', `Onboarding partially failed (${userStatus}): ${successCount}/${totalCount} enrollment(s) succeeded. Errors: ${result.Errors.join('; ')}`, Params, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return this.buildErrorResult('ERROR', `Error during onboarding: ${errorMessage}`, Params); } } /** * Define the parameters this action expects */ public get Params(): ActionParam[] { const baseParams = this.getCommonLMSParams(); const specificParams: ActionParam[] = [ { Name: 'Email', Type: 'Input', Value: null }, { Name: 'FirstName', Type: 'Input', Value: null }, { Name: 'LastName', Type: 'Input', Value: null }, { Name: 'Role', Type: 'Input', Value: 'student' }, { Name: 'Tags', Type: 'Input', Value: null }, { Name: 'CustomFields', Type: 'Input', Value: null }, { Name: 'CourseIDs', Type: 'Input', Value: null }, { Name: 'BundleIDs', Type: 'Input', Value: null }, { Name: 'RedirectTo', Type: 'Input', Value: null }, { Name: 'SendWelcomeEmail', Type: 'Input', Value: false }, { Name: 'LoginURL', Type: 'Output', Value: null }, { Name: 'LearnWorldsUserId', Type: 'Output', Value: null }, { Name: 'IsNewUser', Type: 'Output', Value: null }, { Name: 'Enrollments', Type: 'Output', Value: null }, { Name: 'Errors', Type: 'Output', Value: null }, ]; return [...baseParams, ...specificParams]; } /** * Metadata about this action */ public get Description(): string { return 'Orchestrates full learner onboarding: create user, enroll in courses/bundles, and generate SSO URL'; } }