UNPKG

@memberjunction/actions-bizapps-lms

Version:

LMS system integration actions for MemberJunction

366 lines 16.4 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { RegisterClass } from '@memberjunction/global'; import { LearnWorldsBaseAction } from '../learnworlds-base.action.js'; import { BaseAction } from '@memberjunction/actions'; /** * Action to retrieve certificates earned by users in LearnWorlds */ let GetCertificatesAction = class GetCertificatesAction extends LearnWorldsBaseAction { // ---------------------------------------------------------------- // Typed public method – can be called directly from code // ---------------------------------------------------------------- /** * Get certificates for a user or course. * Throws on any error. */ async GetCertificates(params, contextUser) { this.SetCompanyContext(params.CompanyID); const { UserID: userId, CourseID: courseId, DateFrom: dateFrom, DateTo: dateTo, IncludeDownloadLinks: includeDownloadLinksRaw, SortBy: sortBy = 'issued_at', SortOrder: sortOrder = 'desc', MaxResults: maxResults = 100, } = params; const includeDownloadLinks = includeDownloadLinksRaw !== false; // Require either userId or courseId if (!userId && !courseId) { throw new Error('Either UserID or CourseID is required'); } // Build query parameters const queryParams = { limit: Math.min(maxResults, LearnWorldsBaseAction.LW_MAX_PAGE_SIZE), sort: sortBy, order: sortOrder, }; const parsedFrom = this.safeParseDateToISO(dateFrom); if (parsedFrom) { queryParams.issued_after = parsedFrom; } const parsedTo = this.safeParseDateToISO(dateTo); if (parsedTo) { queryParams.issued_before = parsedTo; } // Validate path segments before URL interpolation if (userId) this.validatePathSegment(userId, 'UserID'); if (courseId) this.validatePathSegment(courseId, 'CourseID'); // Determine endpoint based on parameters const endpoint = this.buildCertificatesEndpoint(userId, courseId); // Build query string const queryString = this.buildQueryString(queryParams); // Get certificates const certificatesResponse = await this.makeLearnWorldsRequest(endpoint + queryString, 'GET', null, contextUser); if (certificatesResponse.success === false) { throw new Error(certificatesResponse.message || 'Failed to retrieve certificates'); } // Handle single certificate vs array const certificatesArray = this.extractCertificatesArray(certificatesResponse); // Process each certificate const formattedCertificates = await this.processCertificates(certificatesArray, userId, courseId, includeDownloadLinks, contextUser); // Calculate summary const summary = this.buildCertificatesSummary(formattedCertificates, userId, courseId, dateFrom, dateTo); return { Certificates: formattedCertificates, TotalCount: formattedCertificates.length, Summary: summary, }; } // ---------------------------------------------------------------- // Framework wrapper – thin delegation to the public method // ---------------------------------------------------------------- async InternalRunAction(params) { const { Params, ContextUser } = params; this.params = Params; try { const typedParams = this.extractCertificatesParams(Params); const result = await this.GetCertificates(typedParams, ContextUser); this.setOutputParam(Params, 'Certificates', result.Certificates); this.setOutputParam(Params, 'TotalCount', result.TotalCount); this.setOutputParam(Params, 'Summary', result.Summary); return this.buildSuccessResult(`Retrieved ${result.TotalCount} certificate(s)`, Params); } catch (error) { const msg = error instanceof Error ? error.message : 'Unknown error'; return this.buildErrorResult('ERROR', `Error retrieving certificates: ${msg}`, Params); } } // ---------------------------------------------------------------- // Private helpers // ---------------------------------------------------------------- extractCertificatesParams(params) { return { CompanyID: this.getRequiredStringParam(params, 'CompanyID'), UserID: this.getOptionalStringParam(params, 'UserID'), CourseID: this.getOptionalStringParam(params, 'CourseID'), DateFrom: this.getOptionalStringParam(params, 'DateFrom'), DateTo: this.getOptionalStringParam(params, 'DateTo'), IncludeDownloadLinks: this.getOptionalBooleanParam(params, 'IncludeDownloadLinks', true), SortBy: this.getOptionalStringParam(params, 'SortBy'), SortOrder: (this.getOptionalStringParam(params, 'SortOrder') || 'desc'), MaxResults: this.getOptionalNumberParam(params, 'MaxResults', LearnWorldsBaseAction.LW_MAX_PAGE_SIZE), }; } buildCertificatesEndpoint(userId, courseId) { if (userId && courseId) { return `/users/${userId}/courses/${courseId}/certificate`; } else if (userId) { return `/users/${userId}/certificates`; } else if (courseId) { return `/courses/${courseId}/certificates`; } return '/certificates'; } buildQueryString(queryParams) { const filtered = {}; for (const [key, value] of Object.entries(queryParams)) { if (value !== undefined) { filtered[key] = String(value); } } const keys = Object.keys(filtered); if (keys.length === 0) return ''; return '?' + new URLSearchParams(filtered).toString(); } extractCertificatesArray(response) { const rawData = response.data; if (Array.isArray(rawData)) { return rawData; } if (rawData && typeof rawData === 'object') { const nested = rawData; if (Array.isArray(nested.data)) { return nested.data; } if (nested.id) { return [rawData]; } } return []; } async processCertificates(certificatesArray, userId, courseId, includeDownloadLinks, contextUser) { // Pre-fetch all user and course info in batch to avoid per-item API calls const userInfoMap = await this.batchResolveUserInfo(certificatesArray, userId, contextUser); const courseInfoMap = await this.batchResolveCourseInfo(certificatesArray, courseId, contextUser); return certificatesArray.map((cert) => { const formattedCert = this.buildBaseCertificate(cert, userId, courseId); // Attach user info from pre-fetched map const certUserId = cert.user_id || ''; formattedCert.user = cert.user ? this.extractInlineUserInfo(cert) : userInfoMap.get(certUserId); // Attach course info from pre-fetched map const certCourseId = cert.course_id || ''; formattedCert.course = cert.course ? this.extractInlineCourseInfo(cert) : courseInfoMap.get(certCourseId); // Attach download links if requested if (includeDownloadLinks) { formattedCert.downloadLinks = { pdf: cert.pdf_url || cert.download_url, image: cert.image_url, publicUrl: cert.public_url || cert.certificate_url, }; } // Attach verification info formattedCert.verification = { url: cert.verification_url, code: cert.verification_code, qrCode: cert.qr_code_url, }; return formattedCert; }); } /** * Extracts inline user info when the cert already includes user data. */ extractInlineUserInfo(cert) { if (!cert.user) return undefined; return { id: cert.user.id || cert.user_id || '', email: cert.user.email || '', name: cert.user.name || `${cert.user.first_name || ''} ${cert.user.last_name || ''}`.trim(), }; } /** * Extracts inline course info when the cert already includes course data. */ extractInlineCourseInfo(cert) { if (!cert.course) return undefined; return { id: cert.course.id || cert.course_id || '', title: cert.course.title || '', duration: cert.course.duration, }; } /** * Batch-fetches user info for all certificates that need external lookup. * Returns a map from userId to user info. */ async batchResolveUserInfo(certs, filterUserId, contextUser) { const map = new Map(); // Only need to look up users when cert doesn't include inline user data // and we're not filtering by a single user if (filterUserId) return map; const idsToFetch = [...new Set(certs.filter((c) => !c.user && c.user_id).map((c) => c.user_id))]; const results = await Promise.all(idsToFetch.map(async (uid) => { try { const resp = await this.makeLearnWorldsRequest(`/users/${uid}`, 'GET', null, contextUser); if (resp.success !== false && resp.data) { return { id: uid, info: { id: resp.data.id || '', email: resp.data.email || '', name: `${resp.data.first_name || ''} ${resp.data.last_name || ''}`.trim() || resp.data.username || '', }, }; } } catch (error) { console.warn(`Failed to fetch user info for ${uid}:`, error instanceof Error ? error.message : error); } return null; })); for (const result of results) { if (result) map.set(result.id, result.info); } return map; } /** * Batch-fetches course info for all certificates that need external lookup. * Returns a map from courseId to course info. */ async batchResolveCourseInfo(certs, filterCourseId, contextUser) { const map = new Map(); // Only need to look up courses when cert doesn't include inline course data // and we're not filtering by a single course if (filterCourseId) return map; const idsToFetch = [...new Set(certs.filter((c) => !c.course && c.course_id).map((c) => c.course_id))]; const results = await Promise.all(idsToFetch.map(async (cid) => { try { const resp = await this.makeLearnWorldsRequest(`/courses/${cid}`, 'GET', null, contextUser); if (resp.success !== false && resp.data) { return { id: cid, info: { id: resp.data.id || '', title: resp.data.title || '', duration: resp.data.duration, }, }; } } catch (error) { console.warn(`Failed to fetch course info for ${cid}:`, error instanceof Error ? error.message : error); } return null; })); for (const result of results) { if (result) map.set(result.id, result.info); } return map; } buildBaseCertificate(cert, userId, courseId) { return { id: cert.id || cert.certificate_id || '', userId: cert.user_id || userId || '', courseId: cert.course_id || courseId || '', certificateNumber: cert.certificate_number || cert.number, issuedAt: cert.issued_at || cert.created_at, expiresAt: cert.expires_at, status: cert.status || 'active', grade: cert.grade, score: cert.score, completionPercentage: cert.completion_percentage || 100, verification: { url: undefined, code: undefined, qrCode: undefined }, }; } buildCertificatesSummary(certificates, userId, courseId, dateFrom, dateTo) { const totalCertificates = certificates.length; const now = new Date(); const activeCertificates = certificates.filter((c) => c.status === 'active' && (!c.expiresAt || new Date(c.expiresAt) > now)).length; const expiredCertificates = certificates.filter((c) => c.expiresAt && new Date(c.expiresAt) <= now).length; // Group by course or user depending on the filter let groupedData = null; if (userId && !courseId) { groupedData = this.groupCertificatesByCourse(certificates); } else if (courseId && !userId) { groupedData = this.groupCertificatesByUser(certificates); } const filterType = userId && courseId ? 'user-course' : userId ? 'user' : 'course'; return { totalCertificates, activeCertificates, expiredCertificates, dateRange: { from: dateFrom || 'all-time', to: dateTo || 'current', }, filterType, groupedData, }; } groupCertificatesByCourse(certificates) { const grouped = {}; for (const cert of certificates) { const courseTitle = cert.course?.title || 'Unknown Course'; if (!grouped[courseTitle]) { grouped[courseTitle] = []; } grouped[courseTitle].push(cert); } return grouped; } groupCertificatesByUser(certificates) { const grouped = {}; for (const cert of certificates) { const userName = cert.user?.name || cert.user?.email || 'Unknown User'; if (!grouped[userName]) { grouped[userName] = []; } grouped[userName].push(cert); } return grouped; } // ---------------------------------------------------------------- // Params & Description metadata // ---------------------------------------------------------------- /** * Define the parameters this action expects */ get Params() { const baseParams = this.getCommonLMSParams(); const specificParams = [ { Name: 'UserID', Type: 'Input', Value: null }, { Name: 'CourseID', Type: 'Input', Value: null }, { Name: 'DateFrom', Type: 'Input', Value: null }, { Name: 'DateTo', Type: 'Input', Value: null }, { Name: 'IncludeDownloadLinks', Type: 'Input', Value: true }, { Name: 'SortBy', Type: 'Input', Value: 'issued_at' }, { Name: 'SortOrder', Type: 'Input', Value: 'desc' }, { Name: 'MaxResults', Type: 'Input', Value: 100 }, { Name: 'Certificates', Type: 'Output', Value: null }, { Name: 'TotalCount', Type: 'Output', Value: null }, { Name: 'Summary', Type: 'Output', Value: null }, ]; return [...baseParams, ...specificParams]; } /** * Metadata about this action */ get Description() { return 'Retrieves certificates earned by users in LearnWorlds courses with download links and verification info'; } }; GetCertificatesAction = __decorate([ RegisterClass(BaseAction, 'GetCertificatesAction') ], GetCertificatesAction); export { GetCertificatesAction }; //# sourceMappingURL=get-certificates.action.js.map