@memberjunction/actions-bizapps-lms
Version:
LMS system integration actions for MemberJunction
317 lines (276 loc) • 11.8 kB
text/typescript
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 { UpdateUserProgressParams, UpdateUserProgressResult, ProgressDetails, ProgressUpdateSummary, ProgressUpdateResult } from '../interfaces';
// ----------------------------------------------------------------
// File-local interfaces for raw LearnWorlds API shapes
// ----------------------------------------------------------------
/** Shape returned by the enrollment / progress GET endpoints */
interface LWEnrollmentProgress {
progress_percentage?: number;
completed_units?: number;
total_units?: number;
total_time_spent?: number;
last_accessed_at?: string;
completed?: boolean;
completed_at?: string;
}
/** Request body sent to the lesson-level progress PUT endpoint */
interface LWLessonProgressRequest {
completed: boolean;
progress_percentage?: number;
time_spent?: number;
score?: number;
notes?: string;
}
/** Request body sent to the course-level progress PUT endpoint */
interface LWCourseProgressRequest {
progress_percentage?: number;
completed?: boolean;
total_time_spent?: number;
}
/**
* Action to update a user's course progress in LearnWorlds
*/
(BaseAction, 'UpdateUserProgressAction')
export class UpdateUserProgressAction extends LearnWorldsBaseAction {
// ----------------------------------------------------------------
// Typed public method – can be called directly from code
// ----------------------------------------------------------------
/**
* Update user progress for a course or lesson.
* Throws on any error.
*/
public async UpdateProgress(params: UpdateUserProgressParams, contextUser: UserInfo): Promise<UpdateUserProgressResult> {
this.SetCompanyContext(params.CompanyID);
const { UserID, CourseID, LessonID, ProgressPercentage, Completed, TimeSpent, Score, Notes } = params;
// Validate required fields
if (!UserID) {
throw new Error('UserID is required');
}
if (!CourseID) {
throw new Error('CourseID is required');
}
this.validatePathSegment(UserID, 'UserID');
this.validatePathSegment(CourseID, 'CourseID');
if (LessonID) {
this.validatePathSegment(LessonID, 'LessonID');
}
if (ProgressPercentage !== undefined && (ProgressPercentage < 0 || ProgressPercentage > 100)) {
throw new Error('ProgressPercentage must be between 0 and 100');
}
// Get current enrollment first
const currentEnrollment = await this.fetchCurrentEnrollment(UserID, CourseID, contextUser);
const updateResult: ProgressUpdateResult = {};
// Update lesson progress if lessonId is provided
if (LessonID) {
updateResult.lessonProgress = await this.updateLessonProgress(
UserID,
CourseID,
LessonID,
{ Completed, ProgressPercentage, TimeSpent, Score, Notes },
contextUser,
);
}
// Update overall course progress if progressPercentage is provided at course level
if (ProgressPercentage !== undefined && !LessonID) {
updateResult.courseProgress = await this.updateCourseProgress(
UserID,
CourseID,
currentEnrollment,
{ Completed, ProgressPercentage, TimeSpent },
contextUser,
);
}
// Get updated enrollment details
const updatedProgress = await this.fetchUpdatedEnrollment(UserID, CourseID, currentEnrollment, contextUser);
// Build typed result
const updateType: 'lesson' | 'course' = LessonID ? 'lesson' : 'course';
const progressDetails = this.buildProgressDetails(UserID, CourseID, LessonID, currentEnrollment, updatedProgress, updateType, updateResult);
const summary = this.buildProgressSummary(UserID, CourseID, LessonID, currentEnrollment, updatedProgress, TimeSpent, updateType);
return { ProgressDetails: progressDetails, Summary: summary };
}
// ----------------------------------------------------------------
// Framework wrapper – thin delegation to the public method
// ----------------------------------------------------------------
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
const { Params, ContextUser } = params;
this.params = Params;
try {
const typedParams = this.extractUpdateProgressParams(Params);
const result = await this.UpdateProgress(typedParams, ContextUser);
this.setOutputParam(Params, 'ProgressDetails', result.ProgressDetails);
this.setOutputParam(Params, 'Summary', result.Summary);
return this.buildSuccessResult(`Successfully updated ${result.ProgressDetails.updateType} progress`, Params);
} catch (error) {
const msg = error instanceof Error ? error.message : 'Unknown error';
return this.buildErrorResult('ERROR', `Error updating progress: ${msg}`, Params);
}
}
// ----------------------------------------------------------------
// Private helpers
// ----------------------------------------------------------------
private extractUpdateProgressParams(params: ActionParam[]): UpdateUserProgressParams {
return {
CompanyID: this.getRequiredStringParam(params, 'CompanyID'),
UserID: this.getRequiredStringParam(params, 'UserID'),
CourseID: this.getRequiredStringParam(params, 'CourseID'),
LessonID: this.getOptionalStringParam(params, 'LessonID'),
ProgressPercentage: this.getOptionalNumberParam(params, 'ProgressPercentage', undefined),
Completed: this.getOptionalBooleanParam(params, 'Completed', undefined),
TimeSpent: this.getOptionalNumberParam(params, 'TimeSpent', undefined),
Score: this.getOptionalNumberParam(params, 'Score', undefined),
Notes: this.getOptionalStringParam(params, 'Notes'),
};
}
private async fetchCurrentEnrollment(userId: string, courseId: string, contextUser: UserInfo): Promise<LWEnrollmentProgress> {
try {
return await this.makeLearnWorldsRequest<LWEnrollmentProgress>(`users/${userId}/enrollments/${courseId}`, 'GET', undefined, contextUser);
} catch (error) {
throw new Error('User is not enrolled in this course: ' + (error instanceof Error ? error.message : String(error)));
}
}
private async updateLessonProgress(
userId: string,
courseId: string,
lessonId: string,
data: { Completed?: boolean; ProgressPercentage?: number; TimeSpent?: number; Score?: number; Notes?: string },
contextUser: UserInfo,
): Promise<Record<string, unknown>> {
const body: LWLessonProgressRequest = {
completed: data.Completed !== undefined ? data.Completed : false,
progress_percentage: data.ProgressPercentage,
};
if (data.TimeSpent !== undefined) {
body.time_spent = data.TimeSpent;
}
if (data.Score !== undefined) {
body.score = data.Score;
}
if (data.Notes) {
body.notes = data.Notes;
}
return await this.makeLearnWorldsRequest<Record<string, unknown>>(
`users/${userId}/courses/${courseId}/lessons/${lessonId}/progress`,
'PUT',
body,
contextUser,
);
}
private async updateCourseProgress(
userId: string,
courseId: string,
currentEnrollment: LWEnrollmentProgress,
data: { Completed?: boolean; ProgressPercentage?: number; TimeSpent?: number },
contextUser: UserInfo,
): Promise<Record<string, unknown>> {
const body: LWCourseProgressRequest = {
progress_percentage: data.ProgressPercentage,
};
if (data.Completed !== undefined) {
body.completed = data.Completed;
}
if (data.TimeSpent !== undefined) {
body.total_time_spent = (currentEnrollment.total_time_spent || 0) + data.TimeSpent;
}
return await this.makeLearnWorldsRequest<Record<string, unknown>>(
`users/${userId}/enrollments/${courseId}/progress`,
'PUT',
body,
contextUser,
);
}
private async fetchUpdatedEnrollment(userId: string, courseId: string, fallback: LWEnrollmentProgress, contextUser: UserInfo): Promise<LWEnrollmentProgress> {
try {
return await this.makeLearnWorldsRequest<LWEnrollmentProgress>(`users/${userId}/enrollments/${courseId}`, 'GET', undefined, contextUser);
} catch (error) {
// If we can't get updated details, use previous enrollment
console.warn('Failed to get updated enrollment details:', error);
return fallback;
}
}
private buildProgressDetails(
userId: string,
courseId: string,
lessonId: string | undefined,
currentEnrollment: LWEnrollmentProgress,
updatedProgress: LWEnrollmentProgress,
updateType: 'lesson' | 'course',
updateResult: ProgressUpdateResult,
): ProgressDetails {
return {
userId,
courseId,
lessonId,
previousProgress: {
percentage: currentEnrollment.progress_percentage || 0,
completedUnits: currentEnrollment.completed_units || 0,
totalTimeSpent: currentEnrollment.total_time_spent || 0,
},
updatedProgress: {
percentage: updatedProgress.progress_percentage || 0,
completedUnits: updatedProgress.completed_units || 0,
totalUnits: updatedProgress.total_units || 0,
totalTimeSpent: updatedProgress.total_time_spent || 0,
totalTimeSpentText: this.formatDuration(updatedProgress.total_time_spent || 0),
lastAccessedAt: updatedProgress.last_accessed_at || new Date().toISOString(),
completed: updatedProgress.completed || false,
completedAt: updatedProgress.completed_at,
},
updateType,
updateResult,
};
}
private buildProgressSummary(
userId: string,
courseId: string,
lessonId: string | undefined,
currentEnrollment: LWEnrollmentProgress,
updatedProgress: LWEnrollmentProgress,
timeSpent: number | undefined,
updateType: 'lesson' | 'course',
): ProgressUpdateSummary {
return {
userId,
courseId,
lessonId,
progressIncreased: (updatedProgress.progress_percentage || 0) > (currentEnrollment.progress_percentage || 0),
previousPercentage: currentEnrollment.progress_percentage || 0,
newPercentage: updatedProgress.progress_percentage || 0,
timeAdded: timeSpent || 0,
totalTimeSpent: updatedProgress.total_time_spent || 0,
isCompleted: updatedProgress.completed || false,
updateType,
};
}
// ----------------------------------------------------------------
// Params & Description metadata
// ----------------------------------------------------------------
/**
* Define the parameters this action expects
*/
public get Params(): ActionParam[] {
const baseParams = this.getCommonLMSParams();
const specificParams: ActionParam[] = [
{ Name: 'UserID', Type: 'Input', Value: null },
{ Name: 'CourseID', Type: 'Input', Value: null },
{ Name: 'LessonID', Type: 'Input', Value: null },
{ Name: 'ProgressPercentage', Type: 'Input', Value: null },
{ Name: 'Completed', Type: 'Input', Value: null },
{ Name: 'TimeSpent', Type: 'Input', Value: null },
{ Name: 'Score', Type: 'Input', Value: null },
{ Name: 'Notes', Type: 'Input', Value: null },
{ Name: 'ProgressDetails', Type: 'Output', Value: null },
{ Name: 'Summary', Type: 'Output', Value: null },
];
return [...baseParams, ...specificParams];
}
/**
* Metadata about this action
*/
public get Description(): string {
return "Updates a user's progress for a course or specific lesson in LearnWorlds";
}
}