@hed-hog/lms 0.0.364 → 0.0.366
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/class-group/class-group.controller.d.ts +1 -0
- package/dist/class-group/class-group.controller.d.ts.map +1 -1
- package/dist/class-group/class-group.service.d.ts +1 -0
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
- package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12-worker.service.js +109 -0
- package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
- package/dist/course/course-export-scorm12.service.d.ts +42 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12.service.js +628 -0
- package/dist/course/course-export-scorm12.service.js.map +1 -0
- package/dist/course/course-export.service.d.ts +84 -0
- package/dist/course/course-export.service.d.ts.map +1 -0
- package/dist/course/course-export.service.js +237 -0
- package/dist/course/course-export.service.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +20 -10
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +20 -4
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +12 -4
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +98 -23
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.js +398 -0
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
- package/dist/course/course-video-hls.service.d.ts +71 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -0
- package/dist/course/course-video-hls.service.js +784 -0
- package/dist/course/course-video-hls.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +47 -13
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +40 -26
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.js +1 -1
- package/dist/course/course.mcp-tools.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +16 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +8 -9
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +93 -50
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
- package/dist/course/dto/create-course-export.dto.d.ts +14 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-export.dto.js +71 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/ffmpeg.util.d.ts +10 -0
- package/dist/course/ffmpeg.util.d.ts.map +1 -0
- package/dist/course/ffmpeg.util.js +79 -0
- package/dist/course/ffmpeg.util.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +18 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +106 -8
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.js +32 -8
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +43 -2
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +11 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +59 -6
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +2 -1
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +4 -2
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/course/scorm12-schemas.d.ts +4 -0
- package/dist/course/scorm12-schemas.d.ts.map +1 -0
- package/dist/course/scorm12-schemas.js +9 -0
- package/dist/course/scorm12-schemas.js.map +1 -0
- package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +2 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +51 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +217 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/evaluation/evaluation.service.d.ts +18 -0
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +125 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
- package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-standalone-question.dto.js +70 -0
- package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
- package/dist/exam/exam.module.d.ts.map +1 -1
- package/dist/exam/exam.module.js +2 -1
- package/dist/exam/exam.module.js.map +1 -1
- package/dist/exam/exam.service.d.ts +21 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +80 -0
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/exam/question.controller.d.ts +27 -0
- package/dist/exam/question.controller.d.ts.map +1 -0
- package/dist/exam/question.controller.js +53 -0
- package/dist/exam/question.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms-commerce-access.subscriber.d.ts +11 -0
- package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
- package/dist/lms-commerce-access.subscriber.js +74 -0
- package/dist/lms-commerce-access.subscriber.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +16 -5
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
- package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
- package/dist/platforma/dto/heartbeat.dto.js +50 -0
- package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
- package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.js +50 -0
- package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
- package/dist/platforma/platforma-performance.service.d.ts +121 -0
- package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
- package/dist/platforma/platforma-performance.service.js +500 -0
- package/dist/platforma/platforma-performance.service.js.map +1 -0
- package/dist/platforma/platforma-search.service.d.ts +21 -0
- package/dist/platforma/platforma-search.service.d.ts.map +1 -0
- package/dist/platforma/platforma-search.service.js +64 -0
- package/dist/platforma/platforma-search.service.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +39 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -0
- package/dist/platforma/platforma-video.service.js +301 -0
- package/dist/platforma/platforma-video.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +209 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +208 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.controller.d.ts +2 -0
- package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.controller.js +31 -0
- package/dist/realtime/lms-realtime.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.service.d.ts +1 -1
- package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.service.js.map +1 -1
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +15 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
- package/dist/student-xp/student-xp.controller.js +24 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -1
- package/dist/student-xp/student-xp.service.d.ts +16 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -1
- package/dist/student-xp/student-xp.service.js +51 -1
- package/dist/student-xp/student-xp.service.js.map +1 -1
- package/hedhog/data/evaluation_topic.yaml +17 -0
- package/hedhog/data/menu.yaml +0 -17
- package/hedhog/data/queue_definition.yaml +48 -0
- package/hedhog/data/route.yaml +94 -124
- package/hedhog/data/setting_group.yaml +19 -19
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
- package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +158 -45
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +85 -9
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
- package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
- package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
- package/hedhog/frontend/messages/en.json +44 -28
- package/hedhog/frontend/messages/pt.json +47 -29
- package/hedhog/table/course_enrollment.yaml +3 -0
- package/hedhog/table/course_export.yaml +62 -0
- package/hedhog/table/lesson_view_event.yaml +66 -0
- package/package.json +14 -9
- package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
- package/src/course/course-export-scorm12-worker.service.ts +124 -0
- package/src/course/course-export-scorm12.service.ts +668 -0
- package/src/course/course-export.service.ts +280 -0
- package/src/course/course-structure.controller.ts +16 -2
- package/src/course/course-structure.service.ts +100 -7
- package/src/course/course-video-agent-pipeline.service.ts +471 -0
- package/src/course/course-video-hls.service.ts +966 -0
- package/src/course/course.controller.ts +33 -19
- package/src/course/course.mcp-tools.ts +1 -1
- package/src/course/course.module.ts +16 -0
- package/src/course/course.service.ts +119 -61
- package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +56 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
- package/src/course/ffmpeg.util.ts +65 -0
- package/src/course/lms-bulk-upload-automation.service.ts +156 -6
- package/src/course/lms-bulk-upload-infra.service.ts +39 -6
- package/src/course/lms-bulk-upload.controller.ts +32 -2
- package/src/course/lms-bulk-upload.service.ts +70 -7
- package/src/course/lms-setting.controller.ts +4 -2
- package/src/course/scorm12-schemas.ts +9 -0
- package/src/enterprise/training/training-student.service.ts +221 -2
- package/src/evaluation/evaluation.service.ts +123 -0
- package/src/exam/dto/create-standalone-question.dto.ts +66 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +86 -0
- package/src/exam/question.controller.ts +28 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
- package/src/lms-commerce-access.subscriber.ts +88 -0
- package/src/lms.module.ts +16 -5
- package/src/platforma/dto/heartbeat.dto.ts +30 -0
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
- package/src/platforma/platforma-heartbeat.service.ts +33 -0
- package/src/platforma/platforma-performance.service.ts +606 -0
- package/src/platforma/platforma-search.service.ts +48 -0
- package/src/platforma/platforma-video.service.ts +346 -0
- package/src/platforma/platforma.controller.ts +137 -1
- package/src/platforma/platforma.service.ts +268 -268
- package/src/realtime/lms-realtime.controller.ts +27 -1
- package/src/realtime/lms-realtime.service.ts +2 -1
- package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
- package/src/student-xp/student-xp.controller.ts +18 -2
- package/src/student-xp/student-xp.service.ts +84 -2
- package/hedhog/data/video_resolution_profile.yaml +0 -7
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
- package/hedhog/table/course_video_resolution_profile.yaml +0 -22
- package/hedhog/table/video_resolution_profile.yaml +0 -18
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
- package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
- package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
|
@@ -24,6 +24,13 @@ type XpSegmentAiResult = {
|
|
|
24
24
|
learningTypes: { xpLearningTypeId: number; weightPercent: number }[];
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
type LessonMeta = {
|
|
28
|
+
type: 'video' | 'text' | 'quiz';
|
|
29
|
+
content: string | null;
|
|
30
|
+
description: string | null;
|
|
31
|
+
durationSeconds: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
27
34
|
@Injectable()
|
|
28
35
|
export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
|
|
29
36
|
private readonly logger = new Logger(LessonXpAiCalculationService.name);
|
|
@@ -45,15 +52,45 @@ export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
|
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
async triggerCalculation(lessonId: number, userId: number) {
|
|
48
|
-
const
|
|
49
|
-
`SELECT id FROM course_lesson_transcription_segment WHERE course_lesson_id = $1::int LIMIT 1`,
|
|
50
|
-
lessonId,
|
|
51
|
-
);
|
|
55
|
+
const lesson = await this.fetchLessonMeta(lessonId);
|
|
52
56
|
|
|
53
|
-
if (!
|
|
54
|
-
throw new BadRequestException(
|
|
55
|
-
|
|
57
|
+
if (!lesson) {
|
|
58
|
+
throw new BadRequestException('Aula não encontrada.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (lesson.type === 'video') {
|
|
62
|
+
const segments = await this.prisma.$queryRawUnsafe<{ id: number }[]>(
|
|
63
|
+
`SELECT id FROM course_lesson_transcription_segment WHERE course_lesson_id = $1::int LIMIT 1`,
|
|
64
|
+
lessonId,
|
|
65
|
+
);
|
|
66
|
+
if (!segments.length) {
|
|
67
|
+
throw new BadRequestException(
|
|
68
|
+
'Esta aula ainda não possui transcrição. Gere ou importe a transcrição antes de calcular o XP.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
} else if (lesson.type === 'text') {
|
|
72
|
+
if (!lesson.durationSeconds || lesson.durationSeconds <= 0) {
|
|
73
|
+
throw new BadRequestException(
|
|
74
|
+
'Esta aula do tipo post precisa ter uma duração prevista configurada antes de calcular o XP.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
let parsed: any = {};
|
|
78
|
+
try { parsed = JSON.parse(lesson.content ?? '{}'); } catch { /* ignore */ }
|
|
79
|
+
if (!parsed.conteudoPost && !lesson.description) {
|
|
80
|
+
throw new BadRequestException(
|
|
81
|
+
'Esta aula do tipo post não possui conteúdo nem descrição pública. Adicione o conteúdo antes de calcular o XP.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
} else if (lesson.type === 'quiz') {
|
|
85
|
+
const questions = await this.prisma.$queryRawUnsafe<{ id: number }[]>(
|
|
86
|
+
`SELECT id FROM course_lesson_question WHERE course_lesson_id = $1::int LIMIT 1`,
|
|
87
|
+
lessonId,
|
|
56
88
|
);
|
|
89
|
+
if (!questions.length) {
|
|
90
|
+
throw new BadRequestException(
|
|
91
|
+
'Esta aula do tipo questão não possui questões vinculadas. Adicione questões antes de calcular o XP.',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
57
94
|
}
|
|
58
95
|
|
|
59
96
|
const existing = await this.prisma.$queryRawUnsafe<{ id: number }[]>(
|
|
@@ -129,18 +166,53 @@ export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
|
|
|
129
166
|
this.logger.log(`Starting XP calculation for lesson ${lessonId}, map ${mapId}`);
|
|
130
167
|
|
|
131
168
|
try {
|
|
132
|
-
const
|
|
133
|
-
{ startSeconds: number; endSeconds: number; text: string }[]
|
|
134
|
-
>(
|
|
135
|
-
`SELECT start_seconds::float AS "startSeconds", end_seconds::float AS "endSeconds", text
|
|
136
|
-
FROM course_lesson_transcription_segment
|
|
137
|
-
WHERE course_lesson_id = $1
|
|
138
|
-
ORDER BY start_seconds ASC`,
|
|
139
|
-
lessonId,
|
|
140
|
-
);
|
|
169
|
+
const lesson = await this.fetchLessonMeta(lessonId);
|
|
141
170
|
|
|
142
|
-
if (!
|
|
143
|
-
await this.markError(mapId, 'Aula
|
|
171
|
+
if (!lesson) {
|
|
172
|
+
await this.markError(mapId, 'Aula não encontrada.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let contentText: string;
|
|
177
|
+
const durationSeconds = lesson.durationSeconds ?? 0;
|
|
178
|
+
|
|
179
|
+
if (lesson.type === 'video') {
|
|
180
|
+
const transcriptionRows = await this.prisma.$queryRawUnsafe<
|
|
181
|
+
{ startSeconds: number; endSeconds: number; text: string }[]
|
|
182
|
+
>(
|
|
183
|
+
`SELECT start_seconds::float AS "startSeconds", end_seconds::float AS "endSeconds", text
|
|
184
|
+
FROM course_lesson_transcription_segment
|
|
185
|
+
WHERE course_lesson_id = $1
|
|
186
|
+
ORDER BY start_seconds ASC`,
|
|
187
|
+
lessonId,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (!transcriptionRows.length) {
|
|
191
|
+
await this.markError(mapId, 'Aula sem segmentos de transcrição.');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
contentText = transcriptionRows
|
|
196
|
+
.map((s) => {
|
|
197
|
+
const start = this.formatSeconds(s.startSeconds);
|
|
198
|
+
const end = this.formatSeconds(s.endSeconds);
|
|
199
|
+
return `[${start} - ${end}] ${s.text}`;
|
|
200
|
+
})
|
|
201
|
+
.join('\n');
|
|
202
|
+
} else if (lesson.type === 'text') {
|
|
203
|
+
contentText = this.buildPostContentText(lesson.content, lesson.description);
|
|
204
|
+
if (!contentText.trim()) {
|
|
205
|
+
await this.markError(mapId, 'Aula do tipo post sem conteúdo disponível para análise.');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
} else if (lesson.type === 'quiz') {
|
|
209
|
+
contentText = await this.buildQuizContentText(lessonId);
|
|
210
|
+
if (!contentText.trim()) {
|
|
211
|
+
await this.markError(mapId, 'Aula do tipo questão sem questões disponíveis para análise.');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
await this.markError(mapId, `Tipo de aula não suportado: ${(lesson as any).type}`);
|
|
144
216
|
return;
|
|
145
217
|
}
|
|
146
218
|
|
|
@@ -191,15 +263,7 @@ export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
|
|
|
191
263
|
|
|
192
264
|
const aiConfig = buildAiConfigFromIntegration(profile.integration_provider.slug, profile.config);
|
|
193
265
|
|
|
194
|
-
const
|
|
195
|
-
.map((s) => {
|
|
196
|
-
const start = this.formatSeconds(s.startSeconds);
|
|
197
|
-
const end = this.formatSeconds(s.endSeconds);
|
|
198
|
-
return `[${start} - ${end}] ${s.text}`;
|
|
199
|
-
})
|
|
200
|
-
.join('\n');
|
|
201
|
-
|
|
202
|
-
const prompt = this.buildPrompt(transcriptionText, areas, skills, learningTypes);
|
|
266
|
+
const prompt = this.buildPrompt(contentText, areas, skills, learningTypes, lesson.type, durationSeconds);
|
|
203
267
|
|
|
204
268
|
const response = await axios.post(
|
|
205
269
|
'https://api.openai.com/v1/chat/completions',
|
|
@@ -330,6 +394,85 @@ export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
|
|
|
330
394
|
}
|
|
331
395
|
}
|
|
332
396
|
|
|
397
|
+
private async fetchLessonMeta(lessonId: number): Promise<LessonMeta | null> {
|
|
398
|
+
const rows = await this.prisma.$queryRawUnsafe<LessonMeta[]>(
|
|
399
|
+
`SELECT type::text AS type, content, description, duration_seconds AS "durationSeconds"
|
|
400
|
+
FROM course_lesson WHERE id = $1`,
|
|
401
|
+
lessonId,
|
|
402
|
+
);
|
|
403
|
+
return rows[0] ?? null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private buildPostContentText(content: string | null, description: string | null): string {
|
|
407
|
+
const parts: string[] = [];
|
|
408
|
+
|
|
409
|
+
if (description?.trim()) {
|
|
410
|
+
parts.push(`Descrição pública:\n${description.trim()}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let postContent = '';
|
|
414
|
+
try {
|
|
415
|
+
const parsed = JSON.parse(content ?? '{}');
|
|
416
|
+
postContent = String(parsed.conteudoPost ?? '').trim();
|
|
417
|
+
} catch { /* ignore */ }
|
|
418
|
+
|
|
419
|
+
if (postContent) {
|
|
420
|
+
parts.push(`Conteúdo do post:\n${this.stripHtml(postContent)}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return parts.join('\n\n');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private async buildQuizContentText(lessonId: number): Promise<string> {
|
|
427
|
+
const questions = await this.prisma.$queryRawUnsafe<{
|
|
428
|
+
statement: string;
|
|
429
|
+
explanation: string | null;
|
|
430
|
+
options: { text: string; isCorrect: boolean; feedback: string | null }[] | null;
|
|
431
|
+
}[]>(
|
|
432
|
+
`SELECT
|
|
433
|
+
q.statement,
|
|
434
|
+
q.explanation,
|
|
435
|
+
COALESCE(
|
|
436
|
+
json_agg(
|
|
437
|
+
json_build_object('text', o.option_text, 'isCorrect', o.is_correct, 'feedback', o.feedback)
|
|
438
|
+
ORDER BY o.position ASC
|
|
439
|
+
) FILTER (WHERE o.id IS NOT NULL),
|
|
440
|
+
'[]'
|
|
441
|
+
) AS options
|
|
442
|
+
FROM course_lesson_question clq
|
|
443
|
+
JOIN question q ON q.id = clq.question_id
|
|
444
|
+
LEFT JOIN exam_option o ON o.question_id = q.id
|
|
445
|
+
WHERE clq.course_lesson_id = $1
|
|
446
|
+
GROUP BY clq.order, q.statement, q.explanation
|
|
447
|
+
ORDER BY clq.order ASC`,
|
|
448
|
+
lessonId,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (!questions.length) return '';
|
|
452
|
+
|
|
453
|
+
return questions.map((q, i) => {
|
|
454
|
+
const lines: string[] = [`Questão ${i + 1}:`];
|
|
455
|
+
lines.push(`Enunciado: ${q.statement}`);
|
|
456
|
+
|
|
457
|
+
const opts = Array.isArray(q.options) ? q.options : [];
|
|
458
|
+
if (opts.length) {
|
|
459
|
+
const labels = 'abcdefghijklmnopqrstuvwxyz';
|
|
460
|
+
lines.push(
|
|
461
|
+
'Alternativas: ' +
|
|
462
|
+
opts
|
|
463
|
+
.map((o, idx) => `${labels[idx] ?? idx + 1}) ${o.text}${o.isCorrect ? ' [CORRETA]' : ''}`)
|
|
464
|
+
.join('; '),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (q.explanation?.trim()) {
|
|
469
|
+
lines.push(`Explicação: ${q.explanation.trim()}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return lines.join('\n');
|
|
473
|
+
}).join('\n\n');
|
|
474
|
+
}
|
|
475
|
+
|
|
333
476
|
private async markError(mapId: number, message: string) {
|
|
334
477
|
await this.prisma.$executeRawUnsafe(
|
|
335
478
|
`UPDATE lesson_xp_map
|
|
@@ -343,12 +486,30 @@ export class LessonXpAiCalculationService implements OnModuleInit, IJobHandler {
|
|
|
343
486
|
}
|
|
344
487
|
|
|
345
488
|
private buildPrompt(
|
|
346
|
-
|
|
489
|
+
content: string,
|
|
347
490
|
areas: { id: number; slug: string; name: string }[],
|
|
348
491
|
skills: { id: number; slug: string; name: string; areaId: number | null }[],
|
|
349
492
|
learningTypes: { id: number; slug: string; name: string; multiplier: number }[],
|
|
493
|
+
lessonType: 'video' | 'text' | 'quiz',
|
|
494
|
+
durationSeconds: number,
|
|
350
495
|
): string {
|
|
351
|
-
|
|
496
|
+
const totalMinutes = Math.max(Math.ceil(durationSeconds / 60), 1);
|
|
497
|
+
|
|
498
|
+
const contentHeader =
|
|
499
|
+
lessonType === 'video'
|
|
500
|
+
? 'TRANSCRIÇÃO DA AULA'
|
|
501
|
+
: lessonType === 'text'
|
|
502
|
+
? 'CONTEÚDO DO POST'
|
|
503
|
+
: 'QUESTÕES DA AULA';
|
|
504
|
+
|
|
505
|
+
const segmentInstruction =
|
|
506
|
+
lessonType === 'video'
|
|
507
|
+
? 'Analise a transcrição abaixo e mapeie as competências técnicas de cada segmento de ~60 segundos.'
|
|
508
|
+
: lessonType === 'text'
|
|
509
|
+
? `Analise o conteúdo do post abaixo (duração estimada: ${totalMinutes} minutos) e mapeie as competências técnicas. Divida o conteúdo em segmentos de ~60 segundos cobrindo a duração total. Os valores de startSeconds e endSeconds devem cobrir de 0 até ${durationSeconds} segundos.`
|
|
510
|
+
: `Analise as questões da aula abaixo (duração estimada: ${totalMinutes} minutos). Cada questão representa um segmento; distribua o tempo total de ${durationSeconds} segundos igualmente entre as questões. Os valores de startSeconds e endSeconds devem cobrir de 0 até ${durationSeconds} segundos.`;
|
|
511
|
+
|
|
512
|
+
return `${segmentInstruction}
|
|
352
513
|
|
|
353
514
|
CATÁLOGO DE ÁREAS MACRO (use APENAS estes IDs):
|
|
354
515
|
${JSON.stringify(areas.map((a) => ({ id: a.id, slug: a.slug, name: a.name })))}
|
|
@@ -359,8 +520,8 @@ ${JSON.stringify(skills.map((s) => ({ id: s.id, slug: s.slug, name: s.name, area
|
|
|
359
520
|
CATÁLOGO DE TIPOS DE APRENDIZADO (use APENAS estes IDs):
|
|
360
521
|
${JSON.stringify(learningTypes.map((l) => ({ id: l.id, slug: l.slug, name: l.name, multiplier: l.multiplier })))}
|
|
361
522
|
|
|
362
|
-
|
|
363
|
-
${
|
|
523
|
+
${contentHeader}:
|
|
524
|
+
${content}
|
|
364
525
|
|
|
365
526
|
Retorne um JSON com a chave "segments" contendo um array onde cada item representa um segmento de ~60s:
|
|
366
527
|
{
|
|
@@ -388,6 +549,19 @@ Regras obrigatórias:
|
|
|
388
549
|
- aiConfidence: 0 a 1, indica certeza do mapeamento.`;
|
|
389
550
|
}
|
|
390
551
|
|
|
552
|
+
private stripHtml(html: string): string {
|
|
553
|
+
return html
|
|
554
|
+
.replace(/<[^>]+>/g, ' ')
|
|
555
|
+
.replace(/&/g, '&')
|
|
556
|
+
.replace(/</g, '<')
|
|
557
|
+
.replace(/>/g, '>')
|
|
558
|
+
.replace(/"/g, '"')
|
|
559
|
+
.replace(/'/g, "'")
|
|
560
|
+
.replace(/ /g, ' ')
|
|
561
|
+
.replace(/\s{2,}/g, ' ')
|
|
562
|
+
.trim();
|
|
563
|
+
}
|
|
564
|
+
|
|
391
565
|
private formatSeconds(s: number): string {
|
|
392
566
|
const m = Math.floor(s / 60);
|
|
393
567
|
const sec = Math.floor(s % 60);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { IntegrationDeveloperApiService } from '@hed-hog/core';
|
|
2
|
+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
3
|
+
|
|
4
|
+
type SubscriptionActivatedPayload = {
|
|
5
|
+
subscription_id?: number | null;
|
|
6
|
+
customer_id?: number | null;
|
|
7
|
+
user_id?: number | null;
|
|
8
|
+
product_id?: number | null;
|
|
9
|
+
plan_version_id?: number | null;
|
|
10
|
+
price_id?: number | null;
|
|
11
|
+
current_period_start?: string | null;
|
|
12
|
+
current_period_end?: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type SubscriptionCanceledPayload = {
|
|
16
|
+
subscription_id?: number | null;
|
|
17
|
+
reason?: string | null;
|
|
18
|
+
canceled_at?: string | null;
|
|
19
|
+
entitlement_revoked?: boolean | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class LmsCommerceAccessSubscriber implements OnModuleInit {
|
|
24
|
+
private readonly logger = new Logger(LmsCommerceAccessSubscriber.name);
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly integrationApi: IntegrationDeveloperApiService,
|
|
28
|
+
) {}
|
|
29
|
+
|
|
30
|
+
onModuleInit(): void {
|
|
31
|
+
this.integrationApi.subscribeMany([
|
|
32
|
+
{
|
|
33
|
+
eventName: 'commerce.subscription.activated',
|
|
34
|
+
consumerName: 'lms.commerce-subscription-activated',
|
|
35
|
+
priority: 0,
|
|
36
|
+
handler: async (event) => this.onSubscriptionActivated(event),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
eventName: 'commerce.subscription.canceled',
|
|
40
|
+
consumerName: 'lms.commerce-subscription-canceled',
|
|
41
|
+
priority: 0,
|
|
42
|
+
handler: async (event) => this.onSubscriptionCanceled(event),
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async onSubscriptionActivated(event: any): Promise<void> {
|
|
48
|
+
const payload = (event.payload ?? {}) as SubscriptionActivatedPayload;
|
|
49
|
+
const userId = Number(payload.user_id ?? 0);
|
|
50
|
+
const productId = Number(payload.product_id ?? 0);
|
|
51
|
+
const subscriptionId = Number(payload.subscription_id ?? 0);
|
|
52
|
+
|
|
53
|
+
if (!userId || !productId) {
|
|
54
|
+
this.logger.warn(
|
|
55
|
+
`commerce.subscription.activated: missing user_id or product_id (subscription ${subscriptionId}) — skipping LMS side effects`,
|
|
56
|
+
);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.logger.log(
|
|
61
|
+
`LMS access granted: subscription ${subscriptionId} activated for user ${userId} / product ${productId}`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Extension point: look up integration links product_id → lms course/plan
|
|
65
|
+
// and auto-enroll the student in the corresponding class group.
|
|
66
|
+
// Example shape of what would go here:
|
|
67
|
+
//
|
|
68
|
+
// const links = await this.integrationApi.findLinksBySource({
|
|
69
|
+
// module: 'commerce',
|
|
70
|
+
// entityType: 'product',
|
|
71
|
+
// entityId: String(productId),
|
|
72
|
+
// });
|
|
73
|
+
// for (const link of links.filter(l => l.targetModule === 'lms' && l.targetEntityType === 'course')) {
|
|
74
|
+
// await this.classGroupService.enrollStudent(Number(link.targetEntityId), userId);
|
|
75
|
+
// }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async onSubscriptionCanceled(event: any): Promise<void> {
|
|
79
|
+
const payload = (event.payload ?? {}) as SubscriptionCanceledPayload;
|
|
80
|
+
const subscriptionId = Number(payload.subscription_id ?? 0);
|
|
81
|
+
|
|
82
|
+
this.logger.log(
|
|
83
|
+
`LMS access revoked: subscription ${subscriptionId} canceled (reason: ${payload.reason ?? 'none'}, entitlement_revoked: ${payload.entitlement_revoked})`,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Extension point: remove student from active class groups linked to this subscription.
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/lms.module.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { LocaleModule } from '@hed-hog/api-locale';
|
|
|
2
2
|
import { PaginationModule } from '@hed-hog/api-pagination';
|
|
3
3
|
import { PrismaModule } from '@hed-hog/api-prisma';
|
|
4
4
|
import { CoreModule } from '@hed-hog/core';
|
|
5
|
+
import { CommerceModule } from '@hed-hog/commerce';
|
|
5
6
|
import { QueueModule } from '@hed-hog/queue';
|
|
6
7
|
import { forwardRef, Module } from '@nestjs/common';
|
|
7
8
|
import { ConfigModule } from '@nestjs/config';
|
|
@@ -11,8 +12,8 @@ import { LmsCertificateModule } from './certificate/certificate.module';
|
|
|
11
12
|
import { ClassGroupModule } from './class-group/class-group.module';
|
|
12
13
|
import { CourseLessonDiscussionModule } from './course-lesson-discussion/course-lesson-discussion.module';
|
|
13
14
|
import { CourseLessonNoteModule } from './course-lesson-note/course-lesson-note.module';
|
|
15
|
+
import { LmsCommerceAccessSubscriber } from './lms-commerce-access.subscriber';
|
|
14
16
|
import { CourseAudioTranscriptionService } from './course/course-audio-transcription.service';
|
|
15
|
-
import { LmsBulkUploadAutomationService } from './course/lms-bulk-upload-automation.service';
|
|
16
17
|
import { CourseModule } from './course/course.module';
|
|
17
18
|
import { LmsDashboardModule } from './dashboard/dashboard.module';
|
|
18
19
|
import { EnterpriseModule } from './enterprise/enterprise.module';
|
|
@@ -21,14 +22,19 @@ import { EvaluationModule } from './evaluation/evaluation.module';
|
|
|
21
22
|
import { ExamModule } from './exam/exam.module';
|
|
22
23
|
import { InstructorModule } from './instructor/instructor.module';
|
|
23
24
|
import { LessonXpMapModule } from './lesson-xp-map/lesson-xp-map.module';
|
|
25
|
+
import { EmitCertificateHandler } from './platforma/handlers/emit-certificate.handler';
|
|
26
|
+
import { LessonHeartbeatHandler } from './platforma/handlers/lesson-heartbeat.handler';
|
|
24
27
|
import { PlataformaController } from './platforma/platforma.controller';
|
|
28
|
+
import { PlatformaHeartbeatService } from './platforma/platforma-heartbeat.service';
|
|
29
|
+
import { PlatformaPerformanceService } from './platforma/platforma-performance.service';
|
|
30
|
+
import { PlatformaSearchService } from './platforma/platforma-search.service';
|
|
31
|
+
import { PlatformaVideoService } from './platforma/platforma-video.service';
|
|
25
32
|
import { StudentXpModule } from './student-xp/student-xp.module';
|
|
26
33
|
import { XpCatalogModule } from './xp-catalog/xp-catalog.module';
|
|
27
34
|
import { PlatformaService } from './platforma/platforma.service';
|
|
28
35
|
import { LmsRealtimeModule } from './realtime/lms-realtime.module';
|
|
29
36
|
import { LmsReportsModule } from './reports/reports.module';
|
|
30
37
|
import { TrainingModule } from './training/training.module';
|
|
31
|
-
import { VideoResolutionProfileModule } from './video-resolution-profile/video-resolution-profile.module';
|
|
32
38
|
|
|
33
39
|
@Module({
|
|
34
40
|
imports: [
|
|
@@ -37,6 +43,7 @@ import { VideoResolutionProfileModule } from './video-resolution-profile/video-r
|
|
|
37
43
|
forwardRef(() => PrismaModule),
|
|
38
44
|
forwardRef(() => CoreModule),
|
|
39
45
|
forwardRef(() => QueueModule),
|
|
46
|
+
forwardRef(() => CommerceModule),
|
|
40
47
|
forwardRef(() => LocaleModule),
|
|
41
48
|
forwardRef(() => AchievementModule),
|
|
42
49
|
forwardRef(() => BitcodeWalletModule),
|
|
@@ -54,7 +61,6 @@ import { VideoResolutionProfileModule } from './video-resolution-profile/video-r
|
|
|
54
61
|
forwardRef(() => LmsRealtimeModule),
|
|
55
62
|
forwardRef(() => LmsReportsModule),
|
|
56
63
|
forwardRef(() => TrainingModule),
|
|
57
|
-
forwardRef(() => VideoResolutionProfileModule),
|
|
58
64
|
forwardRef(() => XpCatalogModule),
|
|
59
65
|
forwardRef(() => LessonXpMapModule),
|
|
60
66
|
forwardRef(() => StudentXpModule),
|
|
@@ -62,8 +68,14 @@ import { VideoResolutionProfileModule } from './video-resolution-profile/video-r
|
|
|
62
68
|
controllers: [PlataformaController],
|
|
63
69
|
providers: [
|
|
64
70
|
CourseAudioTranscriptionService,
|
|
65
|
-
LmsBulkUploadAutomationService,
|
|
66
71
|
PlatformaService,
|
|
72
|
+
PlatformaVideoService,
|
|
73
|
+
PlatformaHeartbeatService,
|
|
74
|
+
PlatformaPerformanceService,
|
|
75
|
+
PlatformaSearchService,
|
|
76
|
+
LessonHeartbeatHandler,
|
|
77
|
+
EmitCertificateHandler,
|
|
78
|
+
LmsCommerceAccessSubscriber,
|
|
67
79
|
],
|
|
68
80
|
exports: [
|
|
69
81
|
forwardRef(() => AchievementModule),
|
|
@@ -82,7 +94,6 @@ import { VideoResolutionProfileModule } from './video-resolution-profile/video-r
|
|
|
82
94
|
forwardRef(() => LmsRealtimeModule),
|
|
83
95
|
forwardRef(() => LmsReportsModule),
|
|
84
96
|
forwardRef(() => TrainingModule),
|
|
85
|
-
forwardRef(() => VideoResolutionProfileModule),
|
|
86
97
|
forwardRef(() => XpCatalogModule),
|
|
87
98
|
forwardRef(() => LessonXpMapModule),
|
|
88
99
|
forwardRef(() => StudentXpModule),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { IsBoolean, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
export class HeartbeatDto {
|
|
4
|
+
@IsInt()
|
|
5
|
+
lessonId: number;
|
|
6
|
+
|
|
7
|
+
@IsInt()
|
|
8
|
+
@Min(0)
|
|
9
|
+
positionSeconds: number;
|
|
10
|
+
|
|
11
|
+
@IsOptional()
|
|
12
|
+
@IsString()
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
|
|
15
|
+
@IsOptional()
|
|
16
|
+
@IsInt()
|
|
17
|
+
@Min(1)
|
|
18
|
+
@Max(10000)
|
|
19
|
+
screenWidth?: number;
|
|
20
|
+
|
|
21
|
+
@IsOptional()
|
|
22
|
+
@IsInt()
|
|
23
|
+
@Min(1)
|
|
24
|
+
@Max(10000)
|
|
25
|
+
screenHeight?: number;
|
|
26
|
+
|
|
27
|
+
@IsOptional()
|
|
28
|
+
@IsBoolean()
|
|
29
|
+
isTouch?: boolean;
|
|
30
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import { IJobHandler, QueueHandlerRegistry } from '@hed-hog/queue';
|
|
3
|
+
import { Inject, Injectable, Logger, OnModuleInit, forwardRef } from '@nestjs/common';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
5
|
+
|
|
6
|
+
export const EMIT_CERTIFICATE_JOB = 'lms.emit_certificate';
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class EmitCertificateHandler implements OnModuleInit, IJobHandler {
|
|
10
|
+
private readonly logger = new Logger(EmitCertificateHandler.name);
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
@Inject(forwardRef(() => PrismaService))
|
|
14
|
+
private readonly prisma: PrismaService,
|
|
15
|
+
@Inject(forwardRef(() => QueueHandlerRegistry))
|
|
16
|
+
private readonly registry: QueueHandlerRegistry,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
onModuleInit() {
|
|
20
|
+
this.registry.register(EMIT_CERTIFICATE_JOB, this);
|
|
21
|
+
this.logger.log(`Registered handler for "${EMIT_CERTIFICATE_JOB}"`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async handle(job: { payload: Record<string, any> }) {
|
|
25
|
+
const { enrollmentId, courseId, personId } = job.payload as {
|
|
26
|
+
enrollmentId: number;
|
|
27
|
+
courseId: number;
|
|
28
|
+
personId: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Idempotency guard — re-entrancy safe
|
|
32
|
+
const enrollment = await (this.prisma as any).course_enrollment.findUnique({
|
|
33
|
+
where: { id: enrollmentId },
|
|
34
|
+
select: { certificate_issued_at: true, completed_at: true, final_score: true },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!enrollment) {
|
|
38
|
+
this.logger.warn(`Enrollment ${enrollmentId} not found, skipping certificate`);
|
|
39
|
+
return { skipped: true };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (enrollment.certificate_issued_at) {
|
|
43
|
+
this.logger.log(`Certificate already issued for enrollment ${enrollmentId}`);
|
|
44
|
+
return { skipped: true, alreadyIssued: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [person, course] = await Promise.all([
|
|
48
|
+
this.prisma.person.findUnique({
|
|
49
|
+
where: { id: personId },
|
|
50
|
+
select: { name: true },
|
|
51
|
+
}),
|
|
52
|
+
(this.prisma as any).course.findUnique({
|
|
53
|
+
where: { id: courseId },
|
|
54
|
+
select: {
|
|
55
|
+
title: true,
|
|
56
|
+
certificate_workload: true,
|
|
57
|
+
has_certificate: true,
|
|
58
|
+
certificate_template_id: true,
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
if (!person || !course) {
|
|
64
|
+
this.logger.warn(`Person or course not found (enrollment ${enrollmentId})`);
|
|
65
|
+
return { skipped: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!course.has_certificate || !course.certificate_template_id) {
|
|
69
|
+
this.logger.log(`Course ${courseId} does not issue certificates`);
|
|
70
|
+
return { skipped: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Compute workload hours from lesson durations when not set on the course
|
|
74
|
+
let workloadHours: number = course.certificate_workload ?? 0;
|
|
75
|
+
if (!workloadHours) {
|
|
76
|
+
const agg = await (this.prisma as any).course_lesson.aggregate({
|
|
77
|
+
where: { published: true, course_module: { course_id: courseId } },
|
|
78
|
+
_sum: { duration_seconds: true },
|
|
79
|
+
});
|
|
80
|
+
const totalSeconds = (agg._sum?.duration_seconds as number) ?? 0;
|
|
81
|
+
workloadHours = Math.max(1, Math.ceil(totalSeconds / 3600));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const issuedAt = new Date();
|
|
85
|
+
const verificationCode = randomBytes(12).toString('hex').toUpperCase();
|
|
86
|
+
|
|
87
|
+
await (this.prisma as any).certificate.create({
|
|
88
|
+
data: {
|
|
89
|
+
verification_code: verificationCode,
|
|
90
|
+
student_id: personId,
|
|
91
|
+
course_enrollment_id: enrollmentId,
|
|
92
|
+
course_id: courseId,
|
|
93
|
+
certificate_template_id: course.certificate_template_id,
|
|
94
|
+
certificate_type: 'course',
|
|
95
|
+
student_name: person.name ?? '',
|
|
96
|
+
course_name: course.title ?? '',
|
|
97
|
+
workload_hours: workloadHours,
|
|
98
|
+
issued_at: issuedAt,
|
|
99
|
+
completed_at: enrollment.completed_at ?? issuedAt,
|
|
100
|
+
final_score: enrollment.final_score ?? null,
|
|
101
|
+
public_access: false,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Mark enrollment as issued — prevents duplicate certificates on any future re-run
|
|
106
|
+
await (this.prisma as any).course_enrollment.update({
|
|
107
|
+
where: { id: enrollmentId },
|
|
108
|
+
data: { certificate_issued_at: issuedAt },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.logger.log(
|
|
112
|
+
`Certificate issued — enrollment=${enrollmentId} course=${courseId} code=${verificationCode}`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return { verificationCode };
|
|
116
|
+
}
|
|
117
|
+
}
|