UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

427 lines (388 loc) 13.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 { GetCourseDetailsParams, GetCourseDetailsResult, CourseDetailsData, CourseModule, CourseLesson, CourseInstructor, CourseStats, CourseDetailsSummary, } from '../interfaces'; /** * Raw API shape returned by the LearnWorlds GET /v2/courses/{id} endpoint. * The v2 API returns the course object directly (no wrapper). */ interface LWApiCourseData { id: string; title: string; slug?: string; description?: string | null; short_description?: string; access?: string; original_price?: number; discount_price?: number; final_price?: number; currency?: string; level?: string; language?: string; duration?: number; total_enrollments?: number; average_rating?: number; total_ratings?: number; tags?: string[]; categories?: string[]; courseImage?: string | null; image_url?: string; video_url?: string; certificate_enabled?: boolean; created?: number; modified?: number; created_at?: string; updated_at?: string; published_at?: string; label?: string | null; expires?: number | null; expiresType?: string; author?: { name?: string; image?: string | null } | null; } /** * Raw API shape for sections/modules response */ interface LWApiSectionsResponse { data?: LWApiModuleData[] | { data: LWApiModuleData[] }; } interface LWApiModuleData { id: string; title: string; description?: string; order?: number; position?: number; duration?: number; total_lessons?: number; lessons?: LWApiLessonData[]; } interface LWApiLessonData { id: string; title: string; type?: string; duration?: number; order?: number; position?: number; is_free?: boolean; has_video?: boolean; has_quiz?: boolean; has_assignment?: boolean; } /** * Raw API shape for instructors response */ interface LWApiInstructorsResponse { data?: LWApiInstructorData[] | { data: LWApiInstructorData[] }; } interface LWApiInstructorData { id: string; name?: string; first_name?: string; last_name?: string; email?: string; bio?: string; title?: string; image_url?: string; avatar_url?: string; total_courses?: number; total_students?: number; average_rating?: number; } /** * Raw API shape for stats response */ interface LWApiStatsResponse { success?: boolean; data?: { total_enrollments?: number; active_students?: number; completion_rate?: number; average_progress?: number; average_time_to_complete?: number; total_revenue?: number; }; message?: string; } /** * Action to retrieve comprehensive course details including curriculum structure */ @RegisterClass(BaseAction, 'GetLearnWorldsCourseDetailsAction') export class GetLearnWorldsCourseDetailsAction extends LearnWorldsBaseAction { /** * Metadata about this action */ public get Description(): string { return 'Retrieves comprehensive details about a specific LearnWorlds course including curriculum structure'; } /** * Typed public method for direct (non-framework) callers. * Sets company context, fetches course details, and returns a strongly-typed result. * Throws on error. */ public async GetCourseDetails(params: GetCourseDetailsParams, contextUser: UserInfo): Promise<GetCourseDetailsResult> { this.SetCompanyContext(params.CompanyID); const { CourseID: courseId, IncludeModules: includeModules = true, IncludeInstructors: includeInstructors = true, IncludeStats: includeStats = true, } = params; if (!courseId) { throw new Error('CourseID is required'); } this.validatePathSegment(courseId, 'CourseID'); // Get course details — LW v2 API returns the course object directly const course = await this.makeLearnWorldsRequest<LWApiCourseData>(`courses/${courseId}`, 'GET', undefined, contextUser); if (!course || !course.id) { throw new Error('Failed to retrieve course details'); } const courseDetails = this.buildCourseDetails(course); if (includeModules) { await this.attachModules(courseId, courseDetails, contextUser); } if (includeInstructors) { await this.attachInstructors(courseId, courseDetails, contextUser); } if (includeStats) { await this.attachStats(courseId, courseDetails, contextUser); } const summary = this.buildCourseSummary(courseDetails); return { CourseDetails: courseDetails, Summary: summary, }; } /** * Framework entry-point – thin wrapper around the typed public method. */ protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> { const { Params, ContextUser } = params; this.params = Params; try { const typedParams = this.extractGetCourseDetailsParams(Params); const result = await this.GetCourseDetails(typedParams, ContextUser); this.setOutputParam(Params, 'CourseDetails', result.CourseDetails); this.setOutputParam(Params, 'Summary', result.Summary); return this.buildSuccessResult('Course details retrieved successfully', Params); } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error occurred'; return this.buildErrorResult('ERROR', `Error retrieving course details: ${msg}`, Params); } } /** * Extract typed params from framework ActionParam[] */ private extractGetCourseDetailsParams(params: ActionParam[]): GetCourseDetailsParams { return { CompanyID: this.getRequiredStringParam(params, 'CompanyID'), CourseID: this.getRequiredStringParam(params, 'CourseID'), IncludeModules: this.getOptionalBooleanParam(params, 'IncludeModules', true), IncludeInstructors: this.getOptionalBooleanParam(params, 'IncludeInstructors', true), IncludeStats: this.getOptionalBooleanParam(params, 'IncludeStats', true), }; } /** * Build the base course details from raw API data */ private buildCourseDetails(course: LWApiCourseData): CourseDetailsData { return { id: course.id, title: course.title, slug: course.slug, description: course.description || undefined, shortDescription: course.short_description, status: course.access || 'published', price: course.final_price ?? course.original_price ?? 0, originalPrice: course.original_price, currency: course.currency || 'USD', level: course.level || 'all', language: course.language || 'en', duration: course.duration, durationText: this.formatDuration(course.duration || 0), totalEnrollments: course.total_enrollments || 0, averageRating: course.average_rating, totalRatings: course.total_ratings || 0, tags: course.tags || [], categories: course.categories || [], imageUrl: course.courseImage || course.image_url || undefined, videoUrl: course.video_url, certificateEnabled: course.certificate_enabled || false, createdAt: course.created ? String(course.created) : course.created_at, updatedAt: course.modified ? String(course.modified) : course.updated_at, publishedAt: course.published_at, }; } /** * Fetch and attach modules/curriculum to course details */ private async attachModules(courseId: string, courseDetails: CourseDetailsData, contextUser: UserInfo): Promise<void> { try { const modulesResponse = await this.makeLearnWorldsRequest<LWApiSectionsResponse>(`courses/${courseId}/sections`, 'GET', undefined, contextUser); const rawModules = modulesResponse.data ? (Array.isArray(modulesResponse.data) ? modulesResponse.data : (modulesResponse.data as { data: LWApiModuleData[] }).data || []) : []; if (rawModules.length > 0) { courseDetails.modules = this.formatModules(rawModules); courseDetails.totalModules = courseDetails.modules.length; courseDetails.totalLessons = courseDetails.modules.reduce((sum, mod) => sum + (mod.lessons?.length || 0), 0); } } catch (error) { console.warn(`Sections endpoint unavailable for course ${courseId}:`, error instanceof Error ? error.message : error); } } /** * Fetch and attach instructors to course details */ private async attachInstructors(courseId: string, courseDetails: CourseDetailsData, contextUser: UserInfo): Promise<void> { try { const instructorsResponse = await this.makeLearnWorldsRequest<LWApiInstructorsResponse>(`courses/${courseId}/instructors`, 'GET', undefined, contextUser); const rawInstructors = instructorsResponse.data ? (Array.isArray(instructorsResponse.data) ? instructorsResponse.data : (instructorsResponse.data as { data: LWApiInstructorData[] }).data || []) : []; if (rawInstructors.length > 0) { courseDetails.instructors = this.formatInstructors(rawInstructors); } } catch (error) { console.warn(`Instructors endpoint unavailable for course ${courseId}:`, error instanceof Error ? error.message : error); } } /** * Fetch and attach stats to course details */ private async attachStats(courseId: string, courseDetails: CourseDetailsData, contextUser: UserInfo): Promise<void> { try { const statsResponse = await this.makeLearnWorldsRequest<LWApiStatsResponse>(`courses/${courseId}/stats`, 'GET', undefined, contextUser); if (statsResponse.data) { const statsData = statsResponse.data; courseDetails.stats = { totalEnrollments: statsData.total_enrollments || courseDetails.totalEnrollments, activeStudents: statsData.active_students || 0, completionRate: statsData.completion_rate || 0, averageProgressPercentage: statsData.average_progress || 0, averageTimeToComplete: statsData.average_time_to_complete, totalRevenue: statsData.total_revenue || 0, } satisfies CourseStats; } } catch (error) { console.warn(`Stats endpoint unavailable for course ${courseId}:`, error instanceof Error ? error.message : error); } } /** * Format course modules/sections data */ private formatModules(modules: LWApiModuleData[]): CourseModule[] { return modules .map((module) => ({ id: module.id, title: module.title, description: module.description, order: module.order || module.position || 0, duration: module.duration, durationText: this.formatDuration(module.duration || 0), totalLessons: module.total_lessons || module.lessons?.length || 0, lessons: this.formatLessons(module.lessons || []), })) .sort((a, b) => a.order - b.order); } /** * Format lesson data within a module */ private formatLessons(lessons: LWApiLessonData[]): CourseLesson[] { return lessons.map((lesson) => ({ id: lesson.id, title: lesson.title, type: lesson.type || 'video', duration: lesson.duration, durationText: this.formatDuration(lesson.duration || 0), order: lesson.order || lesson.position || 0, isFree: lesson.is_free || false, hasVideo: lesson.has_video || false, hasQuiz: lesson.has_quiz || false, hasAssignment: lesson.has_assignment || false, })); } /** * Format instructor data */ private formatInstructors(instructors: LWApiInstructorData[]): CourseInstructor[] { return instructors.map((instructor) => ({ id: instructor.id, name: instructor.name || `${instructor.first_name || ''} ${instructor.last_name || ''}`.trim(), email: instructor.email, bio: instructor.bio, title: instructor.title, imageUrl: instructor.image_url || instructor.avatar_url, totalCourses: instructor.total_courses || 0, totalStudents: instructor.total_students || 0, averageRating: instructor.average_rating || 0, })); } /** * Build a summary from the course details */ private buildCourseSummary(courseDetails: CourseDetailsData): CourseDetailsSummary { return { courseId: courseDetails.id, title: courseDetails.title, status: courseDetails.status, level: courseDetails.level, duration: courseDetails.durationText, totalModules: courseDetails.totalModules || 0, totalLessons: courseDetails.totalLessons || 0, totalEnrollments: courseDetails.totalEnrollments, averageRating: courseDetails.averageRating || 0, certificateEnabled: courseDetails.certificateEnabled, price: courseDetails.price, currency: courseDetails.currency, }; } /** * Define the parameters this action expects */ public get Params(): ActionParam[] { const baseParams = this.getCommonLMSParams(); const specificParams: ActionParam[] = [ { Name: 'CourseID', Type: 'Input', Value: null, }, { Name: 'IncludeModules', Type: 'Input', Value: true, }, { Name: 'IncludeInstructors', Type: 'Input', Value: true, }, { Name: 'IncludeStats', Type: 'Input', Value: true, }, { Name: 'CourseDetails', Type: 'Output', Value: null, }, { Name: 'Summary', Type: 'Output', Value: null, }, ]; return [...baseParams, ...specificParams]; } }